Compare commits

...

268 Commits

Author SHA1 Message Date
Jim McDonald
3059aa270f Update version. 2025-05-13 20:58:32 +01:00
Jim McDonald
8b7aab7180 Merge pull request #165 from aimxhaisse/fix/eip-7044
Support of EIP-7044 in exit verify cmd
2025-04-07 09:56:39 +01:00
Jim McDonald
c46483586d Add ETH values to epoch summary. 2025-04-03 10:13:45 +01:00
s. rannou
e4f0b934d7 Support of EIP-7044 in exit verify cmd 2025-04-01 23:28:23 +02:00
Jim McDonald
25a5bd917f Support 0 validators for epoch summary. 2025-03-18 10:42:39 +00:00
Jim McDonald
889a884f6e Do not fetch all validators if no validators to parse. 2025-03-18 10:39:57 +00:00
Jim McDonald
1c23926295 Add support for hoodi. 2025-03-16 08:00:45 +00:00
Jim McDonald
992a969eaf Update dependencies. 2025-03-16 07:58:10 +00:00
Jim McDonald
58d8587471 Tidy-ups. 2025-03-16 07:55:00 +00:00
Jim McDonald
b89154ada3 Add "block trail". 2025-02-26 09:40:03 +00:00
Jim McDonald
b9fff0dbde Handle blocks without blobs. 2025-02-25 12:54:58 +00:00
Jim McDonald
0238130895 Fix incorrect argument binding. 2025-02-22 07:23:40 +00:00
Jim McDonald
60b6ce44f1 Merge pull request #159 from wealdtech/update_index
Handle electra attestations for epoch summary
2025-02-22 07:14:47 +00:00
Chris Berry
24486f0175 Merge pull request #160 from wealdtech/fix_help
Update help text to match args
2025-02-21 15:10:50 +00:00
Chris Berry
503bf9a996 Update help text to match args 2025-02-21 14:46:36 +00:00
Jim McDonald
1d14a57204 Update dependencies. 2025-02-17 09:26:00 +00:00
Jim McDonald
842d603524 Update workflow. 2025-02-16 15:43:20 +00:00
Jim McDonald
c3471240a5 Update workflows. 2025-02-16 15:42:52 +00:00
Chris Berry
7b4ea7e27e Handle electra attestations for epoch summary 2025-02-14 11:54:05 +00:00
Jim McDonald
4d3bd966e0 Use standard mock from go-eth2-client. 2025-02-08 11:46:49 +00:00
Jim McDonald
0488b13ba1 Updates for electra. 2025-02-08 11:34:55 +00:00
Jim McDonald
f8a611d63d Merge pull request #157 from wealdtech/electra
Electra
2025-02-08 10:41:46 +00:00
Chris Berry
772f07f8ec Merge pull request #156 from wealdtech/electra_merge
Update go-eth2-client and merge master changes
2025-02-07 09:22:35 +00:00
Chris Berry
f6e23d803b Update go-eth2-client 2025-02-06 16:43:26 +00:00
Chris Berry
86e872294d Merge branch 'master' into electra_merge
# Conflicts:
#	CHANGELOG.md
2025-02-06 16:42:26 +00:00
Jim McDonald
d7d9c66052 Update Dockerfile. 2025-01-31 17:14:12 +00:00
Jim McDonald
3e173f141e Order deposit data from HD wallet accounts by path. 2025-01-31 17:04:50 +00:00
Jim McDonald
5d95e93b76 Allow blockid for validator info. 2025-01-31 17:03:48 +00:00
Jim McDonald
005eed1360 Avoid corner case when deriving from mnemonic. 2025-01-28 19:32:01 +00:00
Chris Berry
34b752edcc Merge pull request #154 from wealdtech/electra_updates
Update to use electra version of go-eth2-client
2025-01-27 11:10:03 +00:00
Chris Berry
f17fe2f5cb Refactor based on review 2025-01-27 10:23:30 +00:00
Chris Berry
8322353af5 Update docker file 2025-01-27 10:08:48 +00:00
Chris Berry
7d0e607f96 Update to use electra version of go-eth2-client 2025-01-27 10:01:36 +00:00
Jim McDonald
fcafa037f8 Bump version. 2024-12-20 22:54:25 +00:00
Jim McDonald
8e1c8c5300 New workflow 2024-12-20 22:53:41 +00:00
Jim McDonald
5af1476bc3 Bump version. 2024-12-20 22:47:59 +00:00
Jim McDonald
24a4220804 Update workflow. 2024-12-20 22:47:25 +00:00
Jim McDonald
fa1d4d60fa Update dependencies. 2024-12-20 22:41:00 +00:00
Jim McDonald
7d00d1261f Avoid crash when signing and verifing signatures using keys rather than accounts. 2024-10-22 22:55:08 +01:00
Jim McDonald
ac85a9539b Update workflow. 2024-10-21 20:25:41 +01:00
Jim McDonald
099e434f43 Update workflows. 2024-10-21 19:50:23 +01:00
Jim McDonald
5bab79bd79 Update workflow. 2024-10-21 19:47:03 +01:00
Jim McDonald
1defa3b121 Merge pull request #149 from wealdtech/add-trin
Add trin.
2024-10-21 19:45:18 +01:00
Jim McDonald
6cb7b034aa Update workflows. 2024-10-21 19:42:14 +01:00
Jim McDonald
bd9659d71f Add trin. 2024-10-21 19:00:36 +01:00
Jim McDonald
68ca31e034 Merge pull request #147 from polskikh/fix-validate-deposit-data-json
Fix validating deposit message root for json output data
2024-10-04 12:16:02 +01:00
Vladislav Polskikh
a37a5f4af3 Fix validating deposit message root for json output data 2024-10-04 12:10:20 +02:00
Jim McDonald
47cf033feb Update workflows. 2024-10-01 13:18:52 +01:00
Jim McDonald
72a9390f97 Linting. 2024-10-01 13:02:39 +01:00
Jim McDonald
0276a72de6 Fix crash when block info has no blobs. 2024-10-01 13:02:07 +01:00
Jim McDonald
fd0a89c258 More data for epoch summaries. 2024-09-04 11:51:16 +01:00
Jim McDonald
1ac505f0bd Disable lint rule that generates false positives 2024-09-01 22:26:59 +01:00
Jim McDonald
b4124b7a27 Add keystore wallet. 2024-09-01 22:15:58 +01:00
Jim McDonald
f3ae2baf8f Allow derived keystore to be displayed on stdout.
Fixes #143
2024-08-02 09:46:25 +01:00
Jim McDonald
8aaf16ab3f Update dependencies. 2024-07-28 18:50:47 +01:00
Jim McDonald
61179045dd Provide client info in block info output. 2024-07-28 18:19:53 +01:00
Jim McDonald
e50b4f015b Update changelog. 2024-06-23 07:35:22 +01:00
Jim McDonald
6e522d6b5a Bump version. 2024-06-23 07:29:43 +01:00
Jim McDonald
d467a8ef9c Linting. 2024-06-18 12:52:53 +01:00
Jim McDonald
d51683426c Update README for S3. 2024-06-17 08:05:09 +01:00
Jim McDonald
4879443e51 Update linter rules. 2024-05-27 09:46:47 +01:00
Jim McDonald
cf9d5718a3 Update dependencies. 2024-05-27 09:42:21 +01:00
Jim McDonald
7e09068d30 Merge pull request #134 from lyfsn/max-distance-var
Add maxDistance as an input parameter
2024-04-29 10:09:21 +01:00
lyfsn
2362a1a058 fix default value for maxDistance in process
Signed-off-by: lyfsn <dev.wangyu@proton.me>
2024-04-29 11:15:13 +08:00
Jim McDonald
80373ae237 Merge pull request #135 from wealdtech/dependabot/go_modules/golang.org/x/net-0.23.0
Bump golang.org/x/net from 0.22.0 to 0.23.0
2024-04-19 14:34:32 +01:00
dependabot[bot]
bd2eea3a60 Bump golang.org/x/net from 0.22.0 to 0.23.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-19 13:21:56 +00:00
lyfsn
a23650a5ae Add maxDistance as an input parameter
Signed-off-by: lyfsn <dev.wangyu@proton.me>
2024-04-11 14:59:58 +08:00
Jim McDonald
327df13fba Update workflows. 2024-03-17 22:31:59 +00:00
Jim McDonald
eaa0a19711 Fix tests. 2024-03-17 22:20:48 +00:00
Jim McDonald
8aba766208 Add deposit contract address to "chain info". 2024-03-17 22:18:44 +00:00
Jim McDonald
dd68af4884 More debug info for validator withdrawal calculation. 2024-03-11 14:59:53 +00:00
Jim McDonald
564228ff00 Linting. 2024-03-04 16:34:21 +00:00
Jim McDonald
697b6d4230 Sort validators by index in chain info. 2024-03-04 16:32:06 +00:00
Jim McDonald
c548058190 Fix tests. 2024-01-31 13:17:32 +00:00
Jim McDonald
daa2ac7b6e Update launchpad output for 2.7.0 2024-01-31 13:16:30 +00:00
Jim McDonald
3b8a98bb83 Better error message on timeout. 2024-01-28 16:34:19 +00:00
Jim McDonald
f1021387b3 Update dependencies. 2024-01-22 13:03:09 +00:00
Jim McDonald
cd27401514 Increase efficiency of fetching many validators. 2024-01-18 09:32:47 +00:00
Jim McDonald
faf3c8afa4 Handle empty blocks. 2024-01-17 11:39:09 +00:00
Jim McDonald
6ad8e7afe4 Update version. 2024-01-16 12:35:59 +00:00
Jim McDonald
1cffa7051d Tidy-ups for deneb. 2024-01-16 12:35:18 +00:00
Jim McDonald
0e089bc8ff Include period start and end in eth1 votes output. 2024-01-16 12:29:16 +00:00
Jim McDonald
c6f90a69af Merge branch 'master' of github.com:wealdtech/ethdo 2023-12-25 09:08:26 +00:00
Jim McDonald
58c3a7e279 Bump version. 2023-12-25 09:08:12 +00:00
Jim McDonald
cd1bf4dcfc Fix period parsing for synccommittee members 2023-12-25 09:07:33 +00:00
Jim McDonald
0a24f4ffe6 Merge pull request #124 from wealdtech/dependabot/go_modules/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc-0.46.0
Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc from 0.45.0 to 0.46.0
2023-12-20 12:22:03 +00:00
Jim McDonald
77dd02a8f1 Merge pull request #126 from wealdtech/dependabot/go_modules/golang.org/x/crypto-0.17.0
Bump golang.org/x/crypto from 0.16.0 to 0.17.0
2023-12-20 12:20:48 +00:00
Jim McDonald
f4c99ac2b1 Update minimum version of go.
Fixes #127
2023-12-20 12:18:59 +00:00
dependabot[bot]
062b968055 Bump golang.org/x/crypto from 0.16.0 to 0.17.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-19 00:06:46 +00:00
dependabot[bot]
8083dd7eeb Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
Bumps [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib) from 0.45.0 to 0.46.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.45.0...zpages/v0.46.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-07 13:47:00 +00:00
Jim McDonald
841ba8e8f0 Update dependencies for deneb beta 5. 2023-12-07 13:45:01 +00:00
Jim McDonald
32a34fb9c1 Separate current and exit fork versions. 2023-11-29 12:09:15 +00:00
Jim McDonald
e356e9e1b7 Update dependencies. 2023-10-31 13:17:44 +00:00
Jim McDonald
82f5200296 Release 1.33.2 2023-10-19 14:46:13 +01:00
Jim McDonald
36ddb1a2fe Merge pull request #114 from wealdtech/dependabot/go_modules/golang.org/x/net-0.17.0
Bump golang.org/x/net from 0.13.0 to 0.17.0
2023-10-13 16:37:21 +01:00
dependabot[bot]
d4791ac8bd Bump golang.org/x/net from 0.13.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.13.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.13.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-11 23:02:40 +00:00
Jim McDonald
fff6469748 Version 1.33.1 2023-09-02 22:24:37 +01:00
Jim McDonald
b11f9cf3a3 Add sepolia support. 2023-08-22 21:56:10 +01:00
Jim McDonald
9d50a72270 Add activation timestamp to validator info. 2023-08-22 09:52:52 +01:00
Jim McDonald
9191cac389 Revert release changes. 2023-08-20 23:34:08 +02:00
Jim McDonald
f1a1d4f64c t 2023-08-20 23:22:05 +02:00
Jim McDonald
1f1ddc1719 Add slot option for proposer duties. 2023-08-16 15:47:06 +02:00
Jim McDonald
92d964f092 Update version. 2023-08-08 07:31:12 +01:00
Jim McDonald
972e35f7d7 Version 1.33.0 2023-08-07 20:54:04 +01:00
Jim McDonald
5f4d7a389d Remove local module directives. 2023-08-07 20:53:02 +01:00
Jim McDonald
696ba64238 Remove local module directives. 2023-08-07 20:52:09 +01:00
Jim McDonald
aeb8142b27 Fix tests with changes to dependency error output. 2023-08-07 20:49:32 +01:00
Jim McDonald
a1ea298983 Add wallet batch command. 2023-08-07 20:47:00 +01:00
Jim McDonald
1c23e7cc54 Update dependencies. 2023-08-04 12:45:54 +01:00
Jim McDonald
b53e42aeb7 Rename fields for latest deneb release. 2023-08-04 12:38:53 +01:00
Jim McDonald
fa325e20f8 Show all epoch slots in synccommittee inclusion. 2023-07-15 22:39:51 +01:00
Jim McDonald
0ce563470f Add blobs to epoch summary. 2023-07-08 22:07:42 +01:00
Jim McDonald
abd3567d05 Merge branch 'master' of github.com:wealdtech/ethdo 2023-07-08 22:04:41 +01:00
Jim McDonald
399d0eaa64 Increase default timeout to 30s. 2023-07-08 22:04:05 +01:00
Jim McDonald
655b9cbbc8 Merge pull request #110 from wealdtech/dependabot/go_modules/google.golang.org/grpc-1.53.0
Bump google.golang.org/grpc from 1.52.0 to 1.53.0
2023-07-06 18:19:49 +02:00
dependabot[bot]
50c22818cb Bump google.golang.org/grpc from 1.52.0 to 1.53.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.52.0 to 1.53.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.52.0...v1.53.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-05 21:41:57 +00:00
Jim McDonald
4991787b9d Merge pull request #109 from barnabasbusa/patch-1
Update usage.md
2023-07-05 11:51:00 +02:00
Barnabas Busa
b652d2e083 Update usage.md 2023-07-05 11:43:06 +02:00
Jim McDonald
ef58db3307 Merge pull request #108 from mksh/patch-1
Fix typo in cmd/root.go
2023-07-05 08:24:32 +02:00
mksh
a95851dc9e Fix typo in cmd/root.go 2023-07-05 03:21:42 +03:00
Jim McDonald
ac7e0985fb "validator summary" requires validators 2023-07-04 21:33:37 +02:00
Jim McDonald
7837165d46 Update block info. 2023-07-04 17:26:52 +02:00
Jim McDonald
46020396e4 Additional debug log for validator exits. 2023-07-02 18:00:08 +02:00
Jim McDonald
105de0a139 Merge branch 'master' of github.com:wealdtech/ethdo 2023-07-02 17:58:42 +02:00
Jim McDonald
a7da10360e Honour --quiet flag in block info command. 2023-07-02 17:58:26 +02:00
Jim McDonald
d5351cbe7f Merge pull request #107 from yorickdowne/patch-1
deposit data format for launchpad
2023-06-27 18:58:29 +02:00
yorickdowne
874839754c Tests use new version
2.5.0 and goerli in tests
2023-06-27 12:31:21 -04:00
yorickdowne
4db0cdae3a deposit data format for launchpad
Goerli launchpad expects "goerli" and version "2.5.0". Make those changes so generated deposit_data works with launchpad out of the box.
2023-06-27 11:37:48 -04:00
Jim McDonald
6f0f3e4c91 Add proposer index to block info output. 2023-06-15 10:54:06 +01:00
Jim McDonald
713bbdd60c Add epoch parameter to validator yield. 2023-06-13 00:22:32 +01:00
Jim McDonald
46ca70a615 Fix sync committee members test. 2023-06-13 00:20:38 +01:00
Jim McDonald
2f24bb7884 Update block info with blob information 2023-06-13 00:18:30 +01:00
Jim McDonald
9136c053b1 Remove print statement. 2023-06-12 12:40:50 +01:00
Jim McDonald
8efab62f8b Do not return error message when creating keystore. 2023-06-06 00:19:11 +01:00
Jim McDonald
7d723148ab linting. 2023-06-05 20:41:04 +01:00
Jim McDonald
2880ec9bdd Allow short form mnemonics. 2023-06-05 20:39:10 +01:00
Jim McDonald
871d1694ef Merge branch 'master' of github.com:wealdtech/ethdo 2023-05-31 11:44:59 +01:00
Jim McDonald
f26c9e9c4a Do not error on deposit verify.
If dpeosit verify was not given credentials it could not recreate the
deposit message, but rather than say this it errored.  Provide a
suitable message instead.

Fixes #102
2023-05-31 11:43:50 +01:00
Jim McDonald
b28d5b2693 Merge pull request #101 from dB2510/rename-exit-offline-image
docs/images: rename exit-offline.png
2023-05-23 06:25:28 +01:00
Dhruv Bodani
695f62bbd5 rename exit-offline.png to show in docs 2023-05-23 10:12:24 +05:30
Jim McDonald
0b8de1f615 Remove 32-bit target. 2023-05-21 09:07:20 +01:00
Jim McDonald
c1e5f1dd23 docker workflow. 2023-05-21 08:54:36 +01:00
Jim McDonald
2e4337fe6d Bump version. 2023-05-19 00:15:16 +01:00
Jim McDonald
dbe45d5c27 JSON output for validator expectation. 2023-05-19 00:04:48 +01:00
Jim McDonald
c963ea8ba5 Merge pull request #98 from wealdtech/mnemonics
Support 12- and 18-word mnemonics with passphrases.
2023-05-18 23:57:58 +01:00
Jim McDonald
484361c034 Support 12- and 18-word mnemonics with passphrases.
Allow single-world (no whitespace) passphrases for 12- and 18-word
mnemonics.

Fixes #87
2023-05-18 23:56:14 +01:00
Jim McDonald
d65f51d5af Merge pull request #97 from wealdtech/exit-validators
Generate multiple validator exits.
2023-05-18 23:38:29 +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
290 changed files with 11081 additions and 5076 deletions

58
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Docker
on:
push:
jobs:
# Set variables that will be available to all builds.
env_vars:
runs-on: ubuntu-latest
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
# Build.
build:
runs-on: ubuntu-latest
needs: [env_vars]
steps:
- name: Check out repository into the Go module directory
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64/v8
push: true
tags: wealdtech/ethdo:latest
- name: build and push on release
uses: docker/build-push-action@v4
if: ${{ github.event.release.tag_name != '' }}
with:
context: .
platforms: linux/amd64,linux/arm64/v8
push: true
tags: wealdtech/ethdo:${{ github.event.release.tag_name }}

View File

@@ -1,22 +1,29 @@
name: golangci-lint
name: 'golangci-lint'
on:
pull_request:
push:
branches:
- master
pull_request:
- 'master'
workflow_dispatch:
permissions:
contents: read
contents: 'read'
pull-requests: 'read'
checks: 'write'
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
name: 'lint'
runs-on: 'ubuntu-24.04'
steps:
- uses: actions/setup-go@v3
- uses: 'actions/setup-go@v5'
with:
go-version: 1.17
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
cache: false
go-version: '^1.22'
- uses: 'actions/checkout@v4'
- uses: 'golangci/golangci-lint-action@v6'
with:
version: latest
args: --timeout 5m
version: 'latest'
args: '--timeout=60m'
only-new-issues: true
skip-cache: true

View File

@@ -3,154 +3,208 @@ name: Release
on:
push:
tags:
- 'v*'
- 't*'
- 'v*'
- 't*'
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
# Set variables that will be available to all builds.
env_vars:
runs-on: ubuntu-22.04
outputs:
release_version: ${{ steps.release_version.outputs.release_version }}
binary: ${{ steps.binary.outputs.binary }}
steps:
- id: release_version
run: |
RELEASE_VERSION=$(echo ${{ github.ref_name }} | sed -e 's/^[vt]//')
echo "release_version=${RELEASE_VERSION}" >> $GITHUB_OUTPUT
- id: binary
run: |
BINARY=$(basename ${{ github.repository }})
echo "binary=${BINARY}" >> $GITHUB_OUTPUT
- name: Set up Go 1.x
uses: actions/setup-go@v3
with:
go-version: 1.17
# Create a github release to hold the assets once built.
create_release:
runs-on: ubuntu-22.04
needs: env_vars
outputs:
upload_url: ${{ steps.release_action.outputs.upload_url }}
steps:
- name: Create Release
id: release_action
uses: ncipollo/release-action@v1
with:
name: Release ${{ needs.env_vars.outputs.release_version }}
draft: true
prerelease: false
- name: Check out code into the Go module directory
uses: actions/checkout@v2
# Build and pack the binaries for linux.
build_linux:
runs-on: ubuntu-22.04
needs: [create_release, env_vars]
steps:
- name: Set up Go
uses: actions/setup-go@v5
with:
cache: false
go-version: '^1.22'
- name: Get dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Check out repository into the Go module directory
uses: actions/checkout@v4
- name: Set env
run: |
echo "GO111MODULE=on" >> $GITHUB_ENV
# Release tag comes from the github reference.
RELEASE_TAG=$(echo ${GITHUB_REF} | sed -e 's!.*/!!')
echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_ENV
echo "::set-output name=RELEASE_TAG::${RELEASE_TAG}"
# Ensure the release tag has expected format.
echo ${RELEASE_TAG} | grep -q '^[vt]' || exit 1
# Release version is same as release tag without leading 'v'.
RELEASE_VERSION=$(echo ${GITHUB_REF} | sed -e 's!.*/[vt]!!')
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> $GITHUB_ENV
echo "::set-output name=RELEASE_VERSION::${RELEASE_VERSION}"
- 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,linux/arm64" github.com/wealdtech/ethdo
- name: Cross compile (ARM64)
run: |
sudo apt install -y gcc-aarch64-linux-gnu libstdc++-11-pic-arm64-cross
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 go build -tags osusergo,netgo -v -ldflags="-X github.com/${{ github.repository }}/cmd.ReleaseVersion=${{ needs.env_vars.outputs.release_version }} -extldflags -static"
tar zcf ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz ${{ needs.env_vars.outputs.binary }}
sha256sum ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz | sed -e 's/ .*//' >${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz.sha256
- name: Cross-compile windows
run: |
xgo -v -x -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION} -s -w -extldflags -static" --targets="windows/amd64" github.com/wealdtech/ethdo
- name: Upload binary (ARM64)
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ needs.create_release.outputs.upload_url }}
asset_path: ./${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz
asset_name: ${{ needs.env_vars.outputs.binary }}-${{ needs.env_vars.outputs.release_version }}-linux-arm64.tar.gz
asset_content_type: application/octet-stream
- name: Create windows 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@v5
with:
cache: false
go-version: '^1.22'
- name: Create linux ARM64 tgz file
run: |
mv ethdo-linux-arm64 ethdo
tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
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@v4
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ env.RELEASE_VERSION }}
draft: 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@v5
with:
cache: false
go-version: '^1.22'
- 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@v4
- 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

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

@@ -0,0 +1,18 @@
name: test
on:
pull_request:
push:
branches:
- master
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
cache: false
go-version: '^1.22'
- uses: actions/checkout@v4
- uses: n8maninger/action-golang-test@v2

6
.gitignore vendored
View File

@@ -15,6 +15,12 @@ coverage.html
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
# Intellij
.idea/
# Makefile
Makefile
# Vim
*.sw?

143
.golangci.yml Normal file
View File

@@ -0,0 +1,143 @@
# This file contains all available configuration options
# with their default values (in comments).
#
# This file is not a configuration example,
# it contains the exhaustive configuration with explanations of the options.
issues:
# Which files to exclude: they will be analyzed, but issues from them won't be reported.
# There is no need to include all autogenerated files,
# we confidently recognize autogenerated files.
# If it's not, please let us know.
# "/" will be replaced by current OS file path separator to properly work on Windows.
# Default: []
exclude-files:
- ".*_ssz\\.go$"
# Options for analysis running.
run:
# The default concurrency value is the number of available CPU.
# concurrency: 4
# Timeout for analysis, e.g. 30s, 5m.
# Default: 1m
timeout: 10m
# Exit code when at least one issue was found.
# Default: 1
# issues-exit-code: 2
# Include test files or not.
# Default: true
tests: false
# List of build tags, all linters use it.
# Default: [].
# build-tags:
# - mytag
# Which dirs to skip: issues from them won't be reported.
# Can use regexp here: `generated.*`, regexp is applied on full path.
# Default value is empty list,
# but default dirs are skipped independently of this option's value (see skip-dirs-use-default).
# "/" will be replaced by current OS file path separator to properly work on Windows.
# skip-dirs:
# - autogenerated_by_my_lib
# Enables skipping of directories:
# - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
# Default: true
# skip-dirs-use-default: false
# If set we pass it to "go list -mod={option}". From "go help modules":
# If invoked with -mod=readonly, the go command is disallowed from the implicit
# automatic updating of go.mod described above. Instead, it fails when any changes
# to go.mod are needed. This setting is most useful to check that go.mod does
# not need updates, such as in a continuous integration and testing system.
# If invoked with -mod=vendor, the go command assumes that the vendor
# directory holds the correct copies of dependencies and ignores
# the dependency descriptions in go.mod.
#
# Allowed values: readonly|vendor|mod
# By default, it isn't set.
modules-download-mode: readonly
# Allow multiple parallel golangci-lint instances running.
# If false (default) - golangci-lint acquires file lock on start.
allow-parallel-runners: true
# Define the Go version limit.
# Mainly related to generics support since go1.18.
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18
# go: '1.19'
# output configuration options
output:
formats:
- format: colored-line-number
path: stderr
# All available settings of specific linters.
linters-settings:
gosec:
excludes:
# Flags for potentially-unsafe casting of ints, but generates a lot of false positives.
- 'G115'
lll:
line-length: 132
stylecheck:
checks: [ "all", "-ST1000" ]
tagliatelle:
case:
# use-field-name: true
rules:
json: snake
yaml: snake
linters:
# Enable all available linters.
# Default: false
enable-all: true
# Disable specific linter
# https://golangci-lint.run/usage/linters/#disabled-by-default
disable:
- contextcheck
- cyclop
- depguard
- dupl
- err113
- errorlint
- exhaustive
- exhaustruct
- forbidigo
- forcetypeassert
- funlen
- gci
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- goheader
- ireturn
- lll
- maintidx
- mnd
- musttag
- nestif
- nilnil
- nlreturn
- nolintlint
- perfsprint
- promlinter
- rowserrcheck
- sqlclosecheck
- tenv
- unparam
- varnamelen
- wastedassign
- wrapcheck
- wsl

View File

@@ -1,3 +1,151 @@
dev:
- provide ETH values as well as validator numbers in "epoch summary"
1.37.3:
- add "hoodi" to the list of supported networks
1.37.2:
- add "block trail"
1.37.1:
- handle missing blobs for block info
- fix `--epoch` flag for epoch summary
1.37.0:
- support Electra
- add `--compounding` flag when creating validator deposit data
1.36.6:
- allow specification of blockid for validator info
- validator depositdata orders deposits from an HD wallet by path
1.36.5:
- avoid corner case mnemonic derivation with 25th word
1.36.2:
- avoid crash when signing and verifing signatures using keys rather than accounts
1.36.1:
- more JSON data for epoch summary
- fix crash when block ifno had no blobs
1.36.0:
- support keystore wallets
1.35.6:
- provide more JSON data in "epoch summary"
1.35.5:
- allow keystore to be output to the console
1.35.4:
- provide consensus and execution client info in block info output
1.35.3:
- provide better error message on context deadlline exceeded
- update launchpad output to match latest version
- add deposit contract address to "chain info"
1.35.2:
- update dependencies
1.35.1:
- fix output for various commands that may encounter an empty slot
1.35.0:
- support Deneb
- add start and end dates for eth1votes period
1.34.1:
- fix period parsing for "synccommittee members" command
1.34.0:
- update dependencies
- use Capella fork for all exits
- support Deneb beta 5
1.33.2:
- fix windows build
1.33.1:
- add "slot" to "proposer duties" command
- add activation epoch and time to "validator info" command where applicable
- add "holesky" to the list of supported networks
- avoid crash when requesting validators from beacon node without debug enabled
1.33.0:
- show all slots with 'synccommittee inclusion'
- add "wallet batch" command
1.32.0:
- fix incorrect error when "deposit verify" is not given a withdrawal address
- allow truncated mnemonics (first four characters of each word)
- add deneb information to "block info"
- add epoch parameter to "validator yield"
- add proposer index to "block info"
- "block info" honours "--quiet" flag
- "block info" accepts "--block-time" option
- increase default operation timeout from 10s to 30s
- "epoch summary" JSON lists number of blobs
1.31.0:
- initial support for deneb
- add "--generate-keystore" option for "account derive"
- update "validator exit" command to be able to generate multiple exits
- support for 12-word and 18-word mnemonics with single-word (no whitespace) passphrases
- add JSON output for "validator expectation"
1.30.0:
- add "chain spec" command
- add "validator withdrawal" command
1.29.2:
- fix regression where validator index could not be used as an account specifier
1.29.0:
- allow use of keystores with validator credentials set
- tidy up various command options to provide more standard usage
- add mainnet fallback beacon node
1.28.4:
- allow validator exit to use a keystore as its validator parameter
1.28.2:
- fix bix stopping validator exit creation by direct validator specification
1.28.1:
- generate error message if "validator credentials set" process fails to generate any credentials
- allow import of accounts with null name field in their keystore
- show text of execution payload extra data if available
1.28.0:
- support additional mnemonic word list languages
- increase minimum timeout for commands that fetch all validators to 2 minutes
- provide better error messages when offline preparation file cannot be read
- allow creation of all credential change operations related to a private key (thanks to @joaocenoura)
1.27.1:
- fix issue with voluntary exits using incorrect domain (thanks to @0xTylerHolmes)
1.27.0:
- use new build system
- support S3 credentials
- update operation of validator exit to match validator credentials set
1.26.5:
- provide validator information in "chain status" verbose output
1.26.4:
- provide details of BLS to execution change operations with verbose block output
1.26.3:
- provide support for additional S3 store options
- show error when attempting to delete non-filesystem wallets
- provide additional support for Capella
1.26.2
- remove check that requires capella prior to generating validator credentials change operations
1.26.1
- add ability to generate validator credentials change operations prior to the fork in which they become usable

View File

@@ -1,4 +1,4 @@
FROM golang:1.17-bullseye as builder
FROM golang:1.23-bookworm AS builder
WORKDIR /app
@@ -10,7 +10,7 @@ COPY . .
RUN go build
FROM debian:bullseye-slim
FROM debian:bookworm-slim
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/*

View File

@@ -38,7 +38,7 @@ docker pull wealdtech/ethdo
go install github.com/wealdtech/ethdo@latest
```
Note that `ethdo` requires at least version 1.13 of go to operate. The version of go can be found with `go version`.
Note that `ethdo` requires at least version 1.20 of go to operate. The version of go can be found with `go version`.
If this does not work please see the [troubleshooting](https://github.com/wealdtech/ethdo/blob/master/docs/troubleshooting.md) page.
@@ -64,7 +64,7 @@ Alternatively, if the beacon node is running in a separate docker container a sh
## Setting up
`ethdo` needs a connection to a beacon node for many of its features. `ethdo` can connect to any beacon node that fully supports the [standard REST API](https://ethereum.github.io/beacon-APIs/). The following changes are required to beacon nodes to make this available.
`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.
@@ -86,9 +86,14 @@ Teku disables the REST API by default. To enable it, the beacon node must be st
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
@@ -98,12 +103,12 @@ The default port for the REST API is 5051, which can be changed with the `--rest
- 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.
@@ -138,6 +143,28 @@ ethdo also supports environment variables. Environment variables are prefixed w
export ETHDO_PASSPHRASE="my account passphrase"
```
### S3 store options
Amazon S3-compatible stores have additional options available, which can be configured under the "stores.s3" key. An example configuration is as follows:
```json
{
"stores": {
"s3": {
"region": "us-west-1",
"bucket": "my-s3-store",
"path": "/wallets",
"credentials": {
"id": "ABCDEF123",
"secret": "XXXXXXXXX"
}
}
}
}
```
Information on these and other options can be found in the S3 store repository.
### Output and exit status
If set, the `--quiet` argument will suppress all output.
@@ -148,6 +175,15 @@ If set, the `--debug` argument will output additional information about the oper
Commands will have an exit status of 0 on success and 1 on failure. The specific definition of success is specified in the help for each command.
### Validator specifier
Ethereum validators can be specified in a number of different ways. The options are:
- an `ethdo` account, in the format _wallet_/_account_. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied, with a passphrase (for local accounts) or authority (for remote accounts)
- the validator's 48-byte public key. It is not possible to use the a validator specified in this way to sign validator-related operations
- a keystore, supplied either as direct JSON or as a path to a keystore on the local filesystem. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied
- the validator's numeric index. It is not possible to use a validator specified in this way to sign validator-related operations. Note that this only works with on-chain operations, as the validator's index must be resolved to its public key
## Passphrase strength
`ethdo` will by default not allow creation or export of accounts or wallets with weak passphrases. If a weak pasphrase is used then `ethdo` will refuse to continue.
@@ -178,7 +214,7 @@ There is a [HOWTO](https://github.com/wealdtech/ethdo/blob/master/docs/howto.md)
Jim McDonald: [@mcdee](https://github.com/mcdee).
Special thanks to [@SuburbanDad](https://github.com/SuburbanDad) for updating xgo to allow for cross-compilation of `ethdo` releaes.
Special thanks to [@SuburbanDad](https://github.com/SuburbanDad) for updating xgo to allow for cross-compilation of `ethdo` releases.
## Contribute

335
beacon/chaininfo.go Normal file
View File

@@ -0,0 +1,335 @@
// Copyright © 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package beacon
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
consensusclient "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/services/chaintime"
"github.com/wealdtech/ethdo/util"
)
type ChainInfo struct {
Version uint64
Validators []*ValidatorInfo
GenesisValidatorsRoot phase0.Root
Epoch phase0.Epoch
GenesisForkVersion phase0.Version
ExitForkVersion phase0.Version
CurrentForkVersion phase0.Version
BLSToExecutionChangeDomainType phase0.DomainType
VoluntaryExitDomainType phase0.DomainType
}
type chainInfoJSON struct {
Version string `json:"version"`
Validators []*ValidatorInfo `json:"validators"`
GenesisValidatorsRoot string `json:"genesis_validators_root"`
Epoch string `json:"epoch"`
GenesisForkVersion string `json:"genesis_fork_version"`
ExitForkVersion string `json:"exit_fork_version"`
CurrentForkVersion string `json:"current_fork_version"`
BLSToExecutionChangeDomainType string `json:"bls_to_execution_change_domain_type"`
VoluntaryExitDomainType string `json:"voluntary_exit_domain_type"`
}
type chainInfoVersionJSON struct {
Version string `json:"version"`
}
// MarshalJSON implements json.Marshaler.
func (c *ChainInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(&chainInfoJSON{
Version: strconv.FormatUint(c.Version, 10),
Validators: c.Validators,
GenesisValidatorsRoot: fmt.Sprintf("%#x", c.GenesisValidatorsRoot),
Epoch: fmt.Sprintf("%d", c.Epoch),
GenesisForkVersion: fmt.Sprintf("%#x", c.GenesisForkVersion),
ExitForkVersion: fmt.Sprintf("%#x", c.ExitForkVersion),
CurrentForkVersion: fmt.Sprintf("%#x", c.CurrentForkVersion),
BLSToExecutionChangeDomainType: fmt.Sprintf("%#x", c.BLSToExecutionChangeDomainType),
VoluntaryExitDomainType: fmt.Sprintf("%#x", c.VoluntaryExitDomainType),
})
}
// UnmarshalJSON implements json.Unmarshaler.
func (c *ChainInfo) UnmarshalJSON(input []byte) error {
// See which version we are dealing with.
var metadata chainInfoVersionJSON
if err := json.Unmarshal(input, &metadata); err != nil {
return errors.Wrap(err, "invalid JSON")
}
if metadata.Version == "" {
return errors.New("version missing")
}
version, err := strconv.ParseUint(metadata.Version, 10, 64)
if err != nil {
return errors.Wrap(err, "version invalid")
}
if version < 3 {
return errors.New("outdated version; please regenerate your offline data")
}
c.Version = version
var data chainInfoJSON
if err := json.Unmarshal(input, &data); err != nil {
return errors.Wrap(err, "invalid JSON")
}
if len(data.Validators) == 0 {
return errors.New("validators missing")
}
c.Validators = data.Validators
if data.GenesisValidatorsRoot == "" {
return errors.New("genesis validators root missing")
}
genesisValidatorsRootBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisValidatorsRoot, "0x"))
if err != nil {
return errors.Wrap(err, "genesis validators root invalid")
}
if len(genesisValidatorsRootBytes) != phase0.RootLength {
return errors.New("genesis validators root incorrect length")
}
copy(c.GenesisValidatorsRoot[:], genesisValidatorsRootBytes)
if data.Epoch == "" {
return errors.New("epoch missing")
}
epoch, err := strconv.ParseUint(data.Epoch, 10, 64)
if err != nil {
return errors.Wrap(err, "epoch invalid")
}
c.Epoch = phase0.Epoch(epoch)
if data.GenesisForkVersion == "" {
return errors.New("genesis fork version missing")
}
genesisForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisForkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "genesis fork version invalid")
}
if len(genesisForkVersionBytes) != phase0.ForkVersionLength {
return errors.New("genesis fork version incorrect length")
}
copy(c.GenesisForkVersion[:], genesisForkVersionBytes)
if data.ExitForkVersion == "" {
return errors.New("exit fork version missing")
}
exitForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.ExitForkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "exit fork version invalid")
}
if len(exitForkVersionBytes) != phase0.ForkVersionLength {
return errors.New("exit fork version incorrect length")
}
copy(c.ExitForkVersion[:], exitForkVersionBytes)
if data.CurrentForkVersion == "" {
return errors.New("current fork version missing")
}
currentForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.CurrentForkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "current fork version invalid")
}
if len(currentForkVersionBytes) != phase0.ForkVersionLength {
return errors.New("current fork version incorrect length")
}
copy(c.CurrentForkVersion[:], currentForkVersionBytes)
if data.BLSToExecutionChangeDomainType == "" {
return errors.New("bls to execution domain type missing")
}
blsToExecutionChangeDomainType, err := hex.DecodeString(strings.TrimPrefix(data.BLSToExecutionChangeDomainType, "0x"))
if err != nil {
return errors.Wrap(err, "bls to execution domain type invalid")
}
if len(blsToExecutionChangeDomainType) != phase0.DomainTypeLength {
return errors.New("bls to execution domain type incorrect length")
}
copy(c.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType)
if data.VoluntaryExitDomainType == "" {
return errors.New("voluntary exit domain type missing")
}
voluntaryExitDomainType, err := hex.DecodeString(strings.TrimPrefix(data.VoluntaryExitDomainType, "0x"))
if err != nil {
return errors.Wrap(err, "voluntary exit domain type invalid")
}
if len(voluntaryExitDomainType) != phase0.DomainTypeLength {
return errors.New("voluntary exit domain type incorrect length")
}
copy(c.VoluntaryExitDomainType[:], voluntaryExitDomainType)
return nil
}
// FetchValidatorInfo fetches validator info given a validator identifier.
func (c *ChainInfo) FetchValidatorInfo(ctx context.Context, id string) (*ValidatorInfo, error) {
var validatorInfo *ValidatorInfo
switch {
case id == "":
return nil, errors.New("no validator specified")
case strings.HasPrefix(id, "0x"):
// ID is a public key.
// Check that the key is the correct length.
if len(id) != 98 {
return nil, errors.New("invalid public key: incorrect length")
}
for _, validator := range c.Validators {
if strings.EqualFold(id, fmt.Sprintf("%#x", validator.Pubkey)) {
validatorInfo = validator
break
}
}
case strings.Contains(id, "/"):
// An account.
_, account, err := util.WalletAndAccountFromPath(ctx, id)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain account")
}
accPubKey, err := util.BestPublicKey(account)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain public key for account")
}
pubkey := fmt.Sprintf("%#x", accPubKey.Marshal())
for _, validator := range c.Validators {
if strings.EqualFold(pubkey, fmt.Sprintf("%#x", validator.Pubkey)) {
validatorInfo = validator
break
}
}
default:
// An index.
index, err := strconv.ParseUint(id, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse validator index")
}
validatorIndex := phase0.ValidatorIndex(index)
for _, validator := range c.Validators {
if validator.Index == validatorIndex {
validatorInfo = validator
break
}
}
}
if validatorInfo == nil {
return nil, errors.New("unknown validator")
}
return validatorInfo, nil
}
// ObtainChainInfoFromNode obtains the chain information from a node.
func ObtainChainInfoFromNode(ctx context.Context,
consensusClient consensusclient.Service,
chainTime chaintime.Service,
) (
*ChainInfo,
error,
) {
res := &ChainInfo{
Version: 3,
Validators: make([]*ValidatorInfo, 0),
Epoch: chainTime.CurrentEpoch(),
}
// Obtain validators.
validatorsResponse, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, &api.ValidatorsOpts{State: "head"})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validatorsResponse.Data {
res.Validators = append(res.Validators, &ValidatorInfo{
Index: validator.Index,
Pubkey: validator.Validator.PublicKey,
WithdrawalCredentials: validator.Validator.WithdrawalCredentials,
State: validator.Status,
})
}
// Order validators by index.
sort.Slice(res.Validators, func(i int, j int) bool {
return res.Validators[i].Index < res.Validators[j].Index
})
// Genesis validators root obtained from beacon node.
genesisResponse, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis information")
}
res.GenesisValidatorsRoot = genesisResponse.Data.GenesisValidatorsRoot
// Fetch the genesis fork version from the specification.
specResponse, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx, &api.SpecOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := specResponse.Data["GENESIS_FORK_VERSION"]
if !exists {
return nil, errors.New("genesis fork version not known by chain")
}
var isForkVersion bool
res.GenesisForkVersion, isForkVersion = tmp.(phase0.Version)
if !isForkVersion {
return nil, errors.New("could not obtain GENESIS_FORK_VERSION")
}
// Fetch the exit fork version (Capella) from the specification.
tmp, exists = specResponse.Data["CAPELLA_FORK_VERSION"]
if !exists {
return nil, errors.New("capella fork version not known by chain")
}
res.ExitForkVersion, isForkVersion = tmp.(phase0.Version)
if !isForkVersion {
return nil, errors.New("could not obtain CAPELLA_FORK_VERSION")
}
// Fetch the current fork version from the fork schedule.
forkScheduleResponse, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx, &api.ForkScheduleOpts{})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain fork schedule")
}
for i := range forkScheduleResponse.Data {
if forkScheduleResponse.Data[i].Epoch <= res.Epoch {
res.CurrentForkVersion = forkScheduleResponse.Data[i].CurrentVersion
}
}
blsToExecutionChangeDomainType, exists := specResponse.Data["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
if !exists {
return nil, errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
}
copy(res.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType[:])
voluntaryExitDomainType, exists := specResponse.Data["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
if !exists {
return nil, errors.New("failed to obtain DOMAIN_VOLUNTARY_EXIT")
}
copy(res.VoluntaryExitDomainType[:], voluntaryExitDomainType[:])
return res, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// 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
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorcredentialsset
package beacon
import (
"encoding/hex"
@@ -20,33 +20,37 @@ import (
"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 {
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"`
WithdrawalCredentials string `json:"withdrawal_credentials"`
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) {
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 {
func (v *ValidatorInfo) UnmarshalJSON(input []byte) error {
var data validatorInfoJSON
if err := json.Unmarshal(input, &data); err != nil {
return errors.Wrap(err, "invalid JSON")
@@ -73,6 +77,11 @@ func (v *validatorInfo) UnmarshalJSON(input []byte) error {
}
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")
}
@@ -88,7 +97,7 @@ func (v *validatorInfo) UnmarshalJSON(input []byte) error {
}
// String implements the Stringer interface.
func (v *validatorInfo) String() string {
func (v *ValidatorInfo) String() string {
data, err := json.Marshal(v)
if err != nil {
return fmt.Sprintf("Err: %v\n", err)

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

@@ -86,7 +86,7 @@ func processPathed(ctx context.Context, data *dataIn) (*dataOut, error) {
if data.passphrase == "" {
return nil, errors.New("passphrase is required")
}
match, err := regexp.Match("^m/[0-9]+/[0-9]+(/[0-9+])+", []byte(data.path))
match, err := regexp.MatchString("^m/[0-9]+/[0-9]+(/[0-9+])+", data.path)
if err != nil {
return nil, errors.Wrap(err, "unable to match path to regular expression")
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package accountcreate
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if !viper.GetBool("verbose") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

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
@@ -22,20 +22,25 @@ import (
type dataIn struct {
quiet bool
json bool
// Derivation information.
mnemonic string
path string
// Output options.
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
}
func input(ctx context.Context) (*dataIn, error) {
func input(_ context.Context) (*dataIn, error) {
data := &dataIn{}
// Quiet.
data.quiet = viper.GetBool("quiet")
// JSON.
data.json = viper.GetBool("json")
// Mnemonic.
if viper.GetString("mnemonic") == "" {
return nil, errors.New("mnemonic is required")
@@ -54,5 +59,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,28 @@ package accountderive
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
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 {
json bool
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
key *e2types.BLSPrivateKey
path string
}
func output(ctx context.Context, data *dataOut) (string, error) {
@@ -37,13 +47,17 @@ 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()))
}
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("Withdrawal credentials: %#x\n", withdrawalCredentials))
}
@@ -53,3 +67,42 @@ func output(ctx context.Context, data *dataOut) (string, error) {
return builder.String(), nil
}
func outputKeystore(_ context.Context, data *dataOut) (string, error) {
passphrase, err := util.GetPassphrase()
if err != nil {
return "", errors.New("no passphrase supplied")
}
encryptor := keystorev4.New()
crypto, err := encryptor.Encrypt(data.key.Marshal(), passphrase)
if err != nil {
return "", errors.New("failed to encrypt private key")
}
uuid, err := uuid.NewRandom()
if err != nil {
return "", errors.New("failed to generate UUID")
}
ks := make(map[string]interface{})
ks["uuid"] = uuid.String()
ks["pubkey"] = hex.EncodeToString(data.key.PublicKey().Marshal())
ks["version"] = 4
ks["path"] = data.path
ks["crypto"] = crypto
out, err := json.Marshal(ks)
if err != nil {
return "", errors.Wrap(err, "failed to marshal keystore JSON")
}
if data.json {
fmt.Fprintf(os.Stdout, "%s\n", string(out))
} else {
keystoreFilename := fmt.Sprintf("keystore-%s-%d.json", strings.ReplaceAll(data.path, "/", "_"), time.Now().Unix())
if err := os.WriteFile(keystoreFilename, out, 0o600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", keystoreFilename))
}
}
return "", nil
}

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
@@ -38,9 +38,12 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
}
results := &dataOut{
json: data.json,
showPrivateKey: data.showPrivateKey,
showWithdrawalCredentials: data.showWithdrawalCredentials,
generateKeystore: data.generateKeystore,
key: key.(*e2types.BLSPrivateKey),
path: data.path,
}
return results, nil

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,9 +15,9 @@ package accountderive
import (
"context"
"errors"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to obtain input"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if dataIn.quiet {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return strings.TrimSuffix(results, "\n"), nil

View File

@@ -16,7 +16,7 @@ package accountimport
import (
"context"
"encoding/hex"
"io/ioutil"
"os"
"strings"
"time"
@@ -115,7 +115,7 @@ func obtainKeystore(input string) ([]byte, 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")
}

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

@@ -15,6 +15,7 @@ package accountimport
import (
"context"
"encoding/json"
"fmt"
"github.com/pkg/errors"
@@ -66,7 +67,7 @@ func processFromKey(ctx context.Context, data *dataIn) (*dataOut, error) {
}
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
@@ -79,15 +80,25 @@ func processFromKeystore(ctx context.Context, data *dataIn) (*dataOut, error) {
encryptor := keystorev4.New()
// Need to add a couple of fields to the keystore to make it compliant.
keystoreData := fmt.Sprintf(`{"name":"Import","encryptor":"keystore",%s`, string(data.keystore[1:]))
walletData := fmt.Sprintf(`{"wallet":{"name":"ImportTest","type":"non-deterministic","uuid":"e1526407-1dc7-4f3f-9d05-ab696f40707c","version":1},"accounts":[%s]}`, keystoreData)
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 wallet")
return nil, errors.Wrap(err, "failed to import account")
}
account := <-wallet.Accounts(ctx)

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package accountimport
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to obtain input"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if !viper.GetBool("verbose") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

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

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package accountkey
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -29,7 +29,7 @@ var accountCreateCmd = &cobra.Command{
ethdo account create --account="primary/operations" --passphrase="my secret"
In quiet mode this will return 0 if the account is created successfully, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := accountcreate.Run(cmd)
if err != nil {
return err
@@ -51,11 +51,11 @@ func init() {
accountCreateCmd.Flags().Uint32("signing-threshold", 1, "Signing threshold (1 for non-distributed 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 {
if err := viper.BindPFlag("signing-threshold", cmd.Flags().Lookup("signing-threshold")); err != nil {
panic(err)
}
}

View File

@@ -29,7 +29,7 @@ var accountDeriveCmd = &cobra.Command{
ethdo account derive --mnemonic="..." --path="m/12381/3600/0/0"
In quiet mode this will return 0 if the inputs can derive an account account, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := accountderive.Run(cmd)
if err != nil {
return err
@@ -49,13 +49,21 @@ func init() {
accountFlags(accountDeriveCmd)
accountDeriveCmd.Flags().Bool("show-private-key", false, "show private key for derived account")
accountDeriveCmd.Flags().Bool("show-withdrawal-credentials", false, "show withdrawal credentials for derived account")
accountDeriveCmd.Flags().Bool("generate-keystore", false, "generate a keystore for the derived account")
accountDeriveCmd.Flags().Bool("json", false, "display the JSON keystore for the derived account on stdout")
}
func accountDeriveBindings() {
if err := viper.BindPFlag("show-private-key", accountDeriveCmd.Flags().Lookup("show-private-key")); 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("show-withdrawal-credentials", accountDeriveCmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
if err := viper.BindPFlag("show-withdrawal-credentials", cmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
panic(err)
}
if err := viper.BindPFlag("generate-keystore", cmd.Flags().Lookup("generate-keystore")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", cmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -29,7 +29,7 @@ var accountImportCmd = &cobra.Command{
ethdo account import --account="primary/testing" --key="0x..." --passphrase="my secret"
In quiet mode this will return 0 if the account is imported successfully, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := accountimport.Run(cmd)
if err != nil {
return err
@@ -52,14 +52,14 @@ func init() {
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", accountImportCmd.Flags().Lookup("keystore")); err != nil {
if err := viper.BindPFlag("keystore", cmd.Flags().Lookup("keystore")); err != nil {
panic(err)
}
if err := viper.BindPFlag("keystore-passphrase", accountImportCmd.Flags().Lookup("keystore-passphrase")); err != nil {
if err := viper.BindPFlag("keystore-passphrase", cmd.Flags().Lookup("keystore-passphrase")); err != nil {
panic(err)
}
}

View File

@@ -33,7 +33,7 @@ var accountInfoCmd = &cobra.Command{
ethdo account info --account="primary/my funds"
In quiet mode this will return 0 if the account exists, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
@@ -44,11 +44,11 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
// Disallow wildcards (for now)
assert(fmt.Sprintf("%s/%s", wallet.Name(), account.Name()) == viper.GetString("account"), "Mismatched account name")
if quiet {
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
outputIf(verbose, fmt.Sprintf("UUID: %v", account.ID()))
outputIf(viper.GetBool("verbose"), fmt.Sprintf("UUID: %v", account.ID()))
var withdrawalPubKey e2types.PublicKey
if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
fmt.Printf("Public key: %#x\n", pubKeyProvider.PublicKey().Marshal())
@@ -58,7 +58,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
if distributedAccount, ok := account.(e2wtypes.DistributedAccount); ok {
fmt.Printf("Composite public key: %#x\n", distributedAccount.CompositePublicKey().Marshal())
fmt.Printf("Signing threshold: %d/%d\n", distributedAccount.SigningThreshold(), len(distributedAccount.Participants()))
if verbose {
if viper.GetBool("verbose") {
fmt.Printf("Participants:\n")
for k, v := range distributedAccount.Participants() {
fmt.Printf(" %d: %s\n", k, v)
@@ -67,7 +67,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
withdrawalPubKey = distributedAccount.CompositePublicKey()
}
if verbose {
if viper.GetBool("verbose") {
withdrawalCredentials := util.SHA256(withdrawalPubKey.Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
fmt.Printf("Withdrawal credentials: %#x\n", withdrawalCredentials)

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.",
@@ -30,7 +30,7 @@ var accountKeyCmd = &cobra.Command{
ethdo account key --account="Personal wallet/Operations" --passphrase="my account passphrase"
In quiet mode this will return 0 if the key can be obtained, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := accountkey.Run(cmd)
if err != nil {
return err

View File

@@ -29,7 +29,7 @@ var accountLockCmd = &cobra.Command{
ethdo account lock --account="primary/my funds"
In quiet mode this will return 0 if the account is locked, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()

View File

@@ -30,7 +30,7 @@ var accountUnlockCmd = &cobra.Command{
ethdo account unlock --account="primary/my funds" --passphrase="secret"
In quiet mode this will return 0 if the account is unlocked, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()

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.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
)
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 any beacon node",
},
}

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,13 @@ package attesterduties
import (
"context"
"encoding/hex"
"fmt"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
@@ -32,43 +29,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 +40,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,12 +50,16 @@ 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 *apiv1.Validator, epoch spec.Epoch) (*apiv1.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
Epoch: epoch,
Indices: []spec.ValidatorIndex{validator.Index},
})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attester duties")
}
duties := dutiesResponse.Data
if len(duties) == 0 {
return nil, errors.New("validator does not have duty for that epoch")

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.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
)
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

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading
// Copyright © 2021, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package attesterduties
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

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.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
)
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 any beacon node",
},
}

View File

@@ -16,8 +16,10 @@ package attesterinclusion
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
@@ -26,7 +28,7 @@ type dataOut struct {
debug bool
quiet bool
verbose bool
attestation *phase0.Attestation
attestation *spec.VersionedAttestation
slot phase0.Slot
attestationIndex uint64
inclusionDelay phase0.Slot
@@ -38,7 +40,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")
@@ -49,7 +51,7 @@ func output(ctx context.Context, data *dataOut) (string, error) {
buf.WriteString("Attestation included in block ")
buf.WriteString(fmt.Sprintf("%d", data.slot))
buf.WriteString(", index ")
buf.WriteString(fmt.Sprintf("%d", data.attestationIndex))
buf.WriteString(strconv.FormatUint(data.attestationIndex, 10))
if data.verbose {
buf.WriteString("\nInclusion delay: ")
buf.WriteString(fmt.Sprintf("%d", data.inclusionDelay))

View File

@@ -17,9 +17,12 @@ import (
"bytes"
"context"
"fmt"
"net/http"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
@@ -31,41 +34,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)),
standardchaintime.WithGenesisProvider(data.eth2Client.(eth2client.GenesisProvider)),
)
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")
}
@@ -76,14 +64,22 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
startSlot := duty.Slot + 1
endSlot := startSlot + 32
for slot := startSlot; slot < endSlot; slot++ {
signedBlock, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
blockResponse, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
// No block for this slot, that's fine.
continue
}
return nil, errors.Wrap(err, "failed to obtain block")
}
if signedBlock == nil {
block := blockResponse.Data
if block == nil {
continue
}
blockSlot, err := signedBlock.Slot()
blockSlot, err := block.Slot()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block slot")
}
@@ -93,15 +89,23 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data.debug {
fmt.Printf("Fetched block for slot %d\n", slot)
}
attestations, err := signedBlock.Attestations()
attestations, err := block.Attestations()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block attestations")
}
for i, attestation := range attestations {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
attestationData, err := attestation.Data()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attestation data")
}
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attestation aggregation bits")
}
if attestationData.Slot == duty.Slot &&
attestationData.Index == duty.CommitteeIndex &&
aggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
headCorrect := false
targetCorrect := false
if data.verbose {
@@ -134,55 +138,79 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return results, nil
}
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *spec.VersionedAttestation) (bool, error) {
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
slot := attestationData.Slot
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
return false, nil
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
// No block.
slot--
continue
}
return false, err
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
return bytes.Equal(response.Data.Root[:], attestationData.BeaconBlockRoot[:]), nil
}
}
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *spec.VersionedAttestation) (bool, error) {
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
// Start with first slot of the target epoch.
slot := data.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
slot := data.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
response, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
return false, nil
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
// No block.
slot--
continue
}
return false, err
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
return bytes.Equal(response.Data.Root[:], attestationData.Target.Root[:]), nil
}
}
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 *apiv1.Validator, epoch phase0.Epoch) (*apiv1.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
dutiesResponse, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, &api.AttesterDutiesOpts{
Epoch: epoch,
Indices: []phase0.ValidatorIndex{validator.Index},
})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attester duties")
}
duties := dutiesResponse.Data
if len(duties) == 0 {
return nil, errors.New("validator does not have duty for that epoch")

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.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
)
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

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package attesterinclusion
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -26,10 +26,10 @@ 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 {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := attesterduties.Run(cmd)
if err != nil {
return err
@@ -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,10 +26,10 @@ var attesterInclusionCmd = &cobra.Command{
Short: "Obtain information about attester inclusion",
Long: `Obtain information about attester inclusion. For example:
ethdo attester inclusion --account=Validators/00001 --epoch=12345
ethdo attester inclusion --validator=Validators/00001 --epoch=12345
In quiet mode this will return 0 if an attestation from the attester is found on the block of the given epoch, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := attesterinclusion.Run(cmd)
if err != nil {
return err
@@ -47,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().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

@@ -55,7 +55,7 @@ type command struct {
weightDenominator uint64
// Processing.
priorAttestations map[string]*attestationData
priorAttestations map[string]*attestationDataInfo
// Head roots provides the root of the head slot at given slots.
headRoots map[phase0.Slot]phase0.Root
// Target roots provides the root of the target epoch at given slots.
@@ -77,20 +77,20 @@ type blockAnalysis struct {
}
type attestationAnalysis struct {
Head phase0.Root `json:"head"`
Target phase0.Root `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
Head phase0.Root `json:"head"`
Target phase0.Root `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationDataInfo `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
type syncCommitteeAnalysis struct {
@@ -100,17 +100,17 @@ type syncCommitteeAnalysis struct {
Value float64 `json:"value"`
}
type attestationData struct {
type attestationDataInfo struct {
Block phase0.Slot `json:"block"`
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"),
debug: viper.GetBool("debug"),
priorAttestations: make(map[string]*attestationData),
priorAttestations: make(map[string]*attestationDataInfo),
headRoots: make(map[phase0.Slot]phase0.Root),
targetRoots: make(map[phase0.Slot]phase0.Root),
votes: make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist),

View File

@@ -17,6 +17,7 @@ import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
)
@@ -33,20 +34,20 @@ func (c *command) output(ctx context.Context) (string, error) {
}
type attestationAnalysisJSON struct {
Head string `json:"head"`
Target string `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
Head string `json:"head"`
Target string `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationDataInfo `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
func (a *attestationAnalysis) MarshalJSON() ([]byte, error) {
@@ -82,33 +83,32 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
for i, attestation := range c.analysis.Attestations {
if c.verbose {
builder.WriteString("Attestation ")
builder.WriteString(fmt.Sprintf("%d", i))
builder.WriteString(strconv.Itoa(i))
builder.WriteString(": ")
builder.WriteString("distance ")
builder.WriteString(fmt.Sprintf("%d", attestation.Distance))
builder.WriteString(strconv.Itoa(attestation.Distance))
builder.WriteString(", ")
if attestation.Duplicate != nil {
builder.WriteString("duplicate of attestation ")
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Index))
builder.WriteString(strconv.Itoa(attestation.Duplicate.Index))
builder.WriteString(" in block ")
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Block))
builder.WriteString("\n")
continue
}
builder.WriteString(fmt.Sprintf("%d", attestation.NewVotes))
builder.WriteString(strconv.Itoa(attestation.NewVotes))
builder.WriteString("/")
builder.WriteString(fmt.Sprintf("%d", attestation.Votes))
builder.WriteString(strconv.Itoa(attestation.Votes))
builder.WriteString("/")
builder.WriteString(fmt.Sprintf("%d", attestation.PossibleVotes))
builder.WriteString(strconv.Itoa(attestation.PossibleVotes))
builder.WriteString(" new/total/possible votes")
if attestation.NewVotes == 0 {
builder.WriteString("\n")
continue
} else {
builder.WriteString(", ")
}
builder.WriteString(", ")
switch {
case !attestation.HeadCorrect:
builder.WriteString("head vote incorrect, ")
@@ -138,7 +138,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
if c.analysis.SyncCommitee.Contributions > 0 {
if c.verbose {
builder.WriteString("Sync committee contributions: ")
builder.WriteString(fmt.Sprintf("%d", c.analysis.SyncCommitee.Contributions))
builder.WriteString(strconv.Itoa(c.analysis.SyncCommitee.Contributions))
builder.WriteString(" contributions, score ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Score))
builder.WriteString(", value ")

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
@@ -17,8 +17,10 @@ import (
"bytes"
"context"
"fmt"
"net/http"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
@@ -33,13 +35,17 @@ func (c *command) process(ctx context.Context) error {
return err
}
block, err := c.blocksProvider.SignedBeaconBlock(ctx, c.blockID)
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: c.blockID,
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
return errors.New("empty beacon block")
}
return errors.Wrap(err, "failed to obtain beacon block")
}
if block == nil {
return errors.New("empty beacon block")
}
block := blockResponse.Data
slot, err := block.Slot()
if err != nil {
@@ -57,8 +63,12 @@ func (c *command) process(ctx context.Context) error {
// Calculate how many parents we need to fetch.
minSlot := slot
for _, attestation := range attestations {
if attestation.Data.Slot < minSlot {
minSlot = attestation.Data.Slot
attestationData, err := attestation.Data()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation data")
}
if attestationData.Slot < minSlot {
minSlot = attestationData.Slot
}
}
if c.debug {
@@ -77,11 +87,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 {
@@ -101,10 +107,16 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
if c.debug {
fmt.Printf("Processing attestation %d\n", i)
}
attestationData, err := attestation.Data()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation data")
}
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
Head: attestationData.BeaconBlockRoot,
Target: attestationData.Target.Root,
Distance: int(slot - attestationData.Slot),
}
root, err := attestation.HashTreeRoot()
@@ -114,45 +126,47 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return err
}
_, exists = blockVotes[data.Slot][data.Index]
_, exists := blockVotes[attestationData.Slot]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
blockVotes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[attestationData.Slot][attestationData.Index]
if !exists {
blockVotes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.PossibleVotes = int(aggregationBits.Len())
for j := range aggregationBits.Len() {
if aggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
if blockVotes[attestationData.Slot][attestationData.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
if c.votes[attestationData.Slot][attestationData.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
blockVotes[attestationData.Slot][attestationData.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
var err error
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate head timely.
analysis.HeadTimely = attestation.Data.Slot == slot-1
analysis.HeadTimely = analysis.HeadCorrect && attestationData.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
analysis.SourceTimely = attestationData.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
@@ -161,7 +175,11 @@ func (c *command) analyzeAttestations(ctx context.Context, block *spec.Versioned
}
// Calculate target timely.
analysis.TargetTimely = attestation.Data.Slot >= slot-32
if block.Version < spec.DataVersionDeneb {
analysis.TargetTimely = attestationData.Slot >= slot-32
} else {
analysis.TargetTimely = true
}
}
// Calculate score and value.
@@ -188,12 +206,30 @@ func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedB
if err != nil {
return err
}
root, err := block.Root()
if err != nil {
panic(err)
}
slot, err := block.Slot()
if err != nil {
panic(err)
}
if c.debug {
fmt.Printf("Parent root of %#x@%d is %#x\n", root, slot, parentRoot)
}
// Obtain the parent block.
parentBlock, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", parentRoot))
parentBlockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: fmt.Sprintf("%#x", parentRoot),
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
return errors.New("empty beacon block")
}
return err
}
parentBlock := parentBlockResponse.Data
if parentBlock == nil {
return fmt.Errorf("unable to obtain parent block %s", parentBlock)
}
@@ -213,7 +249,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
@@ -231,23 +267,31 @@ func (c *command) processParentBlock(ctx context.Context, block *spec.VersionedS
if err != nil {
return err
}
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationDataInfo{
Block: slot,
Index: i,
}
data := attestation.Data
_, exists := c.votes[data.Slot]
if !exists {
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
attestationData, err := attestation.Data()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation data")
}
_, exists = c.votes[data.Slot][data.Index]
if !exists {
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
aggregationBits, err := attestation.AggregationBits()
if err != nil {
return errors.Wrap(err, "failed to obtain attestation aggregation bits")
}
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
c.votes[data.Slot][data.Index].SetBitAt(j, true)
_, exists := c.votes[attestationData.Slot]
if !exists {
c.votes[attestationData.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = c.votes[attestationData.Slot][attestationData.Index]
if !exists {
c.votes[attestationData.Slot][attestationData.Index] = bitfield.NewBitlist(aggregationBits.Len())
}
for j := range aggregationBits.Len() {
if aggregationBits.BitAt(j) {
c.votes[attestationData.Slot][attestationData.Index].SetBitAt(j, true)
}
}
}
@@ -259,15 +303,19 @@ 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)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
@@ -289,12 +337,12 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("connection does not provide spec information")
}
spec, err := specProvider.Spec(ctx)
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["TIMELY_SOURCE_WEIGHT"]
tmp, exists := specResponse.Data["TIMELY_SOURCE_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
@@ -305,7 +353,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("TIMELY_SOURCE_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_TARGET_WEIGHT"]
tmp, exists = specResponse.Data["TIMELY_TARGET_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(26)
@@ -315,7 +363,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("TIMELY_TARGET_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_HEAD_WEIGHT"]
tmp, exists = specResponse.Data["TIMELY_HEAD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
@@ -325,7 +373,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("TIMELY_HEAD_WEIGHT of unexpected type")
}
tmp, exists = spec["SYNC_REWARD_WEIGHT"]
tmp, exists = specResponse.Data["SYNC_REWARD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(2)
@@ -335,7 +383,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("SYNC_REWARD_WEIGHT of unexpected type")
}
tmp, exists = spec["PROPOSER_WEIGHT"]
tmp, exists = specResponse.Data["PROPOSER_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(8)
@@ -345,7 +393,7 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("PROPOSER_WEIGHT of unexpected type")
}
tmp, exists = spec["WEIGHT_DENOMINATOR"]
tmp, exists = specResponse.Data["WEIGHT_DENOMINATOR"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(64)
@@ -357,63 +405,90 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
func (c *command) calcHeadCorrect(ctx context.Context, attestation *spec.VersionedAttestation) (bool, error) {
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
slot := attestationData.Slot
root, exists := c.headRoots[slot]
if !exists {
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
return false, nil
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
if c.debug {
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
}
return false, nil
}
return false, err
}
if header == nil {
if response.Data == nil {
// No block.
slot--
continue
}
if !header.Canonical {
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
c.headRoots[attestation.Data.Slot] = header.Root
root = header.Root
c.headRoots[slot] = response.Data.Root
root = response.Data.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
return bytes.Equal(root[:], attestationData.BeaconBlockRoot[:]), nil
}
func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
root, exists := c.targetRoots[attestation.Data.Slot]
func (c *command) calcTargetCorrect(ctx context.Context, attestation *spec.VersionedAttestation) (bool, error) {
attestationData, err := attestation.Data()
if err != nil {
return false, errors.Wrap(err, "failed to obtain attestation data")
}
root, exists := c.targetRoots[attestationData.Slot]
if !exists {
// Start with first slot of the target epoch.
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
slot := c.chainTime.FirstSlotOfEpoch(attestationData.Target.Epoch)
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
response, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, &api.BeaconBlockHeaderOpts{
Block: fmt.Sprintf("%d", slot),
})
if err != nil {
return false, nil
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
if c.debug {
fmt.Printf("No block available for slot %d, assuming not in canonical chain", slot)
}
return false, nil
}
}
if header == nil {
if response.Data == nil {
// No block.
slot--
continue
}
if !header.Canonical {
if !response.Data.Canonical {
// Not canonical.
slot--
continue
}
c.targetRoots[attestation.Data.Slot] = header.Root
root = header.Root
c.targetRoots[attestationData.Slot] = response.Data.Root
root = response.Data.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
return bytes.Equal(root[:], attestationData.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:
@@ -432,6 +507,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.DataVersionCapella:
c.analysis.SyncCommitee.Contributions = int(block.Capella.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Capella.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
case spec.DataVersionDeneb:
c.analysis.SyncCommitee.Contributions = int(block.Deneb.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Deneb.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
case spec.DataVersionElectra:
c.analysis.SyncCommitee.Contributions = int(block.Electra.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Electra.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
default:
return fmt.Errorf("unsupported block version %d", block.Version)
}

View File

@@ -39,7 +39,7 @@ func TestProcess(t *testing.T) {
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"blockid": "invalid",
},
err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"Invalid block: invalid\"}",
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

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package blockanalyze
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,14 +27,19 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, 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
@@ -34,8 +34,9 @@ type dataIn struct {
jsonOutput bool
sszOutput bool
// Chain information.
blockID string
stream bool
blockID string
blockTime string
stream bool
}
func input(ctx context.Context) (*dataIn, error) {
@@ -50,21 +51,20 @@ func input(ctx context.Context) (*dataIn, error) {
data.debug = viper.GetBool("debug")
data.jsonOutput = viper.GetBool("json")
data.sszOutput = viper.GetBool("ssz")
data.blockID = viper.GetString("blockid")
data.blockTime = viper.GetString("block-time")
data.stream = viper.GetBool("stream")
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: viper.GetString("connection"),
Timeout: viper.GetDuration("timeout"),
AllowInsecure: viper.GetBool("allow-insecure-connections"),
LogFallback: !data.quiet,
})
if err != nil {
return nil, err
}
if viper.GetString("blockid") == "" {
data.blockID = "head"
} else {
// Specific slot.
data.blockID = viper.GetString("blockid")
}
return data, nil
}

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 any beacon node",
},
{
name: "ConnectionBad",
vars: map[string]interface{}{

File diff suppressed because it is too large Load Diff

View File

@@ -175,3 +175,58 @@ func TestOutputBlockETH1Data(t *testing.T) {
})
}
}
func TestBlockGraffiti(t *testing.T) {
tests := []struct {
name string
graffiti []byte
res string
}{
{
name: "Empty",
graffiti: []byte(""),
},
{
name: "NoID",
graffiti: []byte("No identifier"),
res: "Graffiti: No identifier\n",
},
{
name: "SingleClient",
graffiti: []byte("Graffiti TK"),
res: "Graffiti: Graffiti\nConsensus client: teku\n",
},
{
name: "SingleClientImmediate",
graffiti: []byte("TK"),
res: "Consensus client: teku\n",
},
{
name: "SingleClientAndHashImmediate",
graffiti: []byte("TKa9f98260"),
res: "Consensus client: teku (version hash a9f98260)\n",
},
{
name: "DualClients",
graffiti: []byte("LHGE"),
res: "Consensus client: lighthouse\nExecution client: go-ethereum\n",
},
{
name: "DualClientsReverseOrder",
graffiti: []byte("GELH"),
res: "Consensus client: lighthouse\nExecution client: go-ethereum\n",
},
{
name: "DualClientsTruncatedHash",
graffiti: []byte("Freedom To Transact TKa9f9NM220b"),
res: "Graffiti: Freedom To Transact\nConsensus client: teku (version hash a9f9)\nExecution client: nethermind (version hash 220b)\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res := blockGraffiti(context.Background(), test.graffiti)
require.Equal(t, test.res, res)
})
}
}

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
@@ -17,25 +17,39 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/api"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/deneb"
"github.com/attestantio/go-eth2-client/spec/electra"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
)
var jsonOutput bool
var 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 == "" && data.blockTime == "" {
return nil, errors.New("no block ID or block time")
}
results = &dataOut{
debug: data.debug,
@@ -43,41 +57,45 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
eth2Client: data.eth2Client,
}
config, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx)
err := populateResults(ctx, results)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
return nil, err
}
genesis, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
}
results.genesisTime = genesis.GenesisTime
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID)
if data.blockTime != "" {
data.blockID, err = timeToBlockID(ctx, data.eth2Client, data.blockTime)
if err != nil {
return nil, err
}
}
block, err := obtainBlock(ctx, data, results)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon block")
return nil, err
}
if signedBlock == nil {
return nil, errors.New("empty beacon block")
if data.quiet {
os.Exit(0)
}
switch signedBlock.Version {
switch block.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(ctx, data.jsonOutput, signedBlock.Phase0); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
err = outputPhase0Block(ctx, data.jsonOutput, block.Phase0)
case spec.DataVersionAltair:
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Altair); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
err = outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, block.Altair)
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
err = outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, block.Bellatrix)
case spec.DataVersionCapella:
err = outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, block.Capella)
case spec.DataVersionDeneb:
err = processDenebBlock(ctx, data, block)
case spec.DataVersionElectra:
err = processElectraBlock(ctx, data, block)
default:
return nil, errors.New("unknown block version")
}
if err != nil {
return nil, errors.Wrap(err, "failed to process block")
}
if data.stream {
jsonOutput = data.jsonOutput
@@ -95,54 +113,168 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return &dataOut{}, nil
}
func headEventHandler(event *api.Event) {
func populateResults(ctx context.Context, results *dataOut) error {
specResponse, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
if err != nil {
return errors.Wrap(err, "failed to connect to obtain configuration information")
}
genesisResponse, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
if err != nil {
return errors.Wrap(err, "failed to connect to obtain genesis information")
}
genesis := genesisResponse.Data
results.genesisTime = genesis.GenesisTime
results.slotDuration = specResponse.Data["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = specResponse.Data["SLOTS_PER_EPOCH"].(uint64)
return nil
}
func obtainBlock(ctx context.Context, data *dataIn, results *dataOut,
) (
*spec.VersionedSignedBeaconBlock,
error,
) {
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: data.blockID,
})
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
if data.quiet {
os.Exit(1)
}
return nil, errors.New("empty beacon block")
}
return nil, errors.Wrap(err, "failed to obtain beacon block")
}
return blockResponse.Data, nil
}
func processDenebBlock(ctx context.Context,
data *dataIn,
block *spec.VersionedSignedBeaconBlock,
) error {
var blobSidecars []*deneb.BlobSidecar
kzgCommitments, err := block.BlobKZGCommitments()
if err != nil {
return err
}
if len(kzgCommitments) > 0 {
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
Block: data.blockID,
})
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode != http.StatusNotFound {
return errors.Wrap(err, "failed to obtain blob sidecars")
}
} else {
blobSidecars = blobSidecarsResponse.Data
}
}
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, block.Deneb, blobSidecars); err != nil {
return errors.Wrap(err, "failed to output block")
}
return nil
}
func processElectraBlock(ctx context.Context,
data *dataIn,
block *spec.VersionedSignedBeaconBlock,
) error {
var blobSidecars []*deneb.BlobSidecar
kzgCommitments, err := block.BlobKZGCommitments()
if err != nil {
return err
}
if len(kzgCommitments) > 0 {
blobSidecarsResponse, err := results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
Block: data.blockID,
})
if err != nil {
var apiErr *api.Error
if errors.As(err, &apiErr) && apiErr.StatusCode != http.StatusNotFound {
return errors.Wrap(err, "failed to obtain blob sidecars")
}
} else {
blobSidecars = blobSidecarsResponse.Data
}
}
if err := outputElectraBlock(ctx, data.jsonOutput, data.sszOutput, block.Electra, blobSidecars); err != nil {
return errors.Wrap(err, "failed to output block")
}
return nil
}
func headEventHandler(event *apiv1.Event) {
ctx := context.Background()
// Only interested in head events.
if event.Topic != "head" {
return
}
blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:])
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID)
blockID := fmt.Sprintf("%#x", event.Data.(*apiv1.HeadEvent).Block[:])
blockResponse, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: blockID,
})
if err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to obtain block: %v\n", err)
}
return
}
if signedBlock == nil {
block := blockResponse.Data
if block == nil {
if !jsonOutput && !sszOutput {
fmt.Println("Empty beacon block")
}
return
}
switch signedBlock.Version {
switch block.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputPhase0Block(ctx, jsonOutput, block.Phase0)
case spec.DataVersionAltair:
if err := outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputAltairBlock(ctx, jsonOutput, sszOutput, block.Altair)
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
err = outputBellatrixBlock(ctx, jsonOutput, sszOutput, block.Bellatrix)
case spec.DataVersionCapella:
err = outputCapellaBlock(ctx, jsonOutput, sszOutput, block.Capella)
case spec.DataVersionDeneb:
var blobSidecars []*deneb.BlobSidecar
var kzgCommitments []deneb.KZGCommitment
kzgCommitments, err = block.BlobKZGCommitments()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain KZG commitments: %v\n", err)
return
}
default:
if !jsonOutput && !sszOutput {
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
if len(kzgCommitments) > 0 {
var blobSidecarsResponse *api.Response[[]*deneb.BlobSidecar]
blobSidecarsResponse, err = results.eth2Client.(eth2client.BlobSidecarsProvider).BlobSidecars(ctx, &api.BlobSidecarsOpts{
Block: blockID,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain blob sidecars: %v\n", err)
return
}
blobSidecars = blobSidecarsResponse.Data
}
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, block.Deneb, blobSidecars)
default:
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("")
}
@@ -213,3 +345,123 @@ func outputBellatrixBlock(ctx context.Context, jsonOutput bool, sszOutput bool,
}
return nil
}
func outputCapellaBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *capella.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
case sszOutput:
data, err := signedBlock.MarshalSSZ()
if err != nil {
return errors.Wrap(err, "failed to generate SSZ")
}
fmt.Printf("%x\n", data)
default:
data, err := outputCapellaBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Print(data)
}
return nil
}
func outputDenebBlock(ctx context.Context,
jsonOutput bool,
sszOutput bool,
signedBlock *deneb.SignedBeaconBlock,
blobs []*deneb.BlobSidecar,
) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
case sszOutput:
data, err := signedBlock.MarshalSSZ()
if err != nil {
return errors.Wrap(err, "failed to generate SSZ")
}
fmt.Printf("%x\n", data)
default:
data, err := outputDenebBlockText(ctx, results, signedBlock, blobs)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Print(data)
}
return nil
}
func outputElectraBlock(ctx context.Context,
jsonOutput bool,
sszOutput bool,
signedBlock *electra.SignedBeaconBlock,
blobs []*deneb.BlobSidecar,
) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
case sszOutput:
data, err := signedBlock.MarshalSSZ()
if err != nil {
return errors.Wrap(err, "failed to generate SSZ")
}
fmt.Printf("%x\n", data)
default:
data, err := outputElectraBlockText(ctx, results, signedBlock, blobs)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Print(data)
}
return nil
}
func timeToBlockID(ctx context.Context, eth2Client eth2client.Service, input string) (string, error) {
var timestamp time.Time
switch {
case strings.HasPrefix(input, "0x"):
// Hex string.
hexTime, err := strconv.ParseInt(strings.TrimPrefix(input, "0x"), 16, 64)
if err != nil {
return "", errors.Wrap(err, "failed to parse block time as hex string")
}
timestamp = time.Unix(hexTime, 0)
case !strings.Contains(input, ":"):
// No colon, assume decimal string.
decTime, err := strconv.ParseInt(input, 10, 64)
if err != nil {
return "", errors.Wrap(err, "failed to parse block time as decimal string")
}
timestamp = time.Unix(decTime, 0)
default:
dateTime, err := time.Parse("2006-01-02T15:04:05", input)
if err != nil {
return "", errors.Wrap(err, "failed to parse block time as datetime")
}
timestamp = dateTime
}
// Assume timestamp.
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return "", errors.Wrap(err, "failed to set up chaintime service")
}
return fmt.Sprintf("%d", chainTime.TimestampToSlot(timestamp)), nil
}

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

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 2019 - 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package blockinfo
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -0,0 +1,89 @@
// Copyright © 2025 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blocktrail
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Operation.
blockID string
jsonOutput bool
target string
maxBlocks int
// Data access.
consensusClient eth2client.Service
chainTime chaintime.Service
blocksProvider eth2client.SignedBeaconBlockProvider
blockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Processing.
justifiedCheckpoint *phase0.Checkpoint
finalizedCheckpoint *phase0.Checkpoint
// Results.
steps []*step
found bool
}
type step struct {
Slot phase0.Slot `json:"slot"`
Root phase0.Root `json:"root"`
ParentRoot phase0.Root `json:"parent_root"`
State string `json:"state,omitempty"`
// Not a slot, but we're using it to steal the JSON processing.
ExecutionBlock phase0.Slot `json:"execution_block"`
ExecutionHash phase0.Hash32 `json:"execution_hash"`
}
func newCommand(_ context.Context) (*command, error) {
c := &command{
timeout: viper.GetDuration("timeout"),
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
jsonOutput: viper.GetBool("json"),
connection: viper.GetString("connection"),
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
blockID: viper.GetString("blockid"),
target: viper.GetString("target"),
maxBlocks: viper.GetInt("max-blocks"),
steps: make([]*step, 0),
}
// Timeout.
if c.timeout == 0 {
return nil, errors.New("timeout is required")
}
return c, nil
}

74
cmd/block/trail/output.go Normal file
View File

@@ -0,0 +1,74 @@
// Copyright © 2025 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blocktrail
import (
"context"
"encoding/json"
"fmt"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.jsonOutput {
return c.outputJSON(ctx)
}
return c.outputTxt(ctx)
}
type simpleOut struct {
Start *step `json:"start"`
End *step `json:"end"`
Steps int `json:"distance"`
}
func (c *command) outputJSON(_ context.Context) (string, error) {
var err error
var data []byte
if c.verbose {
data, err = json.Marshal(c.steps)
} else {
basic := &simpleOut{
Start: c.steps[0],
End: c.steps[len(c.steps)-1],
Steps: len(c.steps) - 1,
}
data, err = json.Marshal(basic)
}
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputTxt(_ context.Context) (string, error) {
if !c.found {
return "Target not found", nil
}
builder := strings.Builder{}
builder.WriteString("Target '")
builder.WriteString(c.target)
builder.WriteString("' found at a distance of ")
builder.WriteString(fmt.Sprintf("%d", len(c.steps)-1))
builder.WriteString(" block(s)")
return builder.String(), nil
}

182
cmd/block/trail/process.go Normal file
View File

@@ -0,0 +1,182 @@
// Copyright © 2025 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blocktrail
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
untilRoot := phase0.Root{}
var untilBlock phase0.Slot
switch {
case strings.ToLower(c.target) == "justified", strings.ToLower(c.target) == "finalized":
// Nothing to do.
case strings.HasPrefix(c.target, "0x"):
// Assume a root.
if err := json.Unmarshal([]byte(fmt.Sprintf("%q", c.target)), &untilRoot); err != nil {
return err
}
default:
// Assume a block number.
tmp, err := strconv.ParseUint(c.target, 10, 64)
if err != nil {
return err
}
untilBlock = phase0.Slot(tmp)
}
blockID := c.blockID
for range c.maxBlocks {
step := &step{}
blockResponse, err := c.blocksProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{
Block: blockID,
})
if err != nil {
var apiError *api.Error
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound {
return errors.New("empty beacon block")
}
return errors.Wrap(err, "failed to obtain beacon block")
}
block := blockResponse.Data
step.Slot, err = block.Slot()
if err != nil {
return err
}
step.Root, err = block.Root()
if err != nil {
return err
}
step.ParentRoot, err = block.ParentRoot()
if err != nil {
return err
}
executionBlock, err := block.ExecutionBlockNumber()
if err != nil {
return err
}
step.ExecutionBlock = phase0.Slot(executionBlock)
step.ExecutionHash, err = block.ExecutionBlockHash()
if err != nil {
return err
}
if c.debug {
data, err := json.Marshal(step)
if err == nil {
fmt.Fprintf(os.Stderr, "Step is %s\n", string(data))
}
}
c.steps = append(c.steps, step)
blockID = step.ParentRoot.String()
if c.target == "justified" && bytes.Equal(step.Root[:], c.justifiedCheckpoint.Root[:]) {
c.found = true
break
}
if c.target == "finalized" && bytes.Equal(step.Root[:], c.finalizedCheckpoint.Root[:]) {
c.found = true
break
}
if untilBlock > 0 && step.Slot == untilBlock {
c.found = true
break
}
if (!untilRoot.IsZero()) && bytes.Equal(step.Root[:], untilRoot[:]) {
c.found = true
break
}
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.consensusClient, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{
Address: c.connection,
Timeout: c.timeout,
AllowInsecure: c.allowInsecureConnections,
LogFallback: !c.quiet,
})
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.consensusClient.(eth2client.SpecProvider)),
standardchaintime.WithGenesisProvider(c.consensusClient.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.blocksProvider, isProvider = c.consensusClient.(eth2client.SignedBeaconBlockProvider)
if !isProvider {
return errors.New("connection does not provide signed beacon block information")
}
c.blockHeadersProvider, isProvider = c.consensusClient.(eth2client.BeaconBlockHeadersProvider)
if !isProvider {
return errors.New("connection does not provide beacon block header information")
}
finalityProvider, isProvider := c.consensusClient.(eth2client.FinalityProvider)
if !isProvider {
return errors.New("connection does not provide finality information")
}
finalityResponse, err := finalityProvider.Finality(ctx, &api.FinalityOpts{
State: "head",
})
if err != nil {
return errors.Wrap(err, "failed to obtain finality")
}
finality := finalityResponse.Data
c.justifiedCheckpoint = finality.Justified
if c.debug {
fmt.Fprintf(os.Stderr, "Justified checkpoint is %d / %#x\n", c.justifiedCheckpoint.Epoch, c.justifiedCheckpoint.Root)
}
c.finalizedCheckpoint = finality.Finalized
if c.debug {
fmt.Fprintf(os.Stderr, "Finalized checkpoint is %d / %#x\n", c.finalizedCheckpoint.Epoch, c.finalizedCheckpoint.Root)
}
return nil
}

View File

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

59
cmd/block/trail/run.go Normal file
View File

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

View File

@@ -29,7 +29,7 @@ var blockAnalyzeCmd = &cobra.Command{
ethdo block analyze --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := blockanalyze.Run(cmd)
if err != nil {
return err
@@ -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

@@ -29,7 +29,7 @@ var blockInfoCmd = &cobra.Command{
ethdo block info --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := blockinfo.Run(cmd)
if err != nil {
return err
@@ -48,22 +48,22 @@ func init() {
blockCmd.AddCommand(blockInfoCmd)
blockFlags(blockInfoCmd)
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockInfoCmd.Flags().String("block-time", "", "the time of the block to fetch (format YYYY-MM-DDTHH:MM:SS, or a hex or decimal timestamp")
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
}
func blockInfoBindings() {
if err := viper.BindPFlag("blockid", blockInfoCmd.Flags().Lookup("blockid")); err != nil {
func blockInfoBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockInfoCmd.Flags().Lookup("stream")); err != nil {
if err := viper.BindPFlag("block-time", cmd.Flags().Lookup("block-time")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("ssz", blockInfoCmd.Flags().Lookup("ssz")); err != nil {
if err := viper.BindPFlag("ssz", cmd.Flags().Lookup("ssz")); err != nil {
panic(err)
}
}

65
cmd/blocktrail.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright © 2025 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
blocktrail "github.com/wealdtech/ethdo/cmd/block/trail"
)
var blockTrailCmd = &cobra.Command{
Use: "trail",
Short: "Trail back in the chain from a given block.",
Long: `Trail back in the chain for a given block. For example:
ethdo block trail --blockid=12345 --target=finalized
In quiet mode this will return 0 if the block trail ends up at the finalized state, otherwise 1.`,
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := blocktrail.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
blockCmd.AddCommand(blockTrailCmd)
blockFlags(blockTrailCmd)
blockTrailCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockTrailCmd.Flags().String("target", "justified", "the target block (block number, hash, justified or finalized)")
blockTrailCmd.Flags().Int("max-blocks", 16384, "the maximum number of blocks to look at before halting")
}
func blockTrailBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("target", cmd.Flags().Lookup("target")); err != nil {
panic(err)
}
if err := viper.BindPFlag("max-blocks", cmd.Flags().Lookup("max-blocks")); err != nil {
panic(err)
}
}

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

@@ -50,6 +50,8 @@ type command struct {
slot phase0.Slot
epoch phase0.Epoch
period uint64
periodStart time.Time
periodEnd time.Time
incumbent *phase0.ETH1Data
eth1DataVotes []*phase0.ETH1Data
votes map[string]*vote
@@ -60,7 +62,7 @@ type vote struct {
Count int `json:"count"`
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),

View File

@@ -24,11 +24,13 @@ import (
)
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"`
Period uint64 `json:"period"`
PeriodStart int64 `json:"period_start"`
PeriodEnd int64 `json:"period_end"`
Epoch phase0.Epoch `json:"epoch"`
Slot phase0.Slot `json:"slot"`
Incumbent *phase0.ETH1Data `json:"incumbent"`
Votes []*vote `json:"votes"`
}
func (c *command) output(ctx context.Context) (string, error) {
@@ -42,7 +44,7 @@ func (c *command) output(ctx context.Context) (string, error) {
return c.outputText(ctx)
}
func (c *command) outputJSON(ctx context.Context) (string, error) {
func (c *command) outputJSON(_ context.Context) (string, error) {
votes := make([]*vote, 0, len(c.votes))
totalVotes := 0
for _, vote := range c.votes {
@@ -57,11 +59,13 @@ func (c *command) outputJSON(ctx context.Context) (string, error) {
})
output := &jsonOutput{
Period: c.period,
Epoch: c.epoch,
Slot: c.slot,
Incumbent: c.incumbent,
Votes: votes,
Period: c.period,
PeriodStart: c.periodStart.Unix(),
PeriodEnd: c.periodEnd.Unix(),
Epoch: c.epoch,
Slot: c.slot,
Incumbent: c.incumbent,
Votes: votes,
}
data, err := json.Marshal(output)
if err != nil {
@@ -71,13 +75,18 @@ func (c *command) outputJSON(ctx context.Context) (string, error) {
return string(data), nil
}
func (c *command) outputText(ctx context.Context) (string, error) {
func (c *command) outputText(_ context.Context) (string, error) {
builder := strings.Builder{}
builder.WriteString("Voting period: ")
builder.WriteString(fmt.Sprintf("%d\n", c.period))
if c.verbose {
builder.WriteString("Period start: ")
builder.WriteString(fmt.Sprintf("%s\n", c.periodStart))
builder.WriteString("Period end: ")
builder.WriteString(fmt.Sprintf("%s\n", c.periodEnd))
builder.WriteString("Incumbent: ")
builder.WriteString(fmt.Sprintf("block %#x, deposit count %d\n", c.incumbent.BlockHash, c.incumbent.DepositCount))
}

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
@@ -20,6 +20,7 @@ import (
"strconv"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
@@ -58,10 +59,13 @@ func (c *command) process(ctx context.Context) error {
if fetchSlot > c.chainTime.CurrentSlot() {
fetchSlot = c.chainTime.CurrentSlot()
}
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", fetchSlot))
stateResponse, err := c.beaconStateProvider.BeaconState(ctx, &api.BeaconStateOpts{
State: fmt.Sprintf("%d", fetchSlot),
})
if err != nil {
return errors.Wrap(err, "failed to obtain state")
}
state := stateResponse.Data
if state == nil {
return errors.New("state not returned by beacon node")
}
@@ -73,28 +77,36 @@ func (c *command) process(ctx context.Context) error {
}
}
c.slot, err = state.Slot()
if err != nil {
return errors.Wrap(err, "failed to obtain slot")
}
switch state.Version {
case spec.DataVersionPhase0:
c.slot = phase0.Slot(state.Phase0.Slot)
c.incumbent = state.Phase0.ETH1Data
c.eth1DataVotes = state.Phase0.ETH1DataVotes
case spec.DataVersionAltair:
c.slot = state.Altair.Slot
c.incumbent = state.Altair.ETH1Data
c.eth1DataVotes = state.Altair.ETH1DataVotes
case spec.DataVersionBellatrix:
c.slot = state.Bellatrix.Slot
c.incumbent = state.Bellatrix.ETH1Data
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
case spec.DataVersionCapella:
c.slot = state.Capella.Slot
c.incumbent = state.Capella.ETH1Data
c.eth1DataVotes = state.Capella.ETH1DataVotes
case spec.DataVersionDeneb:
c.incumbent = state.Deneb.ETH1Data
c.eth1DataVotes = state.Deneb.ETH1DataVotes
case spec.DataVersionElectra:
c.incumbent = state.Electra.ETH1Data
c.eth1DataVotes = state.Electra.ETH1DataVotes
default:
return fmt.Errorf("unhandled beacon state version %v", state.Version)
}
c.period = uint64(c.epoch) / c.epochsPerEth1VotingPeriod
c.periodStart = c.chainTime.StartOfEpoch(phase0.Epoch(c.period * c.epochsPerEth1VotingPeriod))
c.periodEnd = c.chainTime.StartOfEpoch(phase0.Epoch((c.period + 1) * c.epochsPerEth1VotingPeriod))
c.votes = make(map[string]*vote)
for _, eth1Vote := range c.eth1DataVotes {
@@ -114,15 +126,19 @@ 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)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
@@ -138,12 +154,12 @@ func (c *command) setup(ctx context.Context) error {
return errors.New("connection does not provide spec information")
}
spec, err := specProvider.Spec(ctx)
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["SLOTS_PER_EPOCH"]
tmp, exists := specResponse.Data["SLOTS_PER_EPOCH"]
if !exists {
return errors.New("spec did not contain SLOTS_PER_EPOCH")
}
@@ -152,7 +168,7 @@ func (c *command) setup(ctx context.Context) error {
if !good {
return errors.New("SLOTS_PER_EPOCH value invalid")
}
tmp, exists = spec["EPOCHS_PER_ETH1_VOTING_PERIOD"]
tmp, exists = specResponse.Data["EPOCHS_PER_ETH1_VOTING_PERIOD"]
if !exists {
return errors.New("spec did not contain EPOCHS_PER_ETH1_VOTING_PERIOD")
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package chaineth1votes
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,14 +27,19 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -47,7 +47,7 @@ type command struct {
exitQueue int
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),

View File

@@ -36,7 +36,7 @@ func (c *command) output(ctx context.Context) (string, error) {
return c.outputText(ctx)
}
func (c *command) outputJSON(ctx context.Context) (string, error) {
func (c *command) outputJSON(_ context.Context) (string, error) {
output := &jsonOutput{
ActivationQueue: c.activationQueue,
ExitQueue: c.exitQueue,
@@ -49,7 +49,7 @@ func (c *command) outputJSON(ctx context.Context) (string, error) {
return string(data), nil
}
func (c *command) outputText(ctx context.Context) (string, error) {
func (c *command) outputText(_ context.Context) (string, error) {
builder := strings.Builder{}
if c.activationQueue > 0 {

View File

@@ -18,6 +18,7 @@ import (
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
@@ -34,12 +35,14 @@ func (c *command) process(ctx context.Context) error {
return err
}
validators, err := c.validatorsProvider.Validators(ctx, fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)), nil)
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: fmt.Sprintf("%d", c.chainTime.FirstSlotOfEpoch(epoch)),
})
if err != nil {
return errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validators {
for _, validator := range response.Data {
if validator.Validator == nil {
continue
}
@@ -58,15 +61,19 @@ 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)),
standardchaintime.WithGenesisProvider(c.eth2Client.(eth2client.GenesisProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package chainqueues
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,14 +27,19 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
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 {

View File

@@ -16,6 +16,7 @@ package chaintime
import (
"context"
"fmt"
"strconv"
"strings"
"time"
@@ -34,6 +35,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 +43,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 +58,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(strconv.FormatInt(data.epochStart.Unix(), 10))
builder.WriteString(")")
}
builder.WriteString("\n Epoch end ")
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
if data.verbose {
builder.WriteString(" (")
builder.WriteString(strconv.FormatInt(data.epochEnd.Unix(), 10))
builder.WriteString(")")
}
builder.WriteString("\nSlot ")
builder.WriteString(fmt.Sprintf("%d", data.slot))
builder.WriteString("\n Slot start ")
builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05"))
if data.verbose {
builder.WriteString(" (")
builder.WriteString(strconv.FormatInt(data.slotStart.Unix(), 10))
builder.WriteString(")")
}
builder.WriteString("\n Slot end ")
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
if data.verbose {
builder.WriteString(" (")
builder.WriteString(strconv.FormatInt(data.slotEnd.Unix(), 10))
builder.WriteString(")")
}
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(strconv.FormatUint(data.syncCommitteePeriod, 10))
builder.WriteString("\n Sync committee period start ")
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
if data.verbose {
builder.WriteString(", ")
builder.WriteString(strconv.FormatInt(data.syncCommitteePeriodStart.Unix(), 10))
}
builder.WriteString(")\n Sync committee period end ")
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
if data.verbose {
builder.WriteString(", ")
builder.WriteString(strconv.FormatInt(data.syncCommitteePeriodEnd.Unix(), 10))
}
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}

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.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
)
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

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading
// Copyright © 2021, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package chaintime
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -26,7 +26,7 @@ func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
@@ -34,7 +34,12 @@ func Run(cmd *cobra.Command) (string, error) {
dataOut, err := process(ctx, dataIn)
if err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := output(ctx, dataOut)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

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"),

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

@@ -22,6 +22,7 @@ import (
"os"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
@@ -38,6 +39,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 +86,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")
}
@@ -97,45 +104,48 @@ func (c *command) setup(ctx context.Context) error {
}
stateID := fmt.Sprintf("%d", c.item.Message.Contribution.Slot)
validators, err := c.validatorsProvider.Validators(ctx,
stateID,
[]phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
)
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: stateID,
Indices: []phase0.ValidatorIndex{c.item.Message.AggregatorIndex},
})
if err != nil {
return errors.Wrap(err, "failed to obtain validator information")
}
if len(validators) == 0 || validators[c.item.Message.AggregatorIndex] == nil {
if len(response.Data) == 0 || response.Data[c.item.Message.AggregatorIndex] == nil {
return nil
}
c.validatorKnown = true
c.validator = validators[c.item.Message.AggregatorIndex]
c.validator = response.Data[c.item.Message.AggregatorIndex]
// Obtain the sync committee
syncCommitteesProvider, isProvider := c.eth2Client.(eth2client.SyncCommitteesProvider)
if !isProvider {
return errors.New("connection does not provide sync committee information")
}
c.syncCommittee, err = syncCommitteesProvider.SyncCommittee(ctx, stateID)
syncCommitteeResponse, err := syncCommitteesProvider.SyncCommittee(ctx, &api.SyncCommitteeOpts{
State: stateID,
})
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee information")
}
c.syncCommittee = syncCommitteeResponse.Data
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)
if !isProvider {
return false, errors.New("connection does not provide spec information")
}
var err error
c.spec, err = specProvider.Spec(ctx)
specResponse, err := specProvider.Spec(ctx, &api.SpecOpts{})
if err != nil {
return false, errors.Wrap(err, "failed to obtain spec information")
}
c.spec = specResponse.Data
tmp, exists := c.spec["SYNC_COMMITTEE_SIZE"]
if !exists {
@@ -204,13 +214,14 @@ 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
subCommittee := c.syncCommittee.ValidatorAggregates[c.item.Message.Contribution.SubcommitteeIndex]
includedIndices := make([]phase0.ValidatorIndex, 0, len(subCommittee))
for i := uint64(0); i < c.item.Message.Contribution.AggregationBits.Len(); i++ {
for i := range c.item.Message.Contribution.AggregationBits.Len() {
if c.item.Message.Contribution.AggregationBits.BitAt(i) {
includedIndices = append(includedIndices, subCommittee[int(i)])
}
@@ -219,16 +230,19 @@ func (c *command) confirmContributionSignature(ctx context.Context) error {
fmt.Fprintf(os.Stderr, "Contribution validator indices: %v (%d)\n", includedIndices, len(includedIndices))
}
includedValidators, err := c.validatorsProvider.Validators(ctx, "head", includedIndices)
response, err := c.validatorsProvider.Validators(ctx, &api.ValidatorsOpts{
State: "head",
Indices: includedIndices,
})
if err != nil {
return errors.Wrap(err, "failed to obtain subcommittee validators")
}
if len(includedValidators) == 0 {
if len(response.Data) == 0 {
return errors.New("obtained empty subcommittee validator list")
}
var aggregatePubKey *e2types.BLSPublicKey
for _, v := range includedValidators {
for _, v := range response.Data {
pubKeyBytes := make([]byte, 48)
copy(pubKeyBytes, v.Validator.PublicKey[:])
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
@@ -256,6 +270,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

View File

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading.
// Copyright © 2021, 2024 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,8 +15,8 @@ package chainverifysignedcontributionandproof
import (
"context"
"errors"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -27,14 +27,19 @@ func Run(cmd *cobra.Command) (string, error) {
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to set up command")
return "", errors.Join(errors.New("failed to set up command"), err)
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
switch {
case errors.Is(err, context.DeadlineExceeded):
return "", errors.New("operation timed out; try increasing with --timeout option")
default:
return "", errors.Join(errors.New("failed to process"), err)
}
}
if viper.GetBool("quiet") {
@@ -43,7 +48,7 @@ func Run(cmd *cobra.Command) (string, error) {
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
return "", errors.Join(errors.New("failed to obtain output"), err)
}
return results, nil

View File

@@ -31,7 +31,7 @@ var chainEth1VotesCmd = &cobra.Command{
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 {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := chaineth1votes.Run(cmd)
if err != nil {
return err
@@ -51,17 +51,13 @@ func init() {
chainFlags(chainEth1VotesCmd)
chainEth1VotesCmd.Flags().String("epoch", "", "epoch for which to fetch the votes")
chainEth1VotesCmd.Flags().String("period", "", "period for which to fetch the votes")
chainEth1VotesCmd.Flags().Bool("json", false, "output data in JSON format")
}
func chainEth1VotesBindings() {
if err := viper.BindPFlag("epoch", chainEth1VotesCmd.Flags().Lookup("epoch")); err != nil {
func chainEth1VotesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("period", chainEth1VotesCmd.Flags().Lookup("period")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", chainEth1VotesCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("period", cmd.Flags().Lookup("period")); err != nil {
panic(err)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020, 2022 Weald Technology Trading
// Copyright © 2020 - 2024 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -20,6 +20,8 @@ import (
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -34,43 +36,44 @@ var chainInfoCmd = &cobra.Command{
ethdo chain info
In quiet mode this will return 0 if the chain information can be obtained, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []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)
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
errCheck(err, "Failed to obtain beacon chain specification")
genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
genesisResponse, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{})
errCheck(err, "Failed to obtain beacon chain genesis")
fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
forkResponse, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, &api.ForkOpts{State: "head"})
errCheck(err, "Failed to obtain current fork")
if quiet {
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
if viper.GetBool("prepare-offline") {
fmt.Printf("Add the following to your command to run it offline:\n --offline --genesis-validators=root=%#x --fork-version=%#x\n", genesis.GenesisValidatorsRoot, fork.CurrentVersion)
os.Exit(_exitSuccess)
}
if genesis.GenesisTime.Unix() == 0 {
if genesisResponse.Data.GenesisTime.Unix() == 0 {
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()))
fmt.Printf("Genesis time: %s\n", genesisResponse.Data.GenesisTime.Format(time.UnixDate))
outputIf(viper.GetBool("verbose"), fmt.Sprintf("Genesis timestamp: %v", genesisResponse.Data.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 {
fmt.Printf("Genesis validators root: %#x\n", genesisResponse.Data.GenesisValidatorsRoot)
fmt.Printf("Genesis fork version: %#x\n", specResponse.Data["GENESIS_FORK_VERSION"].(spec.Version))
fmt.Printf("Current fork version: %#x\n", forkResponse.Data.CurrentVersion)
if viper.GetBool("verbose") {
forkData := &spec.ForkData{
CurrentVersion: fork.CurrentVersion,
GenesisValidatorsRoot: genesis.GenesisValidatorsRoot,
CurrentVersion: forkResponse.Data.CurrentVersion,
GenesisValidatorsRoot: genesisResponse.Data.GenesisValidatorsRoot,
}
forkDataRoot, err := forkData.HashTreeRoot()
if err == nil {
@@ -79,8 +82,12 @@ In quiet mode this will return 0 if the chain information can be obtained, other
fmt.Printf("Fork digest: %#x\n", forkDigest)
}
}
fmt.Printf("Seconds per slot: %d\n", int(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
fmt.Printf("Slots per epoch: %d\n", config["SLOTS_PER_EPOCH"].(uint64))
fmt.Printf("Seconds per slot: %d\n", int(specResponse.Data["SECONDS_PER_SLOT"].(time.Duration).Seconds()))
fmt.Printf("Slots per epoch: %d\n", specResponse.Data["SLOTS_PER_EPOCH"].(uint64))
depositContractAddress := bellatrix.ExecutionAddress{}
copy(depositContractAddress[:], specResponse.Data["DEPOSIT_CONTRACT_ADDRESS"].([]byte))
fmt.Printf("Deposit contract address: %s\n", depositContractAddress.String())
os.Exit(_exitSuccess)
},
@@ -89,11 +96,7 @@ In quiet mode this will return 0 if the chain information can be obtained, other
func init() {
chainCmd.AddCommand(chainInfoCmd)
chainFlags(chainInfoCmd)
chainInfoCmd.Flags().Bool("prepare-offline", false, "Provide information useful for offline commands")
}
func chainInfoBindings() {
if err := viper.BindPFlag("prepare-offline", chainInfoCmd.Flags().Lookup("prepare-offline")); err != nil {
panic(err)
}
func chainInfoBindings(_ *cobra.Command) {
}

View File

@@ -29,7 +29,7 @@ var chainQueuesCmd = &cobra.Command{
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 {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := chainqueues.Run(cmd)
if err != nil {
return err
@@ -48,14 +48,10 @@ func init() {
chainCmd.AddCommand(chainQueuesCmd)
chainFlags(chainQueuesCmd)
chainQueuesCmd.Flags().String("epoch", "", "epoch for which to fetch the queues")
chainQueuesCmd.Flags().Bool("json", false, "output data in JSON format")
}
func chainQueuesBindings() {
if err := viper.BindPFlag("epoch", chainQueuesCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", chainQueuesCmd.Flags().Lookup("json")); err != nil {
func chainQueuesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
}

99
cmd/chainspec.go Normal file
View File

@@ -0,0 +1,99 @@
// 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"
"strconv"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
"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(_ *cobra.Command, _ []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")
specResponse, err := eth2Client.(eth2client.SpecProvider).Spec(ctx, &api.SpecOpts{})
errCheck(err, "Failed to obtain chain specification")
if viper.GetBool("quiet") {
return
}
// Tweak the spec for output.
for k, v := range specResponse.Data {
switch t := v.(type) {
case phase0.Version:
specResponse.Data[k] = fmt.Sprintf("%#x", t)
case phase0.DomainType:
specResponse.Data[k] = fmt.Sprintf("%#x", t)
case time.Time:
specResponse.Data[k] = strconv.FormatInt(t.Unix(), 10)
case time.Duration:
specResponse.Data[k] = strconv.FormatUint(uint64(t.Seconds()), 10)
case []byte:
specResponse.Data[k] = fmt.Sprintf("%#x", t)
case uint64:
specResponse.Data[k] = strconv.FormatUint(t, 10)
}
}
if viper.GetBool("json") {
data, err := json.Marshal(specResponse.Data)
errCheck(err, "Failed to marshal JSON")
fmt.Printf("%s\n", string(data))
} else {
keys := make([]string, 0, len(specResponse.Data))
for k := range specResponse.Data {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s: %v\n", key, specResponse.Data[key])
}
}
},
}
func init() {
chainCmd.AddCommand(chainSpecCmd)
chainFlags(chainSpecCmd)
}
func chainSpecBindings(_ *cobra.Command) {
}

View File

@@ -17,14 +17,19 @@ import (
"context"
"fmt"
"os"
"strconv"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/api"
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{
@@ -35,23 +40,36 @@ var chainStatusCmd = &cobra.Command{
ethdo chain status
In quiet mode this will return 0 if the chain status can be obtained, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
Run: func(_ *cobra.Command, _ []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.WithGenesisProvider(eth2Client.(eth2client.GenesisProvider)),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
)
errCheck(err, "Failed to configure chaintime service")
finalityProvider, isProvider := eth2Client.(eth2client.FinalityProvider)
assert(isProvider, "beacon node does not provide finality; cannot report on chain status")
finality, err := finalityProvider.Finality(ctx, "head")
finalityResponse, err := finalityProvider.Finality(ctx, &api.FinalityOpts{
State: "head",
})
errCheck(err, "Failed to obtain finality information")
finality := finalityResponse.Data
blockProvider, isProvider := eth2Client.(eth2client.SignedBeaconBlockProvider)
assert(isProvider, "beacon node does not provide signed beacon blocks; cannot report on chain status")
blockResponse, err := blockProvider.SignedBeaconBlock(ctx, &api.SignedBeaconBlockOpts{Block: "head"})
errCheck(err, "Failed to obtain block information")
block := blockResponse.Data
slot := chainTime.CurrentSlot()
@@ -66,17 +84,33 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
nextEpochStartSlot := chainTime.FirstSlotOfEpoch(nextEpoch)
nextEpochTimestamp := chainTime.StartOfEpoch(nextEpoch)
headSlot, err := block.Slot()
errCheck(err, "Failed to obtain block slot")
res := strings.Builder{}
res.WriteString("Current slot: ")
res.WriteString(fmt.Sprintf("%d", slot))
res.WriteString("\n")
res.WriteString("Head slot: ")
res.WriteString(fmt.Sprintf("%d", headSlot))
if headSlot != slot {
if slot-headSlot == 1 {
res.WriteString(" (1 slot behind)")
} else {
res.WriteString(" (")
res.WriteString(fmt.Sprintf("%d", slot-headSlot))
res.WriteString(" slots behind)")
}
}
res.WriteString("\n")
res.WriteString("Current epoch: ")
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 +133,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 +143,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 {
validatorsResponse, err := validatorsProvider.Validators(ctx, &api.ValidatorsOpts{State: "head"})
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 validatorsResponse.Data {
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)
@@ -127,10 +189,10 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
nextPeriodTimestamp := chainTime.StartOfEpoch(nextPeriodStartEpoch)
res.WriteString("Sync committee period: ")
res.WriteString(fmt.Sprintf("%d", period))
res.WriteString(strconv.FormatUint(period, 10))
res.WriteString("\n")
if verbose {
if viper.GetBool("verbose") {
res.WriteString("Sync committee epochs: ")
res.WriteString(fmt.Sprintf("%d", periodStartEpoch))
res.WriteString("-")

View File

@@ -27,7 +27,7 @@ var chainTimeCmd = &cobra.Command{
Long: `Obtain info about the chain at a given time. For example:
ethdo chain time --slot=12345`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := chaintime.Run(cmd)
if err != nil {
return err
@@ -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

@@ -28,7 +28,7 @@ var chainVerifySignedContributionAndProofCmd = &cobra.Command{
ethdo chain verify signedcontributionandproof --data=... --validator=...
validator can be an account, a public key or an index.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := chainverifysignedcontributionandproof.Run(cmd)
if err != nil {
return err

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
@@ -16,38 +16,41 @@ package cmd
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"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",
Short: "Verify deposit data matches the provided data",
Long: `Verify deposit data matches the provided input data. For example:
ethdo deposit verify --data=depositdata.json --withdrawalaccount=primary/current --value="32 Ether"
ethdo deposit verify --data=depositdata.json --withdrawalaccount=primary/current --depositvalue="32 Ether"
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.`,
Run: func(cmd *cobra.Command, args []string) {
In quiet mode this will return 0 if the data is verified correctly, otherwise 1.`,
Run: func(_ *cobra.Command, _ []string) {
assert(depositVerifyData != "", "--data is required")
var data []byte
var err error
@@ -64,7 +67,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) + "]")
@@ -73,6 +76,12 @@ In quiet mode this will return 0 if the the data is verified correctly, otherwis
deposits, err := util.DepositInfoFromJSON(data)
errCheck(err, "Failed to fetch deposit data")
if viper.GetBool("debug") {
data, err := json.Marshal(deposits)
if err == nil {
fmt.Fprintf(os.Stderr, "Deposit data is %s\n", string(data))
}
}
var withdrawalCredentials []byte
if depositVerifyWithdrawalPubKey != "" {
@@ -91,7 +100,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 +128,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 +164,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,34 +198,34 @@ 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 phase0.BLSPubKey
@@ -236,34 +245,37 @@ 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(!quiet, "Deposit message root not supplied; not checked")
} else {
switch {
case len(deposit.DepositMessageRoot) != 32:
outputIf(!viper.GetBool("quiet"), "Deposit message root not supplied; not checked")
case len(withdrawalCredentials) != 32:
outputIf(!viper.GetBool("quiet"), "Withdrawal credentials not available; cannot recreate deposit message")
default:
// We can also verify the deposit message signature.
depositMessage := &phase0.DepositMessage{
PublicKey: pubKey,
@@ -276,9 +288,9 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
}
if bytes.Equal(deposit.DepositMessageRoot, depositMessageRoot[:]) {
outputIf(!quiet, "Deposit message root verified")
outputIf(!viper.GetBool("quiet"), "Deposit message root verified")
} else {
outputIf(!quiet, "Deposit message root incorrect")
outputIf(!viper.GetBool("quiet"), "Deposit message root incorrect")
return false, nil
}
@@ -304,9 +316,9 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali
}
signatureVerified := blsSig.Verify(containerRoot[:], validatorPubKey)
if signatureVerified {
outputIf(!quiet, "Deposit message signature verified")
outputIf(!viper.GetBool("quiet"), "Deposit message signature verified")
} else {
outputIf(!quiet, "Deposit message signature NOT verified")
outputIf(!viper.GetBool("quiet"), "Deposit message signature NOT verified")
return false, nil
}
}

View File

@@ -18,7 +18,7 @@ import (
"github.com/spf13/viper"
)
// epochCmd represents the epoch command
// epochCmd represents the epoch command.
var epochCmd = &cobra.Command{
Use: "epoch",
Short: "Obtain information about Ethereum 2 epochs",
@@ -30,11 +30,11 @@ func init() {
}
func epochFlags(cmd *cobra.Command) {
epochSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)")
cmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)")
}
func epochBindings() {
if err := viper.BindPFlag("epoch", epochSummaryCmd.Flags().Lookup("epoch")); err != nil {
func epochBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
}

View File

@@ -15,9 +15,12 @@ package epochsummary
import (
"context"
"math/big"
"time"
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/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
@@ -35,9 +38,11 @@ type command struct {
allowInsecureConnections bool
// Operation.
epoch string
stream bool
jsonOutput bool
epoch string
validatorsStr []string
validators map[phase0.ValidatorIndex]struct{}
stream bool
jsonOutput bool
// Data access.
eth2Client eth2client.Service
@@ -49,49 +54,102 @@ type command struct {
beaconCommitteesProvider eth2client.BeaconCommitteesProvider
beaconBlockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Intermediate data.
validatorInfo map[phase0.ValidatorIndex]*apiv1.Validator
participatingValidators map[phase0.ValidatorIndex]struct{}
headCorrectValidators map[phase0.ValidatorIndex]struct{}
headTimelyValidators map[phase0.ValidatorIndex]struct{}
sourceTimelyValidators map[phase0.ValidatorIndex]struct{}
targetCorrectValidators map[phase0.ValidatorIndex]struct{}
targetTimelyValidators map[phase0.ValidatorIndex]struct{}
participations map[phase0.ValidatorIndex]*attestingValidator
// Caches.
blocksCache map[string]*spec.VersionedSignedBeaconBlock
// 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"`
Epoch phase0.Epoch `json:"epoch"`
FirstSlot phase0.Slot `json:"first_slot"`
LastSlot phase0.Slot `json:"last_slot"`
Blocks int `json:"blocks"`
Proposals []*epochProposal `json:"proposals"`
SyncCommitteeValidators int `json:"sync_committee_validators"`
SyncCommittee []*epochSyncCommittee `json:"sync_committees"`
ActiveValidators int `json:"active_validators"`
ActiveBalance *big.Int `json:"active_balance"`
ParticipatingValidators int `json:"participating_validators"`
ParticipatingBalance *big.Int `json:"participating_balance"`
HeadCorrectValidators int `json:"head_correct_validators"`
HeadCorrectBalance *big.Int `json:"head_correct_balance"`
HeadTimelyValidators int `json:"head_timely_validators"`
HeadTimelyBalance *big.Int `json:"head_timely_balance"`
SourceTimelyValidators int `json:"source_timely_validators"`
SourceTimelyBalance *big.Int `json:"source_timely_balance"`
TargetCorrectValidators int `json:"target_correct_validators"`
TargetCorrectBalance *big.Int `json:"target_correct_balance"`
TargetTimelyValidators int `json:"target_timely_validators"`
TargetTimelyBalance *big.Int `json:"target_timely_balance"`
NonParticipatingValidators []*attestingValidator `json:"nonparticipating_validators"`
NonHeadCorrectValidators []*attestingValidator `json:"nonheadcorrect_validators"`
NonHeadTimelyValidators []*attestingValidator `json:"nonheadtimely_validators"`
NonTargetCorrectValidators []*attestingValidator `json:"nontargetcorrect_validators"`
NonSourceTimelyValidators []*attestingValidator `json:"nonsourcetimely_validators"`
Blobs int `json:"blobs"`
}
type epochProposal struct {
Slot phase0.Slot `json:"slot"`
Proposer phase0.ValidatorIndex `json:"proposer"`
Block bool `json:"block"`
ValidatorIndex phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"`
Block bool `json:"block"`
}
type epochSyncCommittee struct {
Index phase0.ValidatorIndex `json:"index"`
Missed int `json:"missed"`
ValidatorIndex phase0.ValidatorIndex `json:"validator_index"`
Missed int `json:"missed"`
MissedSlots []phase0.Slot `json:"missed_slots"`
}
type nonParticipatingValidator struct {
Validator phase0.ValidatorIndex `json:"validator_index"`
Slot phase0.Slot `json:"slot"`
Committee phase0.CommitteeIndex `json:"committee_index"`
type attestingValidator struct {
Validator phase0.ValidatorIndex `json:"validator_index"`
EffectiveBalance phase0.Gwei `json:"effective_balance"`
Slot phase0.Slot `json:"slot"`
Committee phase0.CommitteeIndex `json:"committee_index"`
HeadVote *phase0.Root `json:"head_vote,omitempty"`
Head *phase0.Root `json:"head,omitempty"`
TargetVote *phase0.Root `json:"target_vote,omitempty"`
Target *phase0.Root `json:"target,omitempty"`
InclusionSlot phase0.Slot `json:"inclusion_slot,omitempty"`
}
func newCommand(ctx context.Context) (*command, error) {
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
summary: &epochSummary{},
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
validatorsStr: viper.GetStringSlice("validators"),
summary: &epochSummary{
Proposals: make([]*epochProposal, 0),
ActiveBalance: big.NewInt(0),
ParticipatingBalance: big.NewInt(0),
HeadCorrectBalance: big.NewInt(0),
HeadTimelyBalance: big.NewInt(0),
SourceTimelyBalance: big.NewInt(0),
TargetCorrectBalance: big.NewInt(0),
TargetTimelyBalance: big.NewInt(0),
},
validators: make(map[phase0.ValidatorIndex]struct{}),
participatingValidators: make(map[phase0.ValidatorIndex]struct{}),
headCorrectValidators: make(map[phase0.ValidatorIndex]struct{}),
headTimelyValidators: make(map[phase0.ValidatorIndex]struct{}),
sourceTimelyValidators: make(map[phase0.ValidatorIndex]struct{}),
targetCorrectValidators: make(map[phase0.ValidatorIndex]struct{}),
targetTimelyValidators: make(map[phase0.ValidatorIndex]struct{}),
participations: make(map[phase0.ValidatorIndex]*attestingValidator),
blocksCache: make(map[string]*spec.VersionedSignedBeaconBlock),
}
// Timeout.

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2025 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -17,6 +17,7 @@ import (
"context"
"encoding/json"
"fmt"
"math/big"
"strings"
)
@@ -50,7 +51,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
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))
missedProposals = append(missedProposals, fmt.Sprintf("\n Slot %d (validator %d)", proposal.Slot, proposal.ValidatorIndex))
} else {
proposedBlocks++
}
@@ -64,17 +65,85 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
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(fmt.Sprintf("%d", proposal.ValidatorIndex))
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)))
gweiToEth := big.NewInt(1e9)
mul := big.NewInt(10000)
participatingBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.ParticipatingBalance, mul), c.summary.ActiveBalance)
builder.WriteString(fmt.Sprintf("\n Attesting balance: %s/%s (%0.2f%%)",
new(big.Int).Div(c.summary.ParticipatingBalance, gweiToEth).String(),
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
float64(participatingBalancePct.Uint64())/100.0,
))
sourceTimelyBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.SourceTimelyBalance, mul), c.summary.ActiveBalance)
builder.WriteString(fmt.Sprintf("\n Source timely: %s/%s (%0.2f%%)",
new(big.Int).Div(c.summary.SourceTimelyBalance, gweiToEth).String(),
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
float64(sourceTimelyBalancePct.Uint64())/100.0,
))
targetCorrectBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.TargetCorrectBalance, mul), c.summary.ActiveBalance)
builder.WriteString(fmt.Sprintf("\n Target correct: %s/%s (%0.2f%%)",
new(big.Int).Div(c.summary.TargetCorrectBalance, gweiToEth).String(),
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
float64(targetCorrectBalancePct.Uint64())/100.0,
))
targetTimelyBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.TargetTimelyBalance, mul), c.summary.ActiveBalance)
builder.WriteString(fmt.Sprintf("\n Target timely: %s/%s (%0.2f%%)",
new(big.Int).Div(c.summary.TargetTimelyBalance, gweiToEth).String(),
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
float64(targetTimelyBalancePct.Uint64())/100.0,
))
headCorrectBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.HeadCorrectBalance, mul), c.summary.ActiveBalance)
builder.WriteString(fmt.Sprintf("\n Head correct: %s/%s (%0.2f%%)",
new(big.Int).Div(c.summary.HeadCorrectBalance, gweiToEth).String(),
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
float64(headCorrectBalancePct.Uint64())/100.0,
))
headTimelyBalancePct := new(big.Int).Div(new(big.Int).Mul(c.summary.HeadTimelyBalance, mul), c.summary.ActiveBalance)
builder.WriteString(fmt.Sprintf("\n Head timely: %s/%s (%0.2f%%)",
new(big.Int).Div(c.summary.HeadTimelyBalance, gweiToEth).String(),
new(big.Int).Div(c.summary.ActiveBalance, gweiToEth).String(),
float64(headTimelyBalancePct.Uint64())/100.0,
))
builder.WriteString(fmt.Sprintf("\n Attesting validators: %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 {
@@ -98,7 +167,7 @@ func (c *command) outputTxt(_ context.Context) (string, error) {
if c.verbose {
for _, syncCommittee := range c.summary.SyncCommittee {
builder.WriteString("\n Validator ")
builder.WriteString(fmt.Sprintf("%d", syncCommittee.Index))
builder.WriteString(fmt.Sprintf("%d", syncCommittee.ValidatorIndex))
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)))

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