Compare commits

...

233 Commits

Author SHA1 Message Date
Jim McDonald
16a94d726a Withdrawal credentials operations in advance.
Allow withdrawal credentials change operations to be generated prior to
the Capella hard fork, allowing queueing if the beacon node supports it.
2022-11-29 14:20:46 +00:00
Jim McDonald
9d00f6bafc Update dependencies. 2022-11-29 12:41:22 +00:00
Jim McDonald
d274ab3db0 Release 1.26.0 2022-11-24 15:15:22 +00:00
Jim McDonald
ad3d8606fd Update workflow. 2022-10-31 17:56:35 +00:00
Jim McDonald
30455e7c43 Merge pull request #47 from wealdtech/credentials-set
Set withdrawal credentials.
2022-10-31 17:41:54 +00:00
Jim McDonald
7eb2c68a19 Update operation and docs. 2022-10-29 12:15:14 +01:00
Jim McDonald
2d96f7cb13 Update operation and docs. 2022-10-29 12:10:31 +01:00
Jim McDonald
fd1e4a97bb Initial work on validator summary. 2022-10-16 23:35:44 +01:00
Jim McDonald
be2270c543 Increase security of shamir number generation. 2022-10-16 22:50:30 +01:00
Jim McDonald
0c36239b8b Update output and associated documentation. 2022-10-06 18:19:38 +00:00
Jim McDonald
f78b2922ec Tidy-ups. 2022-10-06 16:15:45 +00:00
Jim McDonald
bcf6ffdaf0 Linting. 2022-10-06 15:33:01 +00:00
Jim McDonald
9fc184f6a1 Update documentation. 2022-10-06 15:24:15 +00:00
Jim McDonald
c9a30a6e4b Update documentation. 2022-10-06 15:20:08 +00:00
Jim McDonald
1ec6ddc914 Set withdrawal credentials. 2022-10-06 15:14:23 +00:00
Jim McDonald
2c96ef958e Use standard function to obtain best public key. 2022-10-05 13:54:41 +00:00
Jim McDonald
3c10131c45 Use util func to obtain validator from input. 2022-10-02 20:53:10 +01:00
Jim McDonald
fe0bfd4f87 Add more information to "epoch summary". 2022-10-01 22:52:20 +01:00
Jim McDonald
290413f115 Update install instructions. 2022-09-27 19:53:39 +01:00
Jim McDonald
4aa6bef6a3 Update release. 2022-08-28 21:28:54 +01:00
Jim McDonald
1b0f4e2803 Bump version. 2022-08-18 14:42:57 +01:00
Jim McDonald
301224748c Update workflow. 2022-08-18 14:28:23 +01:00
Jim McDonald
1e15b836c2 Bump version. 2022-08-11 08:16:55 +01:00
Jim McDonald
1e709b7592 Remove mandatory connection parameter.
The connection parameter is no longer mandatory, in that ethdo will
attempt to obtain a connection using well-known ports if no override is
supplied.  As such the `--connection` parameter can be omitted and so is
not force-required as part of the command initialisation.
2022-08-11 08:11:55 +01:00
Jim McDonald
8744a85cb7 Merge pull request #45 from tcrossland/master
feat: support block analyze on bellatrix
2022-08-06 08:16:58 +01:00
Tom Crossland
92ad77d8f5 feat: support block analyze on bellatrix 2022-08-04 17:25:20 +02:00
Jim McDonald
2298640e4c Merge pull request #44 from aaron-alderman/fix/deposit-message-root-verification
Add deposit message root match verification
2022-07-16 16:27:24 +01:00
Jim McDonald
5baef59672 Tidy up streaming output. 2022-07-13 11:21:09 +01:00
Aaron Alderman
e54e8affa7 Add deposit message root match verification 2022-07-12 10:47:27 +08:00
Jim McDonald
97fa04a7b2 Bump version. 2022-07-10 18:28:54 +01:00
Jim McDonald
4977ee82e5 Add dpeosit signature verirication to "deposit verify". 2022-07-10 12:33:19 +01:00
Jim McDonald
090680366c Do not print 0-value deposit validator information. 2022-06-23 09:52:45 +01:00
Jim McDonald
531c86847f Tidy up tests. 2022-06-22 07:51:22 +01:00
Jim McDonald
446e437531 Update docs. 2022-06-22 07:51:14 +01:00
Jim McDonald
63d8ccf1a0 Add "proposer duties". 2022-06-22 07:50:53 +01:00
Jim McDonald
77abe0e158 Add sepolia support. 2022-06-21 10:05:29 +01:00
Jim McDonald
547f8d9e71 Fix potential crash when new validator is activated. 2022-06-20 16:18:28 +01:00
Jim McDonald
e144217f25 Add "validator yield". 2022-06-12 11:17:59 +01:00
Jim McDonald
d919810ce1 Tidy up eth1 votes output. 2022-06-10 18:13:49 +01:00
Jim McDonald
0bdf68edf6 Do not fetch future states. 2022-06-01 12:42:40 +01:00
Jim McDonald
b24341b7da Do not fetch future states. 2022-06-01 12:42:12 +01:00
Jim McDonald
384ee3dcaa Bump version. 2022-06-01 12:22:23 +01:00
Jim McDonald
3e8b1a6dad Add "chain eth1votes". 2022-06-01 12:21:40 +01:00
Jim McDonald
d2dec4a444 Tidy up formatting. 2022-05-31 13:39:36 +01:00
Jim McDonald
7e171bdb1e Provide more epoch summary information. 2022-05-30 21:52:30 +01:00
Jim McDonald
0cedf79a89 Bump version. 2022-05-30 16:08:58 +01:00
Jim McDonald
65ad1248ce epoch summary no sync committee information pre-Altair. 2022-05-30 16:08:31 +01:00
Jim McDonald
e1180f97ce error on attester inclusion without required params. 2022-05-26 22:29:21 +01:00
Jim McDonald
394b4a7cd2 Bump version. 2022-05-19 13:00:38 +01:00
Jim McDonald
fd574aae34 Tidy up tests. 2022-05-19 13:00:05 +01:00
Jim McDonald
7fe503f51d Add ropsten support. 2022-05-19 12:59:48 +01:00
Jim McDonald
6bfb0ef098 Add validator credentials get command. 2022-05-10 15:02:41 +01:00
Jim McDonald
46c667d387 Add "chain queues". 2022-04-23 08:51:30 +01:00
Jim McDonald
50f4a9cace Update workflow. 2022-04-15 08:09:06 +01:00
Jim McDonald
cd20875744 Provide clearer error if attempting to import to an HD wallet. 2022-03-24 17:28:46 +00:00
Jim McDonald
84f682a0da Guess connection if none supplied. 2022-03-24 09:46:11 +00:00
Jim McDonald
6389b7dfbd Test coverage. 2022-03-23 22:14:34 +00:00
Jim McDonald
0ef65b8bda Allow account import from keystores. 2022-03-23 22:08:10 +00:00
Jim McDonald
4426c3279d Allow account import from keystores. 2022-03-23 22:06:48 +00:00
Jim McDonald
883f9f834e Update changelog. 2022-03-17 20:21:30 +00:00
Jim McDonald
e0fd3df9dd Add "epoch summary" command. 2022-03-17 20:20:52 +00:00
Jim McDonald
e6d3c67e39 Bump version. 2022-03-16 11:19:52 +00:00
Jim McDonald
6d0a0225c2 Tidy up graffiti and execution output for block info. 2022-03-16 08:52:28 +00:00
Jim McDonald
d60d8beb0b Bump version. 2022-03-11 14:16:49 +00:00
Jim McDonald
a657b3bc24 Do not show execution payload if empty. 2022-03-11 14:16:09 +00:00
Jim McDonald
28b90414d2 Bump version. 2022-03-11 13:39:35 +00:00
Jim McDonald
879a20a7af Support bellatrix. 2022-03-11 13:38:22 +00:00
Jim McDonald
3d49e091e5 Add block analyze. 2022-03-06 22:47:37 +00:00
Jim McDonald
3b51c67e7d Add ssz option to block info. 2022-02-16 15:06:17 +00:00
Jim McDonald
1d559c167b Update dependencies. 2022-01-31 08:10:44 +00:00
Jim McDonald
824c53f6f2 Do not write out text-based error messages whilst streaming with json output. 2022-01-24 16:59:01 +00:00
Jim McDonald
50ffdcd97c Allow custom epochs for attester duties. 2022-01-23 22:38:15 +00:00
Jim McDonald
0e79334863 Tidy up synccommittee inclusion output. 2022-01-17 22:06:08 +00:00
Jim McDonald
c6c3143dd5 Bump version. 2022-01-12 15:10:00 +00:00
Jim McDonald
4d718f614c Tidy up tests, standardise error messages. 2022-01-12 15:07:07 +00:00
Jim McDonald
9a1db9b0a4 Add synccommittee inclusion. 2022-01-12 14:05:35 +00:00
Jim McDonald
7ede620ce7 Add test for new output. 2022-01-10 15:09:46 +00:00
Jim McDonald
a41cc77c18 Add vote success info to attester inclusion. 2022-01-10 15:04:37 +00:00
Jim McDonald
ad83006069 Refactor. 2022-01-10 13:43:11 +00:00
Jim McDonald
976d758cac Add sync committee information. 2022-01-10 13:41:14 +00:00
Jim McDonald
1be72d9ea8 Reduce process time when committees not available. 2022-01-10 13:22:15 +00:00
Jim McDonald
175c33a494 Update docker file. 2021-12-06 22:33:24 +00:00
Jim McDonald
b712f70667 Fix incorrect use of validator index. 2021-12-01 11:39:00 +00:00
Jim McDonald
d4ef9d43b5 Remove rightmost null bytes from graffiti. 2021-11-14 10:40:13 +00:00
Jim McDonald
58de55b40f Bump version. 2021-11-03 13:32:09 +00:00
Jim McDonald
22dad263db Use faster method to obtain sync committees for future epochs. 2021-11-03 13:31:46 +00:00
Jim McDonald
81fa11ad45 Provide sync committee slots in chain status. 2021-11-03 13:29:26 +00:00
Jim McDonald
f5c4551c0c Clarify use of --connection. 2021-11-02 11:39:00 +00:00
Jim McDonald
a00d09e28f Do not report insecure local connection. 2021-11-01 09:46:53 +00:00
Jim McDonald
6bfd5677e6 Bump version. 2021-10-31 09:09:40 +00:00
Jim McDonald
dbe0a9d9f1 Add --validators option to validator expectation. 2021-10-31 09:08:09 +00:00
Jim McDonald
fa390ecdf7 Add "validator expectation". 2021-10-30 22:03:47 +01:00
Jim McDonald
f70abb2165 Add --period to "synccommittee members". 2021-10-30 20:54:11 +01:00
Jim McDonald
ac18cbab3e Bump version. 2021-10-27 14:30:02 +01:00
Jim McDonald
2f1c89d0a6 Update for test. 2021-10-26 18:02:51 +01:00
Jim McDonald
a3ad4181d3 Add exit/withdrawable epoch to validator info. 2021-10-26 17:29:38 +01:00
Jim McDonald
f8ac23e8d7 Merge pull request #39 from Blockdaemon/prater_launchpad
Output "prater" as network name in launchpad format
2021-10-01 15:13:06 +01:00
Jonas Pfannschmidt
b6815d1a2a Output "prater" as network name in launchpad format
Before it would output "unknown" in "eth2_network_name" when creating
a launchpad file using prater's fork version: 0x00001020
2021-10-01 12:19:03 +01:00
Jim McDonald
a79b813bd0 Add "chain verify signedcontributionandproof". 2021-09-28 20:34:01 +01:00
Jim McDonald
ad971145f0 Show both block and body root in "block info". 2021-09-25 17:21:33 +01:00
Jim McDonald
602948921c Generate SHA256 of compressed file. 2021-09-24 12:09:07 +01:00
Jim McDonald
607e969a30 Rework chain status output. 2021-09-24 08:52:09 +01:00
Jim McDonald
79f1ae9930 Update dependencies. 2021-09-21 14:23:17 +01:00
Jim McDonald
a98f681f98 Fix bad dependency. 2021-09-21 13:58:37 +01:00
Jim McDonald
e0e1f697d3 Bump version. 2021-09-21 13:47:33 +01:00
Jim McDonald
1b70a66120 Update workflow. 2021-09-21 13:44:02 +01:00
Jim McDonald
94eba96a6e Tidy-ups. 2021-09-15 22:50:49 +01:00
Jim McDonald
f052d8e307 Update dependencies. 2021-09-15 08:54:07 +01:00
Jim McDonald
df45686828 Update dependencies. 2021-09-15 08:51:28 +01:00
Jim McDonald
84d228877a Documentation updates. 2021-08-28 20:18:29 +01:00
Jim McDonald
b2b26742b0 Fix documentation. 2021-08-28 20:04:52 +01:00
Jim McDonald
9dc630c809 Add synccommittee members. 2021-08-21 00:08:50 +01:00
Jim McDonald
452430db56 Linting. 2021-08-19 13:52:06 +01:00
Jim McDonald
b0d676a734 Update version. 2021-08-19 13:50:29 +01:00
Jim McDonald
ff73470085 Merge pull request #38 from wealdtech/altair
Altair block info
2021-08-19 13:48:18 +01:00
Jim McDonald
a41349999f Add block info for Altair. 2021-08-19 13:44:06 +01:00
Jim McDonald
004f4bc41a Update for block info. 2021-08-19 13:42:22 +01:00
Jim McDonald
64c8e1a051 Updates for Altair. 2021-08-19 13:41:42 +01:00
Jim McDonald
d95d48f6b2 Add more data to "chain info". 2021-08-19 13:18:43 +01:00
Jim McDonald
3e702f0c51 Bump go build version. 2021-08-03 23:50:58 +01:00
Jim McDonald
2e36fcc3ce Use local shamir codebase. 2021-08-03 23:34:16 +01:00
Jim McDonald
aa0cda306b Update dependencies. 2021-08-03 22:58:35 +01:00
Jim McDonald
aa79f83f35 Update changelog 2021-08-03 14:05:15 +01:00
Jim McDonald
8de7e75c77 Merge pull request #36 from wealdtech/sss-export
Shared wallet export/import
2021-08-03 14:03:18 +01:00
Jim McDonald
4a1b419c0e Update documentation. 2021-08-03 13:49:28 +01:00
Jim McDonald
b6a08d5073 Tidy-ups. 2021-08-03 13:49:28 +01:00
Jim McDonald
65d2ab5d53 Tidy-ups. 2021-08-03 13:49:27 +01:00
Jim McDonald
34b03f9d53 Handle timezone in chain time. 2021-08-03 13:49:27 +01:00
Jim McDonald
dca513b8c9 Handle timezone in chain time. 2021-07-30 08:31:43 +01:00
Jim McDonald
446941be92 Add SSS import/export. 2021-07-02 22:48:30 +01:00
Jim McDonald
b76cdb01d1 Update version. 2021-05-13 12:42:14 +01:00
Jim McDonald
ce5b250ef0 Report on missing interfaces.
This update handles the situation where an ETH2 client does not provide
all required interfaces for the 'chain status' command, returning an
error rather than simply panicing.

Fixes #35.
2021-05-13 12:39:14 +01:00
Jim McDonald
2c4ccf62af Avoid crash with latest version of herumi/go-bls. 2021-05-13 12:37:46 +01:00
Jim McDonald
c7ad5194e6 Bump version number. 2021-03-14 22:03:22 +00:00
Jim McDonald
ddb866131b Merge pull request #32 from wealdtech/eth1-withdrawal-credentials
Allow use of Ethereum 1 withdrawal credentials
2021-03-14 21:47:21 +00:00
Jim McDonald
49fb03aa3a Allow use of Ethereum 1 withdrawal credentials.
Release 1.0.1 of the Ethereum 2 specification allows withdrawal
credentials to be Ethereum 1 addresses.  This enables the use of such
addresses when generating and verifying deposit data.
2021-03-12 12:53:42 +00:00
Jim McDonald
1ed3a51117 ETH1 withdrawal credentials. 2021-02-26 15:19:37 +00:00
Jim McDonald
4d5660ccbb Fix crash in attester/duties and inclusion.
A recent change for a return value going from an array to a map caused a
bad indexing in to the returned data.  This ensures that the value is
read directly from the map rather than using a hard-coded offset.
2021-02-13 22:25:26 +00:00
Jim McDonald
7596d271ad Linting. 2021-02-10 10:24:49 +00:00
Jim McDonald
943f9350f3 Add 'chain time' and 'validator keycheck' commands. 2021-02-10 10:13:24 +00:00
Jim McDonald
07863846e6 Use double quotes for Windows compatability. 2021-02-04 21:54:43 +00:00
Jim McDonald
cc59ab618d Tidy up tests. 2021-02-02 20:57:01 +00:00
Jim McDonald
9794949e8a Tidy up separation of input and process. 2021-02-02 20:50:45 +00:00
Jim McDonald
5c741d2b27 Update dependencies. 2021-02-01 19:13:18 +00:00
Jim McDonald
52c76deb5e Add attester duties and slot time commands. 2021-01-24 13:46:35 +00:00
Jim McDonald
c986118f16 Fix typo 2021-01-24 12:25:47 +00:00
Jim McDonald
df6694e3b7 Update tests. 2021-01-02 15:21:56 +00:00
Jim McDonald
a55ad238e6 Add 'node events' command. 2021-01-02 15:20:42 +00:00
Jim McDonald
be21db030e Update chain info fork version type 2021-01-02 10:15:07 +00:00
Jim McDonald
16488c8a40 Add activation epoch to validator info. 2021-01-02 10:13:29 +00:00
Jim McDonald
a7489aa675 Reformat changelog 2020-12-10 07:14:04 +00:00
Jim McDonald
b1647d2f3d Update version. 2020-12-10 00:10:46 +00:00
Jim McDonald
c7f3275dfa Add validtor duties; update validator exit 2020-12-09 20:38:21 +00:00
Jim McDonald
7aeba43338 Add validtor duties; update validator exit 2020-12-09 20:33:30 +00:00
Jim McDonald
688db9ef8c Update dependencies. 2020-12-07 15:21:37 +00:00
Jim McDonald
173883da3e Move core functions to util 2020-12-07 15:18:23 +00:00
Jim McDonald
6077e04619 Move core functions to util 2020-12-07 14:52:08 +00:00
Jim McDonald
95c57363a2 Release 1.7.2 2020-11-23 09:36:28 +00:00
Jim McDonald
9b96b4e34f Update workflow 2020-11-23 09:34:48 +00:00
Jim McDonald
40ba1987cd Add ability to show withdrawal credentials 2020-11-23 08:57:59 +00:00
Jim McDonald
9c08c0a1a4 Add "account derive" command 2020-11-20 19:59:49 +00:00
Jim McDonald
b2360fa2f6 Do not fail verification on missing fork version 2020-11-20 12:21:06 +00:00
Jim McDonald
e7cc6ce18b Merge pull request #26 from OisinKyne/master
Update import/export usage examples to 1.7.0
2020-11-20 00:11:56 +00:00
Jim McDonald
a83e206c89 Additional checks and chattiness for deposit verification 2020-11-19 23:32:27 +00:00
Jim McDonald
947dbdaef6 Bump version 2020-11-19 10:33:56 +00:00
Oisín Kyne
ac87f51047 Update usage examples to 1.7.0 2020-11-18 12:57:20 +00:00
Jim McDonald
b30db1b6c7 Fix missing viper variable 2020-11-18 09:33:09 +00:00
Jim McDonald
405b2d66de Bump version 2020-11-18 09:02:18 +00:00
Jim McDonald
757a5e1492 Catch missing configuration value. 2020-11-17 21:43:01 +00:00
Jim McDonald
0b7a24df6e Linting 2020-11-17 15:53:42 +00:00
Jim McDonald
e042be75ce Tidy-ups and additional test coverage 2020-11-17 15:27:46 +00:00
Jim McDonald
eaf7e34baf Move to V3 of deposit data output to allow additional checks. 2020-11-16 14:43:44 +00:00
Jim McDonald
3b086dd588 Module updates. 2020-11-12 11:31:06 +00:00
Jim McDonald
cbd8cbbf38 Update flags 2020-11-12 11:26:53 +00:00
Jim McDonald
7391dbe6fb Move to eth2client 2020-11-10 23:49:10 +00:00
Jim McDonald
3dd1bab526 Move to eth2client 2020-11-10 23:48:44 +00:00
Jim McDonald
93e632972a Move to eth2client 2020-11-10 23:47:21 +00:00
Jim McDonald
5a385c3c23 Update dependencies. 2020-11-08 20:24:18 +00:00
Jim McDonald
d701cd032a Update changelog. 2020-11-08 20:18:25 +00:00
Jim McDonald
224059ba8e Tidy-ups. 2020-11-08 20:14:51 +00:00
Jim McDonald
1a5234e39f Tidy-ups 2020-11-08 20:04:41 +00:00
Jim McDonald
3cbc27f53d Additional modular commands. 2020-11-08 19:44:30 +00:00
Jim McDonald
a80a1707cf Use standard signing container. 2020-11-07 08:33:21 +00:00
Jim McDonald
290ceb3f0d Initial cut of modular command structure 2020-11-07 08:33:20 +00:00
Jim McDonald
136e2fe9ba Update changelog 2020-11-04 07:27:42 +00:00
Jim McDonald
4b6ea09555 Merge branch 'passphrase-strength' 2020-11-04 07:24:17 +00:00
Jim McDonald
508e2eafcb Refuse weak passphrases without explicit flag. 2020-10-30 12:30:57 +00:00
Jim McDonald
6fc581edc7 Merge pull request #25 from superphiz/patch-3
hd wallet creation requires a walletpassphrase
2020-10-16 22:57:25 +01:00
superphiz
2f1f2e5da0 hd wallet creation requires a walletpassphrase
Doing a walk through, discovered that this example fails without a walletpassphrase parameter.
2020-10-16 16:04:46 -05:00
Jim McDonald
4600f2a0d4 Attester inclusion defaults to last complete epoch. 2020-10-14 22:03:23 +01:00
Jim McDonald
58bc417f52 Add note to docs regarding account option format.
Fixes #24
2020-10-06 22:06:33 +01:00
Jim McDonald
65af8f3cde Fix incorrect name for account passphrase option 2020-10-06 15:54:22 +01:00
Jim McDonald
7e1aa10f60 Fix doc seed -> mnemonic 2020-10-06 15:50:07 +01:00
Jim McDonald
623f3c89ad Add note on fork version 2020-09-30 15:10:45 +01:00
Jim McDonald
628e3113b2 Tidy doc 2020-09-30 15:08:46 +01:00
Jim McDonald
aa27a0c1f4 Added documentation on conversions 2020-09-30 15:07:25 +01:00
Jim McDonald
0a90ae9e97 Ensure launchpad deposit data is always an array. 2020-09-29 08:57:03 +01:00
Jim McDonald
d10b7f2739 Update changelog for v1.6.0 2020-09-25 08:29:05 +01:00
Jim McDonald
5f4be0415f Update BLS version. 2020-09-25 08:27:56 +01:00
Jim McDonald
0f1c6f09bd Bump version 2020-09-21 09:39:28 +01:00
Jim McDonald
6118f9cab8 Release version 1.5.9 2020-09-21 09:34:32 +01:00
Jim McDonald
829dbd3bf2 fix issue where wallet mnemonics were not normalised to NFKD 2020-09-21 09:33:51 +01:00
Jim McDonald
f0ad10463e Tidy up verbose output 2020-09-07 11:03:35 +01:00
Jim McDonald
3d0dab0b95 block info supports fetching the genesis block 2020-09-07 09:37:03 +01:00
Jim McDonald
5abfabc355 Add attester inclusion command 2020-09-02 11:17:38 +01:00
Jim McDonald
e84b600d5d Add participants to account info; passphrase for account creation is optional for Dirk 2020-08-30 09:38:21 +01:00
Jim McDonald
e64a46f126 Update HOWTO for launchpad 2020-08-26 22:14:59 +01:00
Jim McDonald
0746fa3048 Release 1.5.8 2020-08-25 09:20:45 +01:00
Jim McDonald
94eb3fbca7 Add genesis validators root to chain info 2020-08-23 13:44:17 +01:00
Jim McDonald
48d63398d4 Bump version 2020-08-08 14:25:35 +01:00
Jim McDonald
4e1f47e187 Add ability to create HD accounts with a specific path 2020-08-08 14:25:02 +01:00
Jim McDonald
85c7c7fc55 Bump version 2020-08-02 21:54:43 +01:00
Jim McDonald
05406e8d81 Allow fetching wallet from account variable as well as wallet 2020-08-02 17:02:45 +01:00
Jim McDonald
70d451cea5 Bump version 2020-08-01 16:05:02 +01:00
Jim McDonald
c270d7a2f7 Fix bad path spec for validator deposits 2020-08-01 16:04:38 +01:00
Jim McDonald
115d037948 Add 'exit verify' command 2020-07-31 16:31:06 +01:00
Jim McDonald
b34a633e53 Bump version 2020-07-30 12:33:10 +01:00
Jim McDonald
d1b989a711 Add ability to see the private key for a dynamic account. 2020-07-30 12:32:00 +01:00
Jim McDonald
8078359eab Show account info for dynamically generated HD accounts 2020-07-28 20:16:41 +01:00
Jim McDonald
7de8dc7424 Show account info for dynamically generated HD accounts 2020-07-28 20:14:43 +01:00
Jim McDonald
987dbd26c6 Provide deposit info prior to chain-based info 2020-07-24 13:56:53 +01:00
Jim McDonald
1f0aeac9b4 Bump version 2020-07-24 13:34:33 +01:00
Jim McDonald
37cab43242 Add deposit data to validator info. 2020-07-24 13:30:52 +01:00
Jim McDonald
6f94e599ac Update dependencies. 2020-07-24 12:40:19 +01:00
Jim McDonald
86bc3d5418 Allow creation of HD wallets with seed extension. 2020-07-24 12:25:06 +01:00
Jim McDonald
8cc45f0b4b Sort accounts by path (if available) or name (if not). 2020-07-24 12:24:16 +01:00
Jim McDonald
77df1bba43 Update documentation 2020-07-22 09:04:59 +01:00
Jim McDonald
1d4e52cb49 Update troubleshooting 2020-07-22 08:48:17 +01:00
Jim McDonald
c1e124fbd3 Defer relocking appropriately. 2020-07-21 19:00:24 +01:00
Jim McDonald
91bc6ec86d Update cross-compile 2020-07-21 18:38:06 +01:00
Jim McDonald
cd55abc9bf Fix release version 2020-07-21 18:16:53 +01:00
Jim McDonald
44478e147e Add tag-based version 2020-07-21 18:04:36 +01:00
Jim McDonald
61b73b0f61 Rename workflow 2020-07-21 17:52:29 +01:00
342 changed files with 32119 additions and 2804 deletions

22
.github/workflows/golangci-lint.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: golangci-lint
on:
push:
branches:
- master
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.17
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout 5m

View File

@@ -1,10 +1,10 @@
name: Go
name: Release
on:
push:
# branches: [ master ]
tags:
- 'v*'
- 't*'
jobs:
build:
@@ -13,10 +13,9 @@ jobs:
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ^1.13
id: go
go-version: 1.17
- name: Check out code into the Go module directory
uses: actions/checkout@v2
@@ -29,47 +28,55 @@ jobs:
dep ensure
fi
- 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: Build
run: go build -v .
run: go build -v -ldflags="-X github.com/wealdtech/ethdo/cmd.ReleaseVersion=${RELEASE_VERSION}" .
- name: Test
run: go test -v .
- name: Set env
run: |
echo '::set-env name=GO111MODULE::on'
# Release tag comes from the github reference.
RELEASE_TAG=$(echo ${GITHUB_REF} | sed -e 's!.*/!!')
echo "::set-env name=RELEASE_TAG::${RELEASE_TAG}"
echo "::set-output name=RELEASE_TAG::${RELEASE_TAG}"
# Ensure the release tag has expected format.
echo ${RELEASE_TAG} | grep -q '^v' || exit 1
# Release version is same as release tag without leading 'v'.
RELEASE_VERSION=$(echo ${GITHUB_REF} | sed -e 's!.*/v!!')
echo "::set-env name=RELEASE_VERSION::${RELEASE_VERSION}"
echo "::set-output name=RELEASE_VERSION::${RELEASE_VERSION}"
- name: Fetch xgo
run: |
go get github.com/suburbandad/xgo
go install github.com/wealdtech/xgo@latest
- name: Cross-compile
run: xgo -v -x --targets="linux/amd64,linux/arm64,windows/amd64" github.com/wealdtech/ethdo
- 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: Create windows zip file
- 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: 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: 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
- name: Create linux ARM64 tgz file
run: |
mv ethdo-linux-arm64 ethdo
tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
sha256sum ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz >ethdo-${RELEASE_VERSION}-linux-arm64.sha256
- name: Create release
id: create_release
@@ -80,8 +87,19 @@ jobs:
tag_name: ${{ github.ref }}
release_name: Release ${{ env.RELEASE_VERSION }}
draft: true
prerelease: true
prerelease: false
- 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: Upload windows zip file
id: upload-release-asset-windows
uses: actions/upload-release-asset@v1
@@ -92,7 +110,18 @@ jobs:
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-windows-exe.zip
asset_name: ethdo-${{ env.RELEASE_VERSION }}-windows-exe.zip
asset_content_type: application/zip
- name: Upload linux AMD64 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 linux AMD64 tgz file
id: upload-release-asset-linux-amd64
uses: actions/upload-release-asset@v1
@@ -103,7 +132,18 @@ jobs:
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-amd64.tar.gz
asset_content_type: application/gzip
- name: Upload linux ARM64 checksum file
id: upload-release-asset-linux-arm64-checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
asset_name: ethdo-${{ env.RELEASE_VERSION }}-linux-arm64.sha256
asset_content_type: text/plain
- name: Upload linux ARM64 tgz file
id: upload-release-asset-linux-arm64
uses: actions/upload-release-asset@v1

3
.gitignore vendored
View File

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

158
CHANGELOG.md Normal file
View File

@@ -0,0 +1,158 @@
1.26.1
- add ability to generate validator credentials change operations prior to the fork in which they become usable
1.26.0
- add commands and documentation to set user validator credentials (not usable until capella)
1.25.3
- add more information to "epoch summary"
- add "validator summary"
1.25.2:
- no longer require connection parameter
- support "block analyze" on bellatrix (thanks @tcrossland)
- check deposit message root match for verifying deposits (thanks @aaron-alderman)
1.25.0:
- add "proposer duties"
- add deposit signature verification to "deposit verify"
1.24.1:
- fix potential crash when new validators are activated
- add "sepolia" to the list of supported networks
1.24.0:
- add "validator yield"
1.23.1:
- do not fetch future state for chain eth1votes
1.23.0:
- do not fetch sync committee information for epoch summaries prior to Altair
- ensure that "attester inclusion" without validator returns appropriate error
- provide more information in "epoch summary" with verbose flag
- add "chain eth1votes"
1.22.0:
- add "ropsten" to the list of supported networks
1.21.0:
- add "validator credentials get"
1.20.0:
- add "chain queues"
1.19.1:
- add the ability to import keystores to ethdo wallets
- use defaults to connect to beacon nodes if no explicit connection defined
1.19.0:
- add "epoch summary"
1.18.2:
- tidy up output of "block info"
1.18.1:
- do not show execution payload if empty
1.18.0:
- add "-ssz" option to "block info"
- add "block analyze" command
- support bellatrix
1.17.0:
- add sync committee information to "chain time"
- add details of vote success to "attester inclusion --verbose"
- add "synccommittee inclusion"
1.15.1:
- provide sync committee slots in "chain status"
- clarify that --connection should be a URL
1.15.0:
- add --period to "synccommittee members", can be "current", "next"
- add "validator expectation"
1.14.0:
- add "chain verify signedcontributionandproof"
- show both block and body root in "block info"
- add exit / withdrawable epoch to "validator info"
1.13.0:
- rework and provide additional information to "chain status" output
1.12.0:
- add "synccommittee members"
1.11.0
- add Altair information to "block info"
- add more information to "chain info"
1.10.2
- use local shamir code (copied from github.com/hashicorp/vault)
1.10.0
- add "wallet sharedexport" and "wallet sharedimport"
1.9.1
- Avoid crash when required interfaces for chain status command are not supported
- Avoid crash with latest version of herumi/go-bls
1.9.0
- allow use of Ethereum 1 address as withdrawal credentials
1.8.1
- fix issue where 'attester duties' and 'attester inclusion' could crash
1.8.0
- add "chain time"
- add "validator keycheck"
1.7.5:
- add "slot time"
- add "attester duties"
- add "node events"
- add activation epoch to "validator info"
1.7.3:
- fix issue where base directory was ignored for wallet creation
- new "validator duties" command to display known duties for a given validator
- update go-eth2-client to display correct validator status from prysm
1.7.2:
- new "account derive" command to derive keys directly from a mnemonic and derivation path
- add more output to "deposit verify" to explain operation
1.7.1:
- fix "store not set" issue
1.7.0:
- "validator depositdata" now defaults to mainnet, does not silently fetch fork version from chain
- update deposit data output to version 3, to allow for better deposit checking
- use go-eth2-client for beacon node communications
- deprecated "--basedir" in favor of "--base-dir"
- deprecated "--storepassphrase" in favor of "--store-passphrase"
- deprecated "--walletpassphrsae" in favor of "--wallet-passphrsae"
- renamed "--exportpassphrase" and "--importpassphrase" flags to "--passphrase"
- reworked internal structure of account-related commands
- reject weak passphrases by default
1.6.1:
- "attester inclusion" defaults to previous epoch
- output array for launchpad deposit data JSON in all situations
1.6.0:
- update BLS HKDF function to match spec 04
- add --launchpad option to "validator depositdata" to output data in launchpad format
1.5.9:
- fix issue where wallet mnemonics were not normalised to NFKD
- "block info" supports fetching the gensis block (--slot=0)
- "attester inclusion" command finds the inclusion slot for a validator's attestation
- "account info" with verbose option now displays participants for distributed accounts
- fix issue where distributed account generation without a passphrase was not allowed
1.5.8:
- allow raw deposit transactions to be supplied to "deposit verify"
- move functionality of "account withdrawalcredentials" to be part of "account info"
- add genesis validators root to "chain info"

View File

@@ -1,4 +1,4 @@
FROM golang:1.14-buster as builder
FROM golang:1.17-bullseye as builder
WORKDIR /app
@@ -10,10 +10,12 @@ COPY . .
RUN go build
FROM debian:buster-slim
FROM debian:bullseye-slim
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt install -y ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/ethdo /app
ENTRYPOINT ["/app/ethdo"]
ENTRYPOINT ["/app/ethdo"]

View File

@@ -5,12 +5,13 @@
A command-line tool for managing common tasks in Ethereum 2.
**Please note that this tool and its underlying libraries have not yet undergone a security audit; use at your own risk.**
## Table of Contents
- [Install](#install)
- [Binaries](#binaries)
- [Docker](#docker)
- [Source](#source)
- [Setting up](#setting-up)
- [Usage](#usage)
- [Maintainers](#maintainers)
- [Contribute](#contribute)
@@ -18,15 +19,9 @@ A command-line tool for managing common tasks in Ethereum 2.
## Install
`ethdo` is a standard Go program which can be installed with:
### Binaries
```sh
GO111MODULE=on go get github.com/wealdtech/ethdo
```
Note that `ethdo` requires at least version 1.13 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.
Binaries for the latest version of `ethdo` can be obtained from [the releases page](https://github.com/wealdtech/ethdo/releases).
### Docker
@@ -36,7 +31,18 @@ You can obtain the latest version of `ethdo` using docker with:
docker pull wealdtech/ethdo
```
Or build `ethdo` using docker:
### Source
`ethdo` is a standard Go program which can be installed with:
```sh
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`.
If this does not work please see the [troubleshooting](https://github.com/wealdtech/ethdo/blob/master/docs/troubleshooting.md) page.
The docker image can be build locally with:
```sh
docker build -t ethdo .
@@ -56,13 +62,37 @@ docker run --network=host ethdo chain status
Alternatively, if the beacon node is running in a separate docker container a shared network can be created with `docker network create eth2` and accessed by adding `--network=eth2` added to both the beacon node and `ethdo` containers.
## Setting up
`ethdo` needs a connection to a beacon node for many of its features. `ethdo` can connect to any beacon node that fully supports the [standard REST API](https://ethereum.github.io/beacon-APIs/). The following changes are required to beacon nodes to make this available.
### Lighthouse
Lighthouse disables the REST API by default. To enable it, the beacon node must be started with the `--http` parameter. If you want to access the REST API from a remote server then you should also look to change the `--http-address` and `--http-allow-origin` options as per the Lighthouse documentation.
The default port for the REST API is 5052, which can be changed with the `--http-port` parameter.
### Nimbus
Nimbus disables the REST API by default. To enable it, the beacon node must be started with the `--rest` parameter. If you want to access the REST API from a remote server then you should also look to change the `--rest-address` and `--rest-allow-origin` options as per the Nimbus documentation.
The default port for the REST API is 5052, which can be changed with the `--rest-port` parameter.
### Prysm
Prysm enables the REST API by default. You will need to add the parameter `--grpc-max-msg-size 268435456` to be obtain to obtain large sets of information such as the list of current validators. If you want to access the REST API from a remote server then you should also look to change the `--grpc-gateway-host` and `--grpc-gateway-corsdomain` options as per the Prysm documentation.
The default port for the REST API is 3500, which can be changed with the `--grpc-gateway-port` parameter.
### Teku
Teku disables the REST API by default. To enable it, the beacon node must be started with the `--rest-api-enabled` parameter. If you want to access the REST API from a remote server then you should also look to change the `--rest-api-interface`, `--rest-api-host-allowlist` and `--rest-api-cors-origins` options as per the Teku documentation.
The default port for the REST API is 5051, which can be changed with the `--rest-api-port` parameter.
## Usage
ethdo contains a large number of features that are useful for day-to-day interactions with the Ethereum 2 blockchain.
`ethdo` contains a large number of features that are useful for day-to-day interactions with the Ethereum 2 blockchain.
### Wallets and accounts
ethdo uses the [go-eth2-wallet](https://github.com/wealdtech/go-eth2-wallet) system to provide unified access to different wallet types. When on the filesystem the locations of the created wallets and accounts are:
`ethdo` uses the [go-eth2-wallet](https://github.com/wealdtech/go-eth2-wallet) system to provide unified access to different wallet types. When on the filesystem the locations of the created wallets and accounts are:
- for Linux: $HOME/.config/ethereum2/wallets
- for OSX: $HOME/Library/Application Support/ethereum2/wallets
@@ -83,7 +113,7 @@ All ethdo comands take the following parameters:
- `store`: the name of the storage system for wallets. This can be one of "filesystem" (for local storage of the wallet) or "s3" (for remote storage of the wallet on [Amazon's S3](https://aws.amazon.com/s3/) storage system), and defaults to "filesystem"
- `storepassphrase`: the passphrase for the store. If this is empty the store is unencrypted
- `walletpassphrase`: the passphrase for the wallet. This is required for some wallet-centric operations such as creating new accounts
- `accountpassphrase`: the passphrase for the account. This is required for some account-centric operations such as signing data
- `passphrase`: the passphrase for the account. This is required for some account-centric operations such as signing data
Accounts are specified in the standard "<wallet>/<account>" format, for example the account "savings" in the wallet "primary" would be referenced as "primary/savings".
@@ -118,6 +148,12 @@ 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.
## 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.
If a weak passphrase is required, `ethdo` can be supplied with the `--allow-weak-passphrases` option which will force it to accept any passphrase, even if it is considered weak.
## Rules for account passphrases
Account passphrases are used in various places in `ethdo`. Where they are used, the following rules apply:
@@ -136,12 +172,14 @@ Command information, along with sample outputs and optional arguments, is availa
# HOWTO
There is a [HOWTO](https://github.com/wealdtech/ethdo/blob/master/docs/howto.md) that covers details about how to carry out various common tasks.
There is a [HOWTO](https://github.com/wealdtech/ethdo/blob/master/docs/howto.md) that covers details about how to carry out various common tasks. There is also a specific document that provides details of how to carry out [common conversions](docs/conversions.md) from mnemonic, to account, to deposit data, for launchpad-related configurations.
## Maintainers
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.
## Contribute
Contributions welcome. Please check out [the issues](https://github.com/wealdtech/ethdo/issues).

View File

@@ -0,0 +1,96 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountcreate
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
timeout time.Duration
// For all accounts.
wallet e2wtypes.Wallet
accountName string
passphrase string
walletPassphrase string
// For distributed accounts.
participants uint32
signingThreshold uint32
// For pathed accounts.
path string
}
func input(ctx context.Context) (*dataIn, error) {
var err error
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
// Account name.
if viper.GetString("account") == "" {
return nil, errors.New("account is required")
}
_, data.accountName, err = e2wallet.WalletAndAccountNames(viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account name")
}
if data.accountName == "" {
return nil, errors.New("account name is required")
}
// Wallet.
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
data.wallet, err = util.WalletFromInput(ctx)
cancel()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain wallet")
}
// Passphrase.
data.passphrase, err = util.GetOptionalPassphrase()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain passphrase")
}
// Wallet passphrase.
data.walletPassphrase = util.GetWalletPassphrase()
// Participants.
if viper.GetInt32("participants") == 0 {
return nil, errors.New("participants must be at least one")
}
data.participants = viper.GetUint32("participants")
// Signing threshold.
if viper.GetInt32("signing-threshold") == 0 {
return nil, errors.New("signing threshold must be at least one")
}
data.signingThreshold = viper.GetUint32("signing-threshold")
// Path.
data.path = viper.GetString("path")
return data, nil
}

View File

@@ -0,0 +1,161 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountcreate
import (
"context"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "timeout is required",
},
{
name: "WalletUnknown",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Unknown/Test account",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "failed to obtain wallet: wallet not found",
},
{
name: "AccountMissing",
vars: map[string]interface{}{
"timeout": "5s",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "account is required",
},
{
name: "AccountWalletOnly",
vars: map[string]interface{}{
"timeout": "5s",
"passphrase": "ce%NohGhah4ye5ra",
"account": "Test wallet/",
},
err: "account name is required",
},
{
name: "AccountMalformed",
vars: map[string]interface{}{
"timeout": "5s",
"account": "//",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "failed to obtain account name: invalid account format",
},
{
name: "MultiplePassphrases",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": []string{"ce%NohGhah4ye5ra", "other"},
"participants": 3,
"signing-threshold": 2,
},
err: "failed to obtain passphrase: multiple passphrases supplied",
},
{
name: "ParticipantsZero",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
"participants": 0,
"signing-threshold": 2,
},
err: "participants must be at least one",
},
{
name: "SigningThresholdZero",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
"participants": 3,
"signing-threshold": 0,
},
err: "signing threshold must be at least one",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
"participants": 3,
"signing-threshold": 2,
},
res: &dataIn{
timeout: 5 * time.Second,
accountName: "Test account",
passphrase: "ce%NohGhah4ye5ra",
participants: 3,
signingThreshold: 2,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
// Cannot compare accounts directly, so need to check each element individually.
require.Equal(t, test.res.timeout, res.timeout)
require.Equal(t, test.res.accountName, res.accountName)
require.Equal(t, test.res.passphrase, res.passphrase)
require.Equal(t, test.res.participants, res.participants)
require.Equal(t, test.res.signingThreshold, res.signingThreshold)
}
})
}
}

View File

@@ -0,0 +1,45 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountcreate
import (
"context"
"fmt"
"github.com/pkg/errors"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataOut struct {
account e2wtypes.Account
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.account == nil {
return "", errors.New("no account")
}
if pubKeyProvider, ok := data.account.(e2wtypes.AccountCompositePublicKeyProvider); ok {
return fmt.Sprintf("%#x", pubKeyProvider.CompositePublicKey().Marshal()), nil
}
if pubKeyProvider, ok := data.account.(e2wtypes.AccountPublicKeyProvider); ok {
return fmt.Sprintf("%#x", pubKeyProvider.PublicKey().Marshal()), nil
}
return "", errors.New("no public key available")
}

View File

@@ -0,0 +1,113 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountcreate
import (
"context"
"encoding/hex"
"strings"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
distributed "github.com/wealdtech/go-eth2-wallet-distributed"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func hexToBytes(input string) []byte {
res, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
if err != nil {
panic(err)
}
return res
}
func TestOutput(t *testing.T) {
testWallet, err := nd.CreateWallet(context.Background(), "Test", scratch.New(), keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
distributedWallet, err := distributed.CreateWallet(context.Background(), "Test distributed", scratch.New(), keystorev4.New())
require.NoError(t, err)
require.NoError(t, distributedWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
distributed0, err := distributedWallet.(e2wtypes.WalletDistributedAccountImporter).ImportDistributedAccount(context.Background(),
"Distributed 0",
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
2,
[][]byte{
hexToBytes("0x876dd4705157eb66dc71bc2e07fb151ea53e1a62a0bb980a7ce72d15f58944a8a3752d754f52f4a60dbfc7b18169f268"),
hexToBytes("0xaec922bd7a9b7b1dc21993133b586b0c3041c1e2e04b513e862227b9d7aecaf9444222f7e78282a449622ffc6278915d"),
},
map[uint64]string{
1: "localhost-1:12345",
2: "localhost-2:12345",
3: "localhost-3:12345",
},
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "AccountNil",
dataOut: &dataOut{},
err: "no account",
},
{
name: "Account",
dataOut: &dataOut{
account: interop0,
},
res: "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
{
name: "DistributedAccount",
dataOut: &dataOut{
account: distributed0,
},
res: "0x876dd4705157eb66dc71bc2e07fb151ea53e1a62a0bb980a7ce72d15f58944a8a3752d754f52f4a60dbfc7b18169f268",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

View File

@@ -0,0 +1,147 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountcreate
import (
"context"
"regexp"
"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) {
if data == nil {
return nil, errors.New("no data")
}
if data.passphrase != "" && !util.AcceptablePassphrase(data.passphrase) {
return nil, errors.New("supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag")
}
locker, isLocker := data.wallet.(e2wtypes.WalletLocker)
if isLocker {
if err := locker.Unlock(ctx, []byte(data.walletPassphrase)); err != nil {
return nil, errors.Wrap(err, "failed to unlock wallet")
}
defer func() {
if err := locker.Lock(ctx); err != nil {
util.Log.Trace().Err(err).Msg("Failed to lock wallet")
}
}()
}
if data.participants == 0 {
return nil, errors.New("participants is required")
}
// Create style of account based on input.
switch {
case data.participants > 1:
return processDistributed(ctx, data)
case data.path != "":
return processPathed(ctx, data)
default:
return processStandard(ctx, data)
}
}
func processStandard(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.passphrase == "" {
return nil, errors.New("passphrase is required")
}
results := &dataOut{}
creator, isCreator := data.wallet.(e2wtypes.WalletAccountCreator)
if !isCreator {
return nil, errors.New("wallet does not support account creation")
}
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
account, err := creator.CreateAccount(ctx, data.accountName, []byte(data.passphrase))
if err != nil {
return nil, errors.Wrap(err, "failed to create account")
}
results.account = account
return results, nil
}
func processPathed(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.passphrase == "" {
return nil, errors.New("passphrase is required")
}
match, err := regexp.Match("^m/[0-9]+/[0-9]+(/[0-9+])+", []byte(data.path))
if err != nil {
return nil, errors.Wrap(err, "unable to match path to regular expression")
}
if !match {
return nil, errors.New("path does not match expected format m/…")
}
results := &dataOut{}
creator, isCreator := data.wallet.(e2wtypes.WalletPathedAccountCreator)
if !isCreator {
return nil, errors.New("wallet does not support account creation with an explicit path")
}
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
account, err := creator.CreatePathedAccount(ctx, data.path, data.accountName, []byte(data.passphrase))
if err != nil {
return nil, errors.Wrap(err, "failed to create account")
}
results.account = account
return results, nil
}
func processDistributed(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.signingThreshold == 0 {
return nil, errors.New("signing threshold required")
}
if data.signingThreshold <= data.participants/2 {
return nil, errors.New("signing threshold must be more than half the number of participants")
}
if data.signingThreshold > data.participants {
return nil, errors.New("signing threshold cannot be higher than the number of participants")
}
results := &dataOut{}
creator, isCreator := data.wallet.(e2wtypes.WalletDistributedAccountCreator)
if !isCreator {
return nil, errors.New("wallet does not support distributed account creation")
}
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
account, err := creator.CreateDistributedAccount(ctx,
data.accountName,
data.participants,
data.signingThreshold,
[]byte(data.passphrase))
if err != nil {
return nil, errors.Wrap(err, "failed to create account")
}
results.account = account
return results, nil
}

50
cmd/account/create/run.go Normal file
View File

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

View File

@@ -0,0 +1,58 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountderive
import (
"context"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
quiet bool
// Derivation information.
mnemonic string
path string
// Output options.
showPrivateKey bool
showWithdrawalCredentials bool
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
// Quiet.
data.quiet = viper.GetBool("quiet")
// Mnemonic.
if viper.GetString("mnemonic") == "" {
return nil, errors.New("mnemonic is required")
}
data.mnemonic = viper.GetString("mnemonic")
// Path.
if viper.GetString("path") == "" {
return nil, errors.New("path is required")
}
data.path = viper.GetString("path")
// Show private key.
data.showPrivateKey = viper.GetBool("show-private-key")
// Show withdrawal credentials.
data.showWithdrawalCredentials = viper.GetBool("show-withdrawal-credentials")
return data, nil
}

View File

@@ -0,0 +1,78 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountderive
import (
"context"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
)
func TestInput(t *testing.T) {
require.NoError(t, e2types.InitBLS())
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "MnemonicMissing",
vars: map[string]interface{}{
"path": "m/12381/3600/0/0",
},
err: "mnemonic is required",
},
{
name: "PathMissing",
vars: map[string]interface{}{
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
},
err: "path is required",
},
{
name: "Good",
vars: map[string]interface{}{
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
"path": "m/12381/3600/0/0",
},
res: &dataIn{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
path: "m/12381/3600/0/0",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
// Cannot compare accounts directly, so need to check each element individually.
require.Equal(t, test.res.mnemonic, res.mnemonic)
require.Equal(t, test.res.path, res.path)
}
})
}
}

View File

@@ -0,0 +1,55 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountderive
import (
"context"
"fmt"
"strings"
"github.com/pkg/errors"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
)
type dataOut struct {
showPrivateKey bool
showWithdrawalCredentials bool
key *e2types.BLSPrivateKey
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.key == nil {
return "", errors.New("no key")
}
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[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
builder.WriteString(fmt.Sprintf("Withdrawal credentials: %#x\n", withdrawalCredentials))
}
if !(data.showPrivateKey || data.showWithdrawalCredentials) {
builder.WriteString(fmt.Sprintf("Public key: %#x\n", data.key.PublicKey().Marshal()))
}
return builder.String(), nil
}

View File

@@ -0,0 +1,101 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountderive
import (
"context"
"encoding/hex"
"strings"
"testing"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
)
func blsPrivateKey(input string) *e2types.BLSPrivateKey {
data, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
if err != nil {
panic(err)
}
key, err := e2types.BLSPrivateKeyFromBytes(data)
if err != nil {
panic(err)
}
return key
}
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
needs []string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "KeyMissing",
dataOut: &dataOut{},
err: "no key",
},
{
name: "Good",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
},
needs: []string{"Public key"},
},
{
name: "PrivatKey",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showPrivateKey: true,
},
needs: []string{"Private key"},
},
{
name: "WithdrawalCredentials",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showWithdrawalCredentials: true,
},
needs: []string{"Withdrawal credentials"},
},
{
name: "All",
dataOut: &dataOut{
key: blsPrivateKey("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
showPrivateKey: true,
showWithdrawalCredentials: true,
},
needs: []string{"Private key", "Withdrawal credentials"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
for _, need := range test.needs {
require.Contains(t, res, need)
}
}
})
}
}

View File

@@ -0,0 +1,47 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountderive
import (
"context"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
account, err := util.ParseAccount(ctx, data.mnemonic, []string{data.path}, true)
if err != nil {
return nil, errors.Wrap(err, "failed to derive account")
}
key, err := account.(e2wtypes.AccountPrivateKeyProvider).PrivateKey(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account private key")
}
results := &dataOut{
showPrivateKey: data.showPrivateKey,
showWithdrawalCredentials: data.showWithdrawalCredentials,
key: key.(*e2types.BLSPrivateKey),
}
return results, nil
}

View File

@@ -0,0 +1,97 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountderive
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
)
func TestProcess(t *testing.T) {
require.NoError(t, e2types.InitBLS())
tests := []struct {
name string
dataIn *dataIn
privKey []byte
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "MnemonicMissing",
dataIn: &dataIn{
path: "m/12381/3600/0/0",
},
err: "failed to derive account: no account specified",
},
{
name: "MnemonicInvalid",
dataIn: &dataIn{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
path: "m/12381/3600/0/0",
},
err: "failed to derive account: mnemonic is invalid",
},
{
name: "PathMissing",
dataIn: &dataIn{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
},
err: "failed to derive account: path does not match expected format m/…",
},
{
name: "PathInvalid",
dataIn: &dataIn{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
path: "n/12381/3600/0/0",
},
err: "failed to derive account: path does not match expected format m/…",
},
{
name: "Good",
dataIn: &dataIn{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
path: "m/12381/3600/0/0",
},
privKey: testutil.HexToBytes("0x068dce0c90cb428ab37a74af0191eac49648035f1aaef077734b91e05985ec55"),
},
{
name: "Extended",
dataIn: &dataIn{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art extended",
path: "m/12381/3600/0/0",
},
privKey: testutil.HexToBytes("0x58c8b280ae035de0452797b52fb62555f27f78541ea2f04b23e7bb0fcd0fc2d6"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.privKey, res.key.Marshal())
}
})
}
}

50
cmd/account/derive/run.go Normal file
View File

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

131
cmd/account/import/input.go Normal file
View File

@@ -0,0 +1,131 @@
// Copyright © 2019 - 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 accountimport
import (
"context"
"encoding/hex"
"io/ioutil"
"strings"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
timeout time.Duration
wallet e2wtypes.Wallet
key []byte
accountName string
passphrase string
walletPassphrase string
keystore []byte
keystorePassphrase []byte
}
func input(ctx context.Context) (*dataIn, error) {
var err error
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
// Account name.
if viper.GetString("account") == "" {
return nil, errors.New("account is required")
}
_, data.accountName, err = e2wallet.WalletAndAccountNames(viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account name")
}
if data.accountName == "" {
return nil, errors.New("account name is required")
}
// Wallet.
ctx, cancel := context.WithTimeout(ctx, data.timeout)
defer cancel()
data.wallet, err = util.WalletFromInput(ctx)
cancel()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain wallet")
}
// Passphrase.
data.passphrase, err = util.GetOptionalPassphrase()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain passphrase")
}
// Wallet passphrase.
data.walletPassphrase = util.GetWalletPassphrase()
if viper.GetString("key") == "" && viper.GetString("keystore") == "" {
return nil, errors.New("key or keystore is required")
}
if viper.GetString("key") != "" && viper.GetString("keystore") != "" {
return nil, errors.New("only one of key and keystore is required")
}
if viper.GetString("key") != "" {
data.key, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x"))
if err != nil {
return nil, errors.Wrap(err, "key is malformed")
}
}
if viper.GetString("keystore") != "" {
data.keystorePassphrase = []byte(viper.GetString("keystore-passphrase"))
if len(data.keystorePassphrase) == 0 {
return nil, errors.New("must supply keystore passphrase with keystore-passphrase when supplying keystore")
}
data.keystore, err = obtainKeystore(viper.GetString("keystore"))
if err != nil {
return nil, errors.Wrap(err, "invalid keystore")
}
}
return data, nil
}
// obtainKeystore obtains keystore from an input, could be JSON itself or a path to JSON.
func obtainKeystore(input string) ([]byte, error) {
var err error
var data []byte
// Input could be JSON or a path to JSON
if strings.HasPrefix(input, "{") {
// Looks like JSON
data = []byte(input)
} else {
// Assume it's a path to JSON
data, err = ioutil.ReadFile(input)
if err != nil {
return nil, errors.Wrap(err, "failed to find deposit data file")
}
}
return data, nil
// exitData := &util.ValidatorExitData{}
// err = json.Unmarshal(data, exitData)
// if err != nil {
// return nil, errors.Wrap(err, "data is not valid JSON")
// }
// return exitData, nil
}

View File

@@ -0,0 +1,172 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountimport
import (
"context"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "timeout is required",
},
{
name: "WalletUnknown",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Unknown/Test account",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "failed to obtain wallet: wallet not found",
},
{
name: "AccountMissing",
vars: map[string]interface{}{
"timeout": "5s",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "account is required",
},
{
name: "AccountWalletOnly",
vars: map[string]interface{}{
"timeout": "5s",
"passphrase": "ce%NohGhah4ye5ra",
"account": "Test wallet/",
},
err: "account name is required",
},
{
name: "AccountMalformed",
vars: map[string]interface{}{
"timeout": "5s",
"account": "//",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "failed to obtain account name: invalid account format",
},
{
name: "MultiplePassphrases",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": []string{"ce%NohGhah4ye5ra", "other"},
},
err: "failed to obtain passphrase: multiple passphrases supplied",
},
{
name: "KeyMissing",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "key or keystore is required",
},
{
name: "KeyMalformed",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
"key": "invalid",
},
err: "key is malformed: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "KeyandKeystore",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
"keystore": "{}",
},
err: "only one of key and keystore is required",
},
{
name: "KeystoreNoKeystorePassphrase",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"keystore": "{}",
},
err: "must supply keystore passphrase with keystore-passphrase when supplying keystore",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Test account",
"passphrase": "ce%NohGhah4ye5ra",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
},
res: &dataIn{
timeout: 5 * time.Second,
accountName: "Test account",
passphrase: "ce%NohGhah4ye5ra",
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
// Cannot compare accounts directly, so need to check each element individually.
require.Equal(t, test.res.timeout, res.timeout)
require.Equal(t, test.res.accountName, res.accountName)
require.Equal(t, test.res.passphrase, res.passphrase)
}
})
}
}

View File

@@ -0,0 +1,45 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountimport
import (
"context"
"fmt"
"github.com/pkg/errors"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataOut struct {
account e2wtypes.Account
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.account == nil {
return "", errors.New("no account")
}
if pubKeyProvider, ok := data.account.(e2wtypes.AccountCompositePublicKeyProvider); ok {
return fmt.Sprintf("%#x", pubKeyProvider.CompositePublicKey().Marshal()), nil
}
if pubKeyProvider, ok := data.account.(e2wtypes.AccountPublicKeyProvider); ok {
return fmt.Sprintf("%#x", pubKeyProvider.PublicKey().Marshal()), nil
}
return "", errors.New("no public key available")
}

View File

@@ -0,0 +1,113 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountimport
import (
"context"
"encoding/hex"
"strings"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
distributed "github.com/wealdtech/go-eth2-wallet-distributed"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func hexToBytes(input string) []byte {
res, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
if err != nil {
panic(err)
}
return res
}
func TestOutput(t *testing.T) {
testWallet, err := nd.CreateWallet(context.Background(), "Test", scratch.New(), keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
distributedWallet, err := distributed.CreateWallet(context.Background(), "Test distributed", scratch.New(), keystorev4.New())
require.NoError(t, err)
require.NoError(t, distributedWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
distributed0, err := distributedWallet.(e2wtypes.WalletDistributedAccountImporter).ImportDistributedAccount(context.Background(),
"Distributed 0",
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
2,
[][]byte{
hexToBytes("0x876dd4705157eb66dc71bc2e07fb151ea53e1a62a0bb980a7ce72d15f58944a8a3752d754f52f4a60dbfc7b18169f268"),
hexToBytes("0xaec922bd7a9b7b1dc21993133b586b0c3041c1e2e04b513e862227b9d7aecaf9444222f7e78282a449622ffc6278915d"),
},
map[uint64]string{
1: "localhost-1:12345",
2: "localhost-2:12345",
3: "localhost-3:12345",
},
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "AccountNil",
dataOut: &dataOut{},
err: "no account",
},
{
name: "Account",
dataOut: &dataOut{
account: interop0,
},
res: "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
{
name: "DistributedAccount",
dataOut: &dataOut{
account: distributed0,
},
res: "0x876dd4705157eb66dc71bc2e07fb151ea53e1a62a0bb980a7ce72d15f58944a8a3752d754f52f4a60dbfc7b18169f268",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

View File

@@ -0,0 +1,110 @@
// Copyright © 2019 -2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 accountimport
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
"github.com/wealdtech/go-ecodec"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.passphrase == "" {
return nil, errors.New("passphrase is required")
}
if !util.AcceptablePassphrase(data.passphrase) {
return nil, errors.New("supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag")
}
locker, isLocker := data.wallet.(e2wtypes.WalletLocker)
if isLocker {
if err := locker.Unlock(ctx, []byte(data.walletPassphrase)); err != nil {
return nil, errors.Wrap(err, "failed to unlock wallet")
}
defer func() {
if err := locker.Lock(ctx); err != nil {
util.Log.Trace().Err(err).Msg("Failed to lock wallet")
}
}()
}
if len(data.key) > 0 {
return processFromKey(ctx, data)
}
if len(data.keystore) > 0 {
return processFromKeystore(ctx, data)
}
return nil, errors.New("unsupported import mechanism")
}
func processFromKey(ctx context.Context, data *dataIn) (*dataOut, error) {
results := &dataOut{}
importer, isImporter := data.wallet.(e2wtypes.WalletAccountImporter)
if !isImporter {
return nil, fmt.Errorf("%s wallets do not support importing accounts", data.wallet.Type())
}
account, err := importer.ImportAccount(ctx, data.accountName, data.key, []byte(data.passphrase))
if err != nil {
return nil, errors.Wrap(err, "failed to import account")
}
results.account = account
return results, nil
}
func processFromKeystore(ctx context.Context, data *dataIn) (*dataOut, error) {
// Need to import the keystore in to a temporary wallet to fetch the private key.
store := scratch.New()
encryptor := keystorev4.New()
// Need to add a couple of fields to the keystore to make it compliant.
keystoreData := fmt.Sprintf(`{"name":"Import","encryptor":"keystore",%s`, string(data.keystore[1:]))
walletData := fmt.Sprintf(`{"wallet":{"name":"ImportTest","type":"non-deterministic","uuid":"e1526407-1dc7-4f3f-9d05-ab696f40707c","version":1},"accounts":[%s]}`, keystoreData)
encryptedData, err := ecodec.Encrypt([]byte(walletData), data.keystorePassphrase)
if err != nil {
return nil, err
}
wallet, err := nd.Import(ctx, encryptedData, data.keystorePassphrase, store, encryptor)
if err != nil {
return nil, errors.Wrap(err, "failed to import wallet")
}
account := <-wallet.Accounts(ctx)
privateKeyProvider, isPrivateKeyProvider := account.(e2wtypes.AccountPrivateKeyProvider)
if !isPrivateKeyProvider {
return nil, errors.New("account does not provide its private key")
}
if locker, isLocker := account.(e2wtypes.AccountLocker); isLocker {
if err = locker.Unlock(ctx, data.keystorePassphrase); err != nil {
return nil, errors.Wrap(err, "failed to unlock account")
}
}
key, err := privateKeyProvider.PrivateKey(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain private key")
}
data.key = key.Marshal()
// We have the key from the keystore; import it.
return processFromKey(ctx, data)
}

View File

@@ -0,0 +1,95 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountimport
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
)
func TestProcess(t *testing.T) {
require.NoError(t, e2types.InitBLS())
testNDWallet, err := nd.CreateWallet(context.Background(),
"Test",
scratch.New(),
keystorev4.New(),
)
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "PassphraseMissing",
dataIn: &dataIn{
timeout: 5 * time.Second,
wallet: testNDWallet,
accountName: "Good",
passphrase: "",
walletPassphrase: "pass",
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
},
err: "passphrase is required",
},
{
name: "PassphraseWeak",
dataIn: &dataIn{
timeout: 5 * time.Second,
wallet: testNDWallet,
accountName: "Good",
passphrase: "poor",
walletPassphrase: "pass",
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
},
err: "supplied passphrase is weak; use a stronger one or run with the --allow-weak-passphrases flag",
},
{
name: "Good",
dataIn: &dataIn{
timeout: 5 * time.Second,
wallet: testNDWallet,
accountName: "Good",
passphrase: "ce%NohGhah4ye5ra",
walletPassphrase: "pass",
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.dataIn.accountName, res.account.Name())
}
})
}
}

50
cmd/account/import/run.go Normal file
View File

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

51
cmd/account/key/input.go Normal file
View File

@@ -0,0 +1,51 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountkey
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
timeout time.Duration
account e2wtypes.Account
passphrases []string
}
func input(ctx context.Context) (*dataIn, error) {
var err error
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
// Account.
_, data.account, err = util.WalletAndAccountFromInput(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain acount")
}
// Passphrases.
data.passphrases = util.GetPassphrases()
return data, nil
}

View File

@@ -0,0 +1,128 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountkey
import (
"context"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"account": "Test wallet/Interop 0",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "timeout is required",
},
{
name: "WalletUnknown",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Unknown/Interop 0",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "failed to obtain acount: failed to open wallet for account: wallet not found",
},
{
name: "AccountMissing",
vars: map[string]interface{}{
"timeout": "5s",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "failed to obtain acount: failed to open wallet for account: invalid account format",
},
{
name: "AccountWalletOnly",
vars: map[string]interface{}{
"timeout": "5s",
"passphrase": "ce%NohGhah4ye5ra",
"account": "Test wallet/",
},
err: "failed to obtain acount: no account name",
},
{
name: "AccountMalformed",
vars: map[string]interface{}{
"timeout": "5s",
"account": "//",
"passphrase": "ce%NohGhah4ye5ra",
},
err: "failed to obtain acount: failed to open wallet for account: invalid account format",
},
{
name: "Good",
vars: map[string]interface{}{
"timeout": "5s",
"account": "Test wallet/Interop 0",
"passphrase": []string{"ce%NohGhah4ye5ra", "pass"},
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
},
res: &dataIn{
timeout: 5 * time.Second,
passphrases: []string{"ce%NohGhah4ye5ra", "pass"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
// Cannot compare accounts directly, so need to check each element individually.
require.Equal(t, test.res.timeout, res.timeout)
require.Equal(t, test.res.passphrases, res.passphrases)
}
})
}
}

36
cmd/account/key/output.go Normal file
View File

@@ -0,0 +1,36 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountkey
import (
"context"
"fmt"
"github.com/pkg/errors"
)
type dataOut struct {
key []byte
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if len(data.key) == 0 {
return "", errors.New("no account")
}
return fmt.Sprintf("%#x", data.key), nil
}

View File

@@ -0,0 +1,69 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountkey
import (
"context"
"encoding/hex"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func hexToBytes(input string) []byte {
res, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
if err != nil {
panic(err)
}
return res
}
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "AccountNil",
dataOut: &dataOut{},
err: "no account",
},
{
name: "Good",
dataOut: &dataOut{
key: hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
},
res: "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

View File

@@ -0,0 +1,70 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountkey
import (
"context"
"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) {
if data == nil {
return nil, errors.New("no data")
}
if len(data.passphrases) == 0 {
return nil, errors.New("passphrase is required")
}
results := &dataOut{}
privateKeyProvider, isPrivateKeyProvider := data.account.(e2wtypes.AccountPrivateKeyProvider)
if !isPrivateKeyProvider {
return nil, errors.New("account does not provide its private key")
}
if locker, isLocker := data.account.(e2wtypes.AccountLocker); isLocker {
unlocked, err := locker.IsUnlocked(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to find out if account is locked")
}
if !unlocked {
for _, passphrase := range data.passphrases {
err = locker.Unlock(ctx, []byte(passphrase))
if err == nil {
unlocked = true
break
}
}
if !unlocked {
return nil, errors.New("failed to unlock account")
}
// Because we unlocked the accout we should re-lock it when we're done.
defer func() {
if err := locker.Lock(ctx); err != nil {
util.Log.Trace().Err(err).Msg("Failed to lock account")
}
}()
}
}
key, err := privateKeyProvider.PrivateKey(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain private key")
}
results.key = key.Marshal()
return results, nil
}

View File

@@ -0,0 +1,87 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package accountkey
import (
"context"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
e2types "github.com/wealdtech/go-eth2-types/v2"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestProcess(t *testing.T) {
require.NoError(t, e2types.InitBLS())
testNDWallet, err := nd.CreateWallet(context.Background(),
"Test",
scratch.New(),
keystorev4.New(),
)
require.NoError(t, err)
require.NoError(t, testNDWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
interop0, err := testNDWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "PassphrasesMissing",
dataIn: &dataIn{
timeout: 5 * time.Second,
account: interop0,
},
err: "passphrase is required",
},
{
name: "Good",
dataIn: &dataIn{
timeout: 5 * time.Second,
account: interop0,
passphrases: []string{"ce%NohGhah4ye5ra", "pass"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.NotNil(t, res)
require.NotNil(t, res.key)
}
})
}
}

50
cmd/account/key/run.go Normal file
View File

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

View File

@@ -14,14 +14,11 @@
package cmd
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
accountcreate "github.com/wealdtech/ethdo/cmd/account/create"
)
var accountCreateCmd = &cobra.Command{
@@ -32,61 +29,32 @@ 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.`,
Run: func(cmd *cobra.Command, args []string) {
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
outputIf(debug, fmt.Sprintf("Opened wallet %q of type %s", wallet.Name(), wallet.Type()))
if wallet.Type() == "hierarchical deterministic" {
assert(getWalletPassphrase() != "", "walletpassphrase is required to create new accounts with hierarchical deterministic wallets")
RunE: func(cmd *cobra.Command, args []string) error {
res, err := accountcreate.Run(cmd)
if err != nil {
return err
}
locker, isLocker := wallet.(e2wtypes.WalletLocker)
if isLocker {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
errCheck(locker.Unlock(ctx, []byte(getWalletPassphrase())), "Failed to unlock wallet")
if viper.GetBool("quiet") {
return nil
}
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
var account e2wtypes.Account
if viper.GetUint("participants") > 0 {
// Want a distributed account.
distributedCreator, isDistributedCreator := wallet.(e2wtypes.WalletDistributedAccountCreator)
assert(isDistributedCreator, "Wallet does not support distributed account creation")
outputIf(debug, fmt.Sprintf("Distributed account has %d/%d threshold", viper.GetUint32("signing-threshold"), viper.GetUint32("participants")))
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err = distributedCreator.CreateDistributedAccount(ctx, accountName, viper.GetUint32("participants"), viper.GetUint32("signing-threshold"), []byte(getPassphrase()))
} else {
creator, isCreator := wallet.(e2wtypes.WalletAccountCreator)
assert(isCreator, "Wallet does not support account creation")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err = creator.CreateAccount(ctx, accountName, []byte(getPassphrase()))
if res != "" {
fmt.Println(res)
}
errCheck(err, "Failed to create account")
if pubKeyProvider, ok := account.(e2wtypes.AccountCompositePublicKeyProvider); ok {
outputIf(verbose, fmt.Sprintf("%#x", pubKeyProvider.CompositePublicKey().Marshal()))
} else if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
outputIf(verbose, fmt.Sprintf("%#x", pubKeyProvider.PublicKey().Marshal()))
}
os.Exit(_exitSuccess)
return nil
},
}
func init() {
accountCmd.AddCommand(accountCreateCmd)
accountFlags(accountCreateCmd)
accountCreateCmd.Flags().Uint32("participants", 0, "Number of participants (for distributed accounts)")
accountCreateCmd.Flags().Uint32("participants", 1, "Number of participants (1 for non-distributed accounts, >1 for distributed accounts)")
accountCreateCmd.Flags().Uint32("signing-threshold", 1, "Signing threshold (1 for non-distributed accounts)")
}
func accountCreateBindings() {
if err := viper.BindPFlag("participants", accountCreateCmd.Flags().Lookup("participants")); err != nil {
panic(err)
}
accountCreateCmd.Flags().Uint32("signing-threshold", 0, "Signing threshold (for distributed accounts)")
if err := viper.BindPFlag("signing-threshold", accountCreateCmd.Flags().Lookup("signing-threshold")); err != nil {
panic(err)
}

61
cmd/accountderive.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
accountderive "github.com/wealdtech/ethdo/cmd/account/derive"
)
var accountDeriveCmd = &cobra.Command{
Use: "derive",
Short: "Derive an account",
Long: `Derive an account from a mnemonic and path. For example:
ethdo account derive --mnemonic="..." --path="m/12381/3600/0/0"
In quiet mode this will return 0 if the inputs can derive an account account, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := accountderive.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
accountCmd.AddCommand(accountDeriveCmd)
accountFlags(accountDeriveCmd)
accountDeriveCmd.Flags().Bool("show-private-key", false, "show private key for derived account")
accountDeriveCmd.Flags().Bool("show-withdrawal-credentials", false, "show withdrawal credentials for derived account")
}
func accountDeriveBindings() {
if err := viper.BindPFlag("show-private-key", accountDeriveCmd.Flags().Lookup("show-private-key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("show-withdrawal-credentials", accountDeriveCmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
panic(err)
}
}

View File

@@ -14,19 +14,13 @@
package cmd
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/go-bytesutil"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
accountimport "github.com/wealdtech/ethdo/cmd/account/import"
)
var accountImportKey string
var accountImportCmd = &cobra.Command{
Use: "import",
Short: "Import an account",
@@ -35,49 +29,37 @@ 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.`,
Run: func(cmd *cobra.Command, args []string) {
assert(!remote, "account import not available with remote wallets")
assert(viper.GetString("account") != "", "--account is required")
passphrase := getPassphrase()
assert(accountImportKey != "", "--key is required")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
key, err := bytesutil.FromHexString(accountImportKey)
errCheck(err, "Invalid key")
w, err := walletFromPath(viper.GetString("account"))
errCheck(err, "Failed to access wallet")
_, ok := w.(e2wtypes.WalletAccountImporter)
assert(ok, fmt.Sprintf("wallets of type %q do not allow importing accounts", w.Type()))
_, err = accountFromPath(ctx, viper.GetString("account"))
assert(err != nil, "Account already exists")
locker, isLocker := w.(e2wtypes.WalletLocker)
if isLocker {
errCheck(locker.Unlock(ctx, []byte(getWalletPassphrase())), "Failed to unlock wallet")
RunE: func(cmd *cobra.Command, args []string) error {
res, err := accountimport.Run(cmd)
if err != nil {
return err
}
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
account, err := w.(e2wtypes.WalletAccountImporter).ImportAccount(ctx, accountName, key, []byte(passphrase))
errCheck(err, "Failed to create account")
pubKey, err := bestPublicKey(account)
if err == nil {
outputIf(verbose, fmt.Sprintf("%#x", pubKey.Marshal()))
if viper.GetBool("quiet") {
return nil
}
os.Exit(_exitSuccess)
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
accountCmd.AddCommand(accountImportCmd)
accountFlags(accountImportCmd)
accountImportCmd.Flags().StringVar(&accountImportKey, "key", "", "Private key of the account to import (0x...)")
accountImportCmd.Flags().String("key", "", "Private key of the account to import (0x...)")
accountImportCmd.Flags().String("keystore", "", "Keystore, or path to keystore ")
accountImportCmd.Flags().String("keystore-passphrase", "", "Passphrase of keystore")
}
func accountImportBindings() {
if err := viper.BindPFlag("key", accountImportCmd.Flags().Lookup("key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("keystore", accountImportCmd.Flags().Lookup("keystore")); err != nil {
panic(err)
}
if err := viper.BindPFlag("keystore-passphrase", accountImportCmd.Flags().Lookup("keystore-passphrase")); err != nil {
panic(err)
}
}

View File

@@ -22,7 +22,6 @@ import (
"github.com/spf13/viper"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -35,19 +34,11 @@ var accountInfoCmd = &cobra.Command{
In quiet mode this will return 0 if the account exists, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet cannot obtain accounts by name")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err := accountByNameProvider.AccountByName(ctx, accountName)
assert(viper.GetString("account") != "", "--account is required")
wallet, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
// Disallow wildcards (for now)
@@ -67,6 +58,13 @@ 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 {
fmt.Printf("Participants:\n")
for k, v := range distributedAccount.Participants() {
fmt.Printf(" %d: %s\n", k, v)
}
}
withdrawalPubKey = distributedAccount.CompositePublicKey()
}
if verbose {

View File

@@ -14,13 +14,11 @@
package cmd
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
accountkey "github.com/wealdtech/ethdo/cmd/account/key"
)
// accountKeyCmd represents the account key command
@@ -32,36 +30,18 @@ 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.`,
Run: func(cmd *cobra.Command, args []string) {
assert(!remote, "account keys not available with remote wallets")
assert(viper.GetString("account") != "", "--account is required")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err := accountFromPath(ctx, viper.GetString("account"))
errCheck(err, "Failed to access account")
privateKeyProvider, isPrivateKeyProvider := account.(e2wtypes.AccountPrivateKeyProvider)
assert(isPrivateKeyProvider, fmt.Sprintf("account %q does not provide its private key", viper.GetString("account")))
if locker, isLocker := account.(e2wtypes.AccountLocker); isLocker {
unlocked := false
for _, passphrase := range getPassphrases() {
err = locker.Unlock(ctx, []byte(passphrase))
if err == nil {
unlocked = true
break
}
}
assert(unlocked, "Failed to unlock account to obtain private key")
defer errCheck(locker.Lock(context.Background()), "failed to re-lock account")
RunE: func(cmd *cobra.Command, args []string) error {
res, err := accountkey.Run(cmd)
if err != nil {
return err
}
privateKey, err := privateKeyProvider.PrivateKey(ctx)
errCheck(err, "Failed to obtain private key")
outputIf(!quiet, fmt.Sprintf("%#x", privateKey.Marshal()))
os.Exit(_exitSuccess)
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}

View File

@@ -18,7 +18,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -31,27 +30,18 @@ var accountLockCmd = &cobra.Command{
In quiet mode this will return 0 if the account is locked, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet cannot obtain accounts by name")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err := accountByNameProvider.AccountByName(ctx, accountName)
assert(viper.GetString("account") != "", "--account is required")
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
locker, isLocker := account.(e2wtypes.AccountLocker)
assert(isLocker, "Account does not support locking")
ctx, cancel = context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
err = locker.Lock(ctx)
cancel()
errCheck(err, "Failed to lock account")
},
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -32,19 +31,12 @@ var accountUnlockCmd = &cobra.Command{
In quiet mode this will return 0 if the account is unlocked, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet cannot obtain accounts by name")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err := accountByNameProvider.AccountByName(ctx, accountName)
assert(viper.GetString("account") != "", "--account is required")
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
locker, isLocker := account.(e2wtypes.AccountLocker)

View File

@@ -1,83 +0,0 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"context"
"encoding/hex"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
util "github.com/wealdtech/go-eth2-util"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
var accountWithdrawalCredentialsCmd = &cobra.Command{
Use: "withdrawalcredentials",
Short: "Provide withdrawal credentials for an account",
Long: `Provide withdrawal credentials for an account. For example:
ethdo account withdrawalcredentials --account="Validators/1"
In quiet mode this will return 0 if the account exists, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
assert(viper.GetString("account") != "" || viper.GetString("pubkey") != "", "account or pubkey is required")
var pubKey []byte
if viper.GetString("pubkey") != "" {
var err error
pubKey, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("pubkey"), "0x"))
errCheck(err, "Failed to decode supplied public key")
} else {
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet cannot obtain accounts by name")
account, err := accountByNameProvider.AccountByName(ctx, accountName)
errCheck(err, "Failed to obtain account")
key, err := bestPublicKey(account)
errCheck(err, "Account does not provide a public key")
pubKey = key.Marshal()
}
if quiet {
os.Exit(_exitSuccess)
}
withdrawalCredentials := util.SHA256(pubKey)
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
fmt.Printf("%#x\n", withdrawalCredentials)
},
}
func init() {
accountCmd.AddCommand(accountWithdrawalCredentialsCmd)
accountFlags(accountWithdrawalCredentialsCmd)
accountWithdrawalCredentialsCmd.Flags().String("pubkey", "", "Public key (overrides account)")
if err := viper.BindPFlag("pubkey", accountCreateCmd.Flags().Lookup("pubkey")); err != nil {
panic(err)
}
}

29
cmd/attestation.go Normal file
View File

@@ -0,0 +1,29 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"github.com/spf13/cobra"
)
// attestationCmd represents the attestation command
var attestationCmd = &cobra.Command{
Use: "attestation",
Short: "Obtain information about an Ethereum 2 attestation",
Long: "Obtain information about an Ethereum 2 attestation",
}
func init() {
RootCmd.AddCommand(attestationCmd)
}

32
cmd/attester.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"github.com/spf13/cobra"
)
// attesterCmd represents the attester command
var attesterCmd = &cobra.Command{
Use: "attester",
Short: "Obtain information about Ethereum 2 attesters",
Long: "Obtain information about Ethereum 2 attesters",
}
func init() {
RootCmd.AddCommand(attesterCmd)
}
func attesterFlags(cmd *cobra.Command) {
}

View File

@@ -0,0 +1,89 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterduties
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
json bool
// Chain information.
slotsPerEpoch uint64
// Operation.
account string
pubKey string
eth2Client eth2client.Service
epoch spec.Epoch
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
data.json = viper.GetBool("json")
// Account or pubkey.
if viper.GetString("account") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account or pubkey 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"))
if err != nil {
return nil, err
}
// Required data.
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
// Epoch
epoch := viper.GetInt64("epoch")
if epoch == -1 {
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))
}
data.epoch = spec.Epoch(epoch)
return data, nil
}

View File

@@ -0,0 +1,96 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterduties
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "AccountMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "account or pubkey is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "failed to connect to any beacon node",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
}
})
}
}

View File

@@ -0,0 +1,55 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterduties
import (
"context"
"encoding/json"
"fmt"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
json bool
duty *api.AttesterDuty
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.quiet {
return "", nil
}
if data.duty == nil {
return "No duties found", nil
}
if data.json {
bytes, err := json.Marshal(data.duty)
if err != nil {
return "", errors.Wrap(err, "failed to marshalJSON")
}
return string(bytes), nil
}
return fmt.Sprintf("Validator attesting in slot %d committee %d", data.duty.Slot, data.duty.CommitteeIndex), nil
}

View File

@@ -0,0 +1,85 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterduties
import (
"context"
"testing"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Empty",
dataOut: &dataOut{},
res: "No duties found",
},
{
name: "Present",
dataOut: &dataOut{
duty: &api.AttesterDuty{
PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
Slot: 1,
ValidatorIndex: 2,
CommitteeIndex: 3,
CommitteeLength: 4,
CommitteesAtSlot: 5,
ValidatorCommitteeIndex: 6,
},
},
res: "Validator attesting in slot 1 committee 3",
},
{
name: "JSON",
dataOut: &dataOut{
json: true,
duty: &api.AttesterDuty{
PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
Slot: 1,
ValidatorIndex: 2,
CommitteeIndex: 3,
CommitteeLength: 4,
CommitteesAtSlot: 5,
ValidatorCommitteeIndex: 6,
},
},
res: `{"pubkey":"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95","slot":"1","validator_index":"2","committee_index":"3","committee_length":"4","committees_at_slot":"5","validator_committee_index":"6"}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

View File

@@ -0,0 +1,102 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterduties
import (
"context"
"encoding/hex"
"fmt"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
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)
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
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
duty, err := duty(ctx, data.eth2Client, validator, data.epoch, data.slotsPerEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain duty for validator")
}
results.duty = duty
return results, nil
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attester duties")
}
if len(duties) == 0 {
return nil, errors.New("validator does not have duty for that epoch")
}
return duties[0], nil
}

View File

@@ -0,0 +1,66 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterduties
import (
"context"
"os"
"testing"
"github.com/attestantio/go-eth2-client/auto"
"github.com/rs/zerolog"
"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")
}
eth2Client, err := auto.New(context.Background(),
auto.WithLogLevel(zerolog.Disabled),
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
)
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Client",
dataIn: &dataIn{
eth2Client: eth2Client,
slotsPerEpoch: 32,
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
epoch: 100,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

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

View File

@@ -0,0 +1,102 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"fmt"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
"github.com/wealdtech/ethdo/util"
)
type dataIn struct {
// System.
timeout time.Duration
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
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
// Account.
data.account = viper.GetString("account")
// PubKey.
data.pubKey = viper.GetString("pubkey")
// ID.
data.index = viper.GetString("index")
if viper.GetString("account") == "" && viper.GetString("index") == "" && viper.GetString("pubkey") == "" {
return nil, errors.New("account, index or pubkey is required")
}
// Ethereum 2 client.
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, err
}
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon chain configuration")
}
data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
// Epoch.
epoch := viper.GetInt64("epoch")
if epoch == -1 {
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)
}
return data, nil
}

View File

@@ -0,0 +1,96 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "IndexMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "account, index or pubkey is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
"pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c",
},
err: "failed to connect to any beacon node",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
}
})
}
}

View File

@@ -0,0 +1,92 @@
// Copyright © 2019 - 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 attesterinclusion
import (
"context"
"fmt"
"strings"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
attestation *phase0.Attestation
slot phase0.Slot
attestationIndex uint64
inclusionDelay phase0.Slot
found bool
headCorrect bool
headTimely bool
sourceTimely bool
targetCorrect bool
targetTimely bool
}
func output(ctx context.Context, data *dataOut) (string, error) {
buf := strings.Builder{}
if data == nil {
return buf.String(), errors.New("no data")
}
if !data.quiet {
if data.found {
buf.WriteString("Attestation included in block ")
buf.WriteString(fmt.Sprintf("%d", data.slot))
buf.WriteString(", index ")
buf.WriteString(fmt.Sprintf("%d", data.attestationIndex))
if data.verbose {
buf.WriteString("\nInclusion delay: ")
buf.WriteString(fmt.Sprintf("%d", data.inclusionDelay))
buf.WriteString("\nHead correct: ")
if data.headCorrect {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
buf.WriteString("\nHead timely: ")
if data.headTimely {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
buf.WriteString("\nSource timely: ")
if data.sourceTimely {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
buf.WriteString("\nTarget correct: ")
if data.targetCorrect {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
buf.WriteString("\nTarget timely: ")
if data.targetTimely {
buf.WriteString("✓")
} else {
buf.WriteString("✕")
}
}
} else {
buf.WriteString("Attestation not found")
}
}
return buf.String(), nil
}

View File

@@ -0,0 +1,84 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Empty",
dataOut: &dataOut{},
res: "Attestation not found",
},
{
name: "Found",
dataOut: &dataOut{
found: true,
slot: 123,
attestationIndex: 456,
inclusionDelay: 7,
},
res: `Attestation included in block 123, index 456`,
},
{
name: "Verbose",
dataOut: &dataOut{
verbose: true,
found: true,
slot: 123,
attestationIndex: 456,
inclusionDelay: 7,
headCorrect: true,
headTimely: false,
sourceTimely: false,
targetCorrect: true,
targetTimely: true,
},
res: `Attestation included in block 123, index 456
Inclusion delay: 7
Head correct: ✓
Head timely: ✕
Source timely: ✕
Target correct: ✓
Target timely: ✓`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

View File

@@ -0,0 +1,192 @@
// Copyright © 2019 - 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// 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 attesterinclusion
import (
"bytes"
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
var err error
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(data.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return nil, errors.Wrap(err, "failed to set up chaintime service")
}
validatorIndex, err := util.ValidatorIndex(ctx, data.eth2Client, data.account, data.pubKey, data.index)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator index")
}
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), []phase0.ValidatorIndex{validatorIndex})
if err != nil {
return nil, errors.New("failed to obtain validator information")
}
if len(validators) == 0 {
return nil, errors.New("validator is not known")
}
var validator *api.Validator
for _, v := range validators {
validator = v
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
duty, err := duty(ctx, data.eth2Client, validator, data.epoch, data.slotsPerEpoch)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain duty for validator")
}
if data.debug {
fmt.Printf("Duty is %s\n", duty.String())
}
startSlot := duty.Slot + 1
endSlot := startSlot + 32
for slot := startSlot; slot < endSlot; slot++ {
signedBlock, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block")
}
if signedBlock == nil {
continue
}
blockSlot, err := signedBlock.Slot()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain block slot")
}
if blockSlot != slot {
continue
}
if data.debug {
fmt.Printf("Fetched block for slot %d\n", slot)
}
attestations, err := signedBlock.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) {
headCorrect := false
targetCorrect := false
if data.verbose {
headCorrect, err = calcHeadCorrect(ctx, data, attestation)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain head correct result")
}
targetCorrect, err = calcTargetCorrect(ctx, data, attestation)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain target correct result")
}
}
results.found = true
results.attestation = attestation
results.slot = slot
results.attestationIndex = uint64(i)
results.inclusionDelay = slot - duty.Slot
results.sourceTimely = results.inclusionDelay <= 5 // sqrt(32)
results.targetCorrect = targetCorrect
results.targetTimely = targetCorrect && results.inclusionDelay <= 32
results.headCorrect = headCorrect
results.headTimely = headCorrect && results.inclusionDelay == 1
if data.debug {
fmt.Printf("Attestation is %s\n", attestation.String())
}
return results, nil
}
}
}
return results, nil
}
func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
}
func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.Attestation) (bool, error) {
// Start with first slot of the target epoch.
slot := data.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
return bytes.Equal(header.Root[:], attestation.Data.Target.Root[:]), nil
}
}
func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch phase0.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) {
// Find the attesting slot for the given epoch.
duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []phase0.ValidatorIndex{validator.Index})
if err != nil {
return nil, errors.Wrap(err, "failed to obtain attester duties")
}
if len(duties) == 0 {
return nil, errors.New("validator does not have duty for that epoch")
}
return duties[0], nil
}

View File

@@ -0,0 +1,66 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package attesterinclusion
import (
"context"
"os"
"testing"
"github.com/attestantio/go-eth2-client/auto"
"github.com/rs/zerolog"
"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")
}
eth2Client, err := auto.New(context.Background(),
auto.WithLogLevel(zerolog.Disabled),
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
)
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Client",
dataIn: &dataIn{
eth2Client: eth2Client,
slotsPerEpoch: 32,
pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95",
epoch: 100,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

View File

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

65
cmd/attesterduties.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
attesterduties "github.com/wealdtech/ethdo/cmd/attester/duties"
)
var attesterDutiesCmd = &cobra.Command{
Use: "duties",
Short: "Obtain information about duties of an attester",
Long: `Obtain information about dutes of an attester. For example:
ethdo attester duties --account=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 {
res, err := attesterduties.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
func init() {
attesterCmd.AddCommand(attesterDutiesCmd)
attesterFlags(attesterDutiesCmd)
attesterDutiesCmd.Flags().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")
}
func attesterDutiesBindings() {
if err := viper.BindPFlag("epoch", attesterDutiesCmd.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 {
panic(err)
}
}

65
cmd/attesterinclusion.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright © 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
attesterinclusion "github.com/wealdtech/ethdo/cmd/attester/inclusion"
)
var attesterInclusionCmd = &cobra.Command{
Use: "inclusion",
Short: "Obtain information about attester inclusion",
Long: `Obtain information about attester inclusion. For example:
ethdo attester inclusion --account=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 {
res, err := attesterinclusion.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Println(res)
}
return nil
},
}
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("index", "", "the index of the attester")
}
func attesterInclusionBindings() {
if err := viper.BindPFlag("epoch", attesterInclusionCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("pubkey", attesterInclusionCmd.Flags().Lookup("pubkey")); err != nil {
panic(err)
}
if err := viper.BindPFlag("index", attesterInclusionCmd.Flags().Lookup("index")); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,133 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockanalyze
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/services/chaintime"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Operation.
blockID string
stream bool
jsonOutput bool
// Data access.
eth2Client eth2client.Service
chainTime chaintime.Service
blocksProvider eth2client.SignedBeaconBlockProvider
blockHeadersProvider eth2client.BeaconBlockHeadersProvider
// Constants.
timelySourceWeight uint64
timelyTargetWeight uint64
timelyHeadWeight uint64
syncRewardWeight uint64
proposerWeight uint64
weightDenominator uint64
// Processing.
priorAttestations map[string]*attestationData
// Head roots provides the root of the head slot at given slots.
headRoots map[phase0.Slot]phase0.Root
// Target roots provides the root of the target epoch at given slots.
targetRoots map[phase0.Slot]phase0.Root
// Block info.
// Map is slot -> committee index -> validator committee index -> votes.
votes map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist
// Results.
analysis *blockAnalysis
}
type blockAnalysis struct {
Slot phase0.Slot `json:"slot"`
Attestations []*attestationAnalysis `json:"attestations"`
SyncCommitee *syncCommitteeAnalysis `json:"sync_committee"`
Value float64 `json:"value"`
}
type attestationAnalysis struct {
Head phase0.Root `json:"head"`
Target phase0.Root `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
type syncCommitteeAnalysis struct {
Contributions int `json:"contributions"`
PossibleContributions int `json:"possible_contributions"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
type attestationData struct {
Block phase0.Slot `json:"block"`
Index int `json:"index"`
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
priorAttestations: make(map[string]*attestationData),
headRoots: make(map[phase0.Slot]phase0.Root),
targetRoots: make(map[phase0.Slot]phase0.Root),
votes: make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
c.blockID = viper.GetString("blockid")
c.stream = viper.GetBool("stream")
c.jsonOutput = viper.GetBool("json")
return c, nil
}

View File

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

157
cmd/block/analyze/output.go Normal file
View File

@@ -0,0 +1,157 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockanalyze
import (
"context"
"encoding/json"
"fmt"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
if c.jsonOutput {
return c.outputJSON(ctx)
}
return c.outputTxt(ctx)
}
type attestationAnalysisJSON struct {
Head string `json:"head"`
Target string `json:"target"`
Distance int `json:"distance"`
Duplicate *attestationData `json:"duplicate,omitempty"`
NewVotes int `json:"new_votes"`
Votes int `json:"votes"`
PossibleVotes int `json:"possible_votes"`
HeadCorrect bool `json:"head_correct"`
HeadTimely bool `json:"head_timely"`
SourceTimely bool `json:"source_timely"`
TargetCorrect bool `json:"target_correct"`
TargetTimely bool `json:"target_timely"`
Score float64 `json:"score"`
Value float64 `json:"value"`
}
func (a *attestationAnalysis) MarshalJSON() ([]byte, error) {
return json.Marshal(attestationAnalysisJSON{
Head: fmt.Sprintf("%#x", a.Head),
Target: fmt.Sprintf("%#x", a.Target),
Distance: a.Distance,
Duplicate: a.Duplicate,
NewVotes: a.NewVotes,
Votes: a.Votes,
PossibleVotes: a.PossibleVotes,
HeadCorrect: a.HeadCorrect,
HeadTimely: a.HeadTimely,
SourceTimely: a.SourceTimely,
TargetCorrect: a.TargetCorrect,
TargetTimely: a.TargetTimely,
Score: a.Score,
Value: a.Value,
})
}
func (c *command) outputJSON(_ context.Context) (string, error) {
data, err := json.Marshal(c.analysis)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *command) outputTxt(_ context.Context) (string, error) {
builder := strings.Builder{}
for i, attestation := range c.analysis.Attestations {
if c.verbose {
builder.WriteString("Attestation ")
builder.WriteString(fmt.Sprintf("%d", i))
builder.WriteString(": ")
builder.WriteString("distance ")
builder.WriteString(fmt.Sprintf("%d", attestation.Distance))
builder.WriteString(", ")
if attestation.Duplicate != nil {
builder.WriteString("duplicate of attestation ")
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Index))
builder.WriteString(" in block ")
builder.WriteString(fmt.Sprintf("%d", attestation.Duplicate.Block))
builder.WriteString("\n")
continue
}
builder.WriteString(fmt.Sprintf("%d", attestation.NewVotes))
builder.WriteString("/")
builder.WriteString(fmt.Sprintf("%d", attestation.Votes))
builder.WriteString("/")
builder.WriteString(fmt.Sprintf("%d", attestation.PossibleVotes))
builder.WriteString(" new/total/possible votes")
if attestation.NewVotes == 0 {
builder.WriteString("\n")
continue
} else {
builder.WriteString(", ")
}
switch {
case !attestation.HeadCorrect:
builder.WriteString("head vote incorrect, ")
case !attestation.HeadTimely:
builder.WriteString("head vote correct but late, ")
}
if !attestation.SourceTimely {
builder.WriteString("source vote late, ")
}
switch {
case !attestation.TargetCorrect:
builder.WriteString("target vote incorrect, ")
case !attestation.TargetTimely:
builder.WriteString("target vote correct but late, ")
}
builder.WriteString("score ")
builder.WriteString(fmt.Sprintf("%0.3f", attestation.Score))
builder.WriteString(", value ")
builder.WriteString(fmt.Sprintf("%0.3f", attestation.Value))
builder.WriteString("\n")
}
}
if c.analysis.SyncCommitee.Contributions > 0 {
if c.verbose {
builder.WriteString("Sync committee contributions: ")
builder.WriteString(fmt.Sprintf("%d", c.analysis.SyncCommitee.Contributions))
builder.WriteString(" contributions, score ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Score))
builder.WriteString(", value ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.SyncCommitee.Value))
builder.WriteString("\n")
}
}
builder.WriteString("Value for block ")
builder.WriteString(fmt.Sprintf("%d", c.analysis.Slot))
builder.WriteString(": ")
builder.WriteString(fmt.Sprintf("%0.3f", c.analysis.Value))
builder.WriteString("\n")
return builder.String(), nil
}

View File

@@ -0,0 +1,438 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockanalyze
import (
"bytes"
"context"
"fmt"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
block, err := c.blocksProvider.SignedBeaconBlock(ctx, c.blockID)
if err != nil {
return errors.Wrap(err, "failed to obtain beacon block")
}
if block == nil {
return errors.New("empty beacon block")
}
slot, err := block.Slot()
if err != nil {
return err
}
attestations, err := block.Attestations()
if err != nil {
return err
}
c.analysis = &blockAnalysis{
Slot: slot,
}
// Calculate how many parents we need to fetch.
minSlot := slot
for _, attestation := range attestations {
if attestation.Data.Slot < minSlot {
minSlot = attestation.Data.Slot
}
}
if c.debug {
fmt.Printf("Need to fetch blocks to slot %d\n", minSlot)
}
if err := c.fetchParents(ctx, block, minSlot); err != nil {
return err
}
return c.analyze(ctx, block)
}
func (c *command) analyze(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
if err := c.analyzeAttestations(ctx, block); err != nil {
return err
}
if err := c.analyzeSyncCommittees(ctx, block); err != nil {
return err
}
return nil
}
func (c *command) analyzeAttestations(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
}
slot, err := block.Slot()
if err != nil {
return err
}
c.analysis.Attestations = make([]*attestationAnalysis, len(attestations))
blockVotes := make(map[phase0.Slot]map[phase0.CommitteeIndex]bitfield.Bitlist)
for i, attestation := range attestations {
if c.debug {
fmt.Printf("Processing attestation %d\n", i)
}
analysis := &attestationAnalysis{
Head: attestation.Data.BeaconBlockRoot,
Target: attestation.Data.Target.Root,
Distance: int(slot - attestation.Data.Slot),
}
root, err := attestation.HashTreeRoot()
if err != nil {
return err
}
if info, exists := c.priorAttestations[fmt.Sprintf("%#x", root)]; exists {
analysis.Duplicate = info
} else {
data := attestation.Data
_, exists := blockVotes[data.Slot]
if !exists {
blockVotes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = blockVotes[data.Slot][data.Index]
if !exists {
blockVotes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
// Count new votes.
analysis.PossibleVotes = int(attestation.AggregationBits.Len())
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
analysis.Votes++
if blockVotes[data.Slot][data.Index].BitAt(j) {
// Already attested to in this block; skip.
continue
}
if c.votes[data.Slot][data.Index].BitAt(j) {
// Already attested to in a previous block; skip.
continue
}
analysis.NewVotes++
blockVotes[data.Slot][data.Index].SetBitAt(j, true)
}
}
// Calculate head correct.
var err error
analysis.HeadCorrect, err = c.calcHeadCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate head timely.
analysis.HeadTimely = attestation.Data.Slot == slot-1
// Calculate source timely.
analysis.SourceTimely = attestation.Data.Slot >= slot-5
// Calculate target correct.
analysis.TargetCorrect, err = c.calcTargetCorrect(ctx, attestation)
if err != nil {
return err
}
// Calculate target timely.
analysis.TargetTimely = attestation.Data.Slot >= slot-32
}
// Calculate score and value.
if analysis.TargetCorrect && analysis.TargetTimely {
analysis.Score += float64(c.timelyTargetWeight) / float64(c.weightDenominator)
}
if analysis.SourceTimely {
analysis.Score += float64(c.timelySourceWeight) / float64(c.weightDenominator)
}
if analysis.HeadCorrect && analysis.HeadTimely {
analysis.Score += float64(c.timelyHeadWeight) / float64(c.weightDenominator)
}
analysis.Value = analysis.Score * float64(analysis.NewVotes)
c.analysis.Value += analysis.Value
c.analysis.Attestations[i] = analysis
}
return nil
}
func (c *command) fetchParents(ctx context.Context, block *spec.VersionedSignedBeaconBlock, minSlot phase0.Slot) error {
parentRoot, err := block.ParentRoot()
if err != nil {
return err
}
// Obtain the parent block.
parentBlock, err := c.blocksProvider.SignedBeaconBlock(ctx, fmt.Sprintf("%#x", parentRoot))
if err != nil {
return err
}
if parentBlock == nil {
return fmt.Errorf("unable to obtain parent block %s", parentBlock)
}
parentSlot, err := parentBlock.Slot()
if err != nil {
return err
}
if parentSlot < minSlot {
return nil
}
if err := c.processParentBlock(ctx, parentBlock); err != nil {
return err
}
return c.fetchParents(ctx, parentBlock, minSlot)
}
func (c *command) processParentBlock(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
attestations, err := block.Attestations()
if err != nil {
return err
}
slot, err := block.Slot()
if err != nil {
return err
}
if c.debug {
fmt.Printf("Processing block %d\n", slot)
}
for i, attestation := range attestations {
root, err := attestation.HashTreeRoot()
if err != nil {
return err
}
c.priorAttestations[fmt.Sprintf("%#x", root)] = &attestationData{
Block: slot,
Index: i,
}
data := attestation.Data
_, exists := c.votes[data.Slot]
if !exists {
c.votes[data.Slot] = make(map[phase0.CommitteeIndex]bitfield.Bitlist)
}
_, exists = c.votes[data.Slot][data.Index]
if !exists {
c.votes[data.Slot][data.Index] = bitfield.NewBitlist(attestation.AggregationBits.Len())
}
for j := uint64(0); j < attestation.AggregationBits.Len(); j++ {
if attestation.AggregationBits.BitAt(j) {
c.votes[data.Slot][data.Index].SetBitAt(j, true)
}
}
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
// Obtain the number of active validators.
var isProvider bool
c.blocksProvider, isProvider = c.eth2Client.(eth2client.SignedBeaconBlockProvider)
if !isProvider {
return errors.New("connection does not provide signed beacon block information")
}
c.blockHeadersProvider, isProvider = c.eth2Client.(eth2client.BeaconBlockHeadersProvider)
if !isProvider {
return errors.New("connection does not provide beacon block header information")
}
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
if !isProvider {
return errors.New("connection does not provide spec information")
}
spec, err := specProvider.Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["TIMELY_SOURCE_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
}
var ok bool
c.timelySourceWeight, ok = tmp.(uint64)
if !ok {
return errors.New("TIMELY_SOURCE_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_TARGET_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(26)
}
c.timelyTargetWeight, ok = tmp.(uint64)
if !ok {
return errors.New("TIMELY_TARGET_WEIGHT of unexpected type")
}
tmp, exists = spec["TIMELY_HEAD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(14)
}
c.timelyHeadWeight, ok = tmp.(uint64)
if !ok {
return errors.New("TIMELY_HEAD_WEIGHT of unexpected type")
}
tmp, exists = spec["SYNC_REWARD_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(2)
}
c.syncRewardWeight, ok = tmp.(uint64)
if !ok {
return errors.New("SYNC_REWARD_WEIGHT of unexpected type")
}
tmp, exists = spec["PROPOSER_WEIGHT"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(8)
}
c.proposerWeight, ok = tmp.(uint64)
if !ok {
return errors.New("PROPOSER_WEIGHT of unexpected type")
}
tmp, exists = spec["WEIGHT_DENOMINATOR"]
if !exists {
// Set a default value based on the Altair spec.
tmp = uint64(64)
}
c.weightDenominator, ok = tmp.(uint64)
if !ok {
return errors.New("WEIGHT_DENOMINATOR of unexpected type")
}
return nil
}
func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
slot := attestation.Data.Slot
root, exists := c.headRoots[slot]
if !exists {
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
c.headRoots[attestation.Data.Slot] = header.Root
root = header.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.BeaconBlockRoot[:]), nil
}
func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Attestation) (bool, error) {
root, exists := c.targetRoots[attestation.Data.Slot]
if !exists {
// Start with first slot of the target epoch.
slot := c.chainTime.FirstSlotOfEpoch(attestation.Data.Target.Epoch)
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
}
if header == nil {
// No block.
slot--
continue
}
if !header.Canonical {
// Not canonical.
slot--
continue
}
c.targetRoots[attestation.Data.Slot] = header.Root
root = header.Root
break
}
}
return bytes.Equal(root[:], attestation.Data.Target.Root[:]), nil
}
func (c *command) analyzeSyncCommittees(ctx context.Context, block *spec.VersionedSignedBeaconBlock) error {
c.analysis.SyncCommitee = &syncCommitteeAnalysis{}
switch block.Version {
case spec.DataVersionPhase0:
return nil
case spec.DataVersionAltair:
c.analysis.SyncCommitee.Contributions = int(block.Altair.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Altair.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
case spec.DataVersionBellatrix:
c.analysis.SyncCommitee.Contributions = int(block.Bellatrix.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Bellatrix.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
default:
return fmt.Errorf("unsupported block version %d", block.Version)
}
}

View File

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

50
cmd/block/analyze/run.go Normal file
View File

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

70
cmd/block/info/input.go Normal file
View File

@@ -0,0 +1,70 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
// Operation.
eth2Client eth2client.Service
jsonOutput bool
sszOutput bool
// Chain information.
blockID string
stream bool
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
data.jsonOutput = viper.GetBool("json")
data.sszOutput = viper.GetBool("ssz")
data.stream = viper.GetBool("stream")
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, err
}
if viper.GetString("blockid") == "" {
data.blockID = "head"
} else {
// Specific slot.
data.blockID = viper.GetString("blockid")
}
return data, nil
}

View File

@@ -0,0 +1,126 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"os"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "ConnectionMissing",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "failed to connect to any beacon node",
},
{
name: "ConnectionBad",
vars: map[string]interface{}{
"timeout": "5s",
"connection": "localhost:1",
"blockid": "justified",
},
res: &dataIn{
timeout: 5 * time.Second,
blockID: "justified",
},
err: "failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
},
{
name: "BlockIDNil",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
res: &dataIn{
timeout: 5 * time.Second,
blockID: "head",
},
},
{
name: "BlockIDSpecific",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"blockid": "justified",
},
res: &dataIn{
timeout: 5 * time.Second,
blockID: "justified",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
require.Equal(t, test.res.blockID, res.blockID)
}
})
}
}

652
cmd/block/info/output.go Normal file
View File

@@ -0,0 +1,652 @@
// Copyright © 2019, 2020, 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"math/big"
"sort"
"strings"
"time"
"unicode/utf8"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
"github.com/wealdtech/go-string2eth"
)
type dataOut struct {
debug bool
verbose bool
eth2Client eth2client.Service
genesisTime time.Time
slotDuration time.Duration
slotsPerEpoch uint64
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
return "", nil
}
func outputBlockGeneral(ctx context.Context,
verbose bool,
slot phase0.Slot,
blockRoot phase0.Root,
bodyRoot phase0.Root,
parentRoot phase0.Root,
stateRoot phase0.Root,
graffiti []byte,
genesisTime time.Time,
slotDuration time.Duration,
slotsPerEpoch uint64,
) (
string,
error,
) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Slot: %d\n", slot))
res.WriteString(fmt.Sprintf("Epoch: %d\n", phase0.Epoch(uint64(slot)/slotsPerEpoch)))
res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(slot)*int64(slotDuration.Seconds()), 0)))
res.WriteString(fmt.Sprintf("Block root: %#x\n", blockRoot))
if verbose {
res.WriteString(fmt.Sprintf("Body root: %#x\n", bodyRoot))
res.WriteString(fmt.Sprintf("Parent root: %#x\n", parentRoot))
res.WriteString(fmt.Sprintf("State root: %#x\n", stateRoot))
}
if len(graffiti) > 0 && hex.EncodeToString(graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
graffiti = bytes.TrimRight(graffiti, "\u0000")
if utf8.Valid(graffiti) {
res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(graffiti)))
} else {
res.WriteString(fmt.Sprintf("Graffiti: %#x\n", graffiti))
}
}
return res.String(), nil
}
func outputBlockETH1Data(ctx context.Context, eth1Data *phase0.ETH1Data) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Ethereum 1 deposit count: %d\n", eth1Data.DepositCount))
res.WriteString(fmt.Sprintf("Ethereum 1 deposit root: %#x\n", eth1Data.DepositRoot))
res.WriteString(fmt.Sprintf("Ethereum 1 block hash: %#x\n", eth1Data.BlockHash))
return res.String(), nil
}
func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*phase0.Attestation) (string, error) {
res := strings.Builder{}
validatorCommittees := make(map[phase0.Slot]map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
res.WriteString(fmt.Sprintf("Attestations: %d\n", len(attestations)))
if verbose {
beaconCommitteesProvider, isProvider := eth2Client.(eth2client.BeaconCommitteesProvider)
if isProvider {
for i, att := range attestations {
res.WriteString(fmt.Sprintf(" %d:\n", i))
// Fetch committees for this epoch if not already obtained.
committees, exists := validatorCommittees[att.Data.Slot]
if !exists {
beaconCommittees, err := beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", att.Data.Slot))
if err != nil {
// Failed to get it; create an empty committee to stop us continually attempting to re-fetch.
validatorCommittees[att.Data.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
} else {
for _, beaconCommittee := range beaconCommittees {
if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists {
validatorCommittees[beaconCommittee.Slot] = make(map[phase0.CommitteeIndex][]phase0.ValidatorIndex)
}
validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators
}
}
committees = validatorCommittees[att.Data.Slot]
}
res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index))
res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()))
res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitlistToString(att.AggregationBits)))
if _, exists := committees[att.Data.Index]; exists {
res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index])))
}
res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot))
res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot))
res.WriteString(fmt.Sprintf(" Source epoch: %d\n", att.Data.Source.Epoch))
res.WriteString(fmt.Sprintf(" Source root: %#x\n", att.Data.Source.Root))
res.WriteString(fmt.Sprintf(" Target epoch: %d\n", att.Data.Target.Epoch))
res.WriteString(fmt.Sprintf(" Target root: %#x\n", att.Data.Target.Root))
}
}
}
return res.String(), nil
}
func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*phase0.AttesterSlashing) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings)))
if verbose {
for i, slashing := range attesterSlashings {
// Say what was slashed.
att1 := slashing.Attestation1
att2 := slashing.Attestation2
slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices)
if len(slashedIndices) == 0 {
continue
}
res.WriteString(fmt.Sprintf(" %d:\n", i))
res.WriteString(fmt.Sprintln(" Slashed validators:"))
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", slashedIndices)
if err != nil {
return "", errors.Wrap(err, "failed to obtain beacon committees")
}
for k, v := range validators {
res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k))
}
// Say what caused the slashing.
if att1.Data.Target.Epoch == att2.Data.Target.Epoch {
res.WriteString(fmt.Sprintf(" Double voted for same target epoch (%d):\n", att1.Data.Target.Epoch))
if !bytes.Equal(att1.Data.Target.Root[:], att2.Data.Target.Root[:]) {
res.WriteString(fmt.Sprintf(" Attestation 1 target epoch root: %#x\n", att1.Data.Target.Root))
res.WriteString(fmt.Sprintf(" Attestation 2target epoch root: %#x\n", att2.Data.Target.Root))
}
if !bytes.Equal(att1.Data.BeaconBlockRoot[:], att2.Data.BeaconBlockRoot[:]) {
res.WriteString(fmt.Sprintf(" Attestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot))
res.WriteString(fmt.Sprintf(" Attestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot))
}
} else if att1.Data.Source.Epoch < att2.Data.Source.Epoch &&
att1.Data.Target.Epoch > att2.Data.Target.Epoch {
res.WriteString(" Surround voted:\n")
res.WriteString(fmt.Sprintf(" Attestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch))
res.WriteString(fmt.Sprintf(" Attestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch))
}
}
}
return res.String(), nil
}
func outputBlockDeposits(ctx context.Context, verbose bool, deposits []*phase0.Deposit) (string, error) {
res := strings.Builder{}
// Deposits.
res.WriteString(fmt.Sprintf("Deposits: %d\n", len(deposits)))
if verbose {
for i, deposit := range deposits {
data := deposit.Data
res.WriteString(fmt.Sprintf(" %d:\n", i))
res.WriteString(fmt.Sprintf(" Public key: %#x\n", data.PublicKey))
res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(data.Amount), true)))
res.WriteString(fmt.Sprintf(" Withdrawal credentials: %#x\n", data.WithdrawalCredentials))
res.WriteString(fmt.Sprintf(" Signature: %#x\n", data.Signature))
}
}
return res.String(), nil
}
func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Service, verbose bool, voluntaryExits []*phase0.SignedVoluntaryExit) (string, error) {
res := strings.Builder{}
res.WriteString(fmt.Sprintf("Voluntary exits: %d\n", len(voluntaryExits)))
if verbose {
for i, voluntaryExit := range voluntaryExits {
res.WriteString(fmt.Sprintf(" %d:\n", i))
validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []phase0.ValidatorIndex{voluntaryExit.Message.ValidatorIndex})
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err))
} else {
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[0].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch))
}
}
}
return res.String(), nil
}
func outputBlockSyncAggregate(ctx context.Context, eth2Client eth2client.Service, verbose bool, syncAggregate *altair.SyncAggregate, epoch phase0.Epoch) (string, error) {
res := strings.Builder{}
res.WriteString("Sync aggregate: ")
res.WriteString(fmt.Sprintf("%d/%d\n", syncAggregate.SyncCommitteeBits.Count(), syncAggregate.SyncCommitteeBits.Len()))
if verbose {
specProvider, isProvider := eth2Client.(eth2client.SpecProvider)
if isProvider {
config, err := specProvider.Spec(ctx)
if err == nil {
slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64)
res.WriteString(" Contributions: ")
res.WriteString(bitvectorToString(syncAggregate.SyncCommitteeBits))
res.WriteString("\n")
syncCommitteesProvider, isProvider := eth2Client.(eth2client.SyncCommitteesProvider)
if isProvider {
syncCommittee, err := syncCommitteesProvider.SyncCommittee(ctx, fmt.Sprintf("%d", uint64(epoch)*slotsPerEpoch))
if err != nil {
res.WriteString(fmt.Sprintf(" Error: failed to obtain sync committee: %v\n", err))
} else {
res.WriteString(" Contributing validators:")
for i := uint64(0); i < syncAggregate.SyncCommitteeBits.Len(); i++ {
if syncAggregate.SyncCommitteeBits.BitAt(i) {
res.WriteString(fmt.Sprintf(" %d", syncCommittee.Validators[i]))
}
}
res.WriteString("\n")
}
}
}
}
}
return res.String(), nil
}
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate body root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Sync aggregate.
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputAltairBlockText(ctx context.Context, data *dataOut, signedBlock *altair.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate body root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Sync aggregate.
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputPhase0BlockText(ctx context.Context, data *dataOut, signedBlock *phase0.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate block root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputBlockExecutionPayload(ctx context.Context,
verbose bool,
payload *bellatrix.ExecutionPayload,
) (
string,
error,
) {
if payload == nil {
return "", nil
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString("Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString("Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
} else {
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
baseFeePerGasBEBytes := make([]byte, len(payload.BaseFeePerGas))
for i := 0; i < 32; i++ {
baseFeePerGasBEBytes[i] = payload.BaseFeePerGas[32-1-i]
}
baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBEBytes)
res.WriteString(" Base fee per gas: ")
res.WriteString(string2eth.WeiToString(baseFeePerGas, true))
res.WriteString("\n Block hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
res.WriteString(" Timestamp: ")
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
res.WriteString(" Prev RANDAO: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
res.WriteString(" Receipts root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
res.WriteString(" Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
}
return res.String(), nil
}
// intersection returns a list of items common between the two sets.
func intersection(set1 []uint64, set2 []uint64) []phase0.ValidatorIndex {
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })
sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] })
res := make([]phase0.ValidatorIndex, 0)
set1Pos := 0
set2Pos := 0
for set1Pos < len(set1) && set2Pos < len(set2) {
switch {
case set1[set1Pos] < set2[set2Pos]:
set1Pos++
case set2[set2Pos] < set1[set1Pos]:
set2Pos++
default:
res = append(res, phase0.ValidatorIndex(set1[set1Pos]))
set1Pos++
set2Pos++
}
}
return res
}
func bitlistToString(input bitfield.Bitlist) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s✓", res)
} else {
res = fmt.Sprintf("%s✕", res)
}
if i%8 == 7 {
res = fmt.Sprintf("%s ", res)
}
}
return strings.TrimSpace(res)
}
func bitvectorToString(input bitfield.Bitvector512) string {
bits := int(input.Len())
res := strings.Builder{}
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res.WriteString("✓")
} else {
res.WriteString("✕")
}
if i%8 == 7 && i != bits-1 {
res.WriteString(" ")
}
}
return res.String()
}
func attestingIndices(input bitfield.Bitlist, indices []phase0.ValidatorIndex) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s%d ", res, indices[i])
}
}
return strings.TrimSpace(res)
}

View File

@@ -0,0 +1,177 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Good",
dataOut: &dataOut{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}
// func TestOutputBlockText(t *testing.T) {
// tests := []struct {
// name string
// dataOut *dataOut
// signedBeaconBlock *spec.SignedBeaconBlock
// err string
// }{
// {
// name: "Nil",
// err: "no data",
// },
// {
// name: "Good",
// dataOut: &dataOut{},
// },
// }
//
// for _, test := range tests {
// t.Run(test.name, func(t *testing.T) {
// res := outputBlockText(context.Background(), test.dataOut, test.signedBeaconBlock)
// if test.err != "" {
// require.EqualError(t, err, test.err)
// } else {
// require.NoError(t, err)
// require.Equal(t, test.res, res)
// }
// })
// }
// }
func TestOutputBlockDeposits(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
verbose bool
deposits []*spec.Deposit
res string
err string
}{
{
name: "Nil",
res: "Deposits: 0\n",
},
{
name: "Empty",
res: "Deposits: 0\n",
},
{
name: "Single",
deposits: []*spec.Deposit{
{
Data: &spec.DepositData{
PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
WithdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
Amount: spec.Gwei(32000000000),
Signature: testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
},
},
},
res: "Deposits: 1\n",
},
{
name: "SingleVerbose",
deposits: []*spec.Deposit{
{
Data: &spec.DepositData{
PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
WithdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"),
Amount: spec.Gwei(32000000000),
Signature: testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"),
},
},
},
verbose: true,
res: "Deposits: 1\n 0:\n Public key: 0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c\n Amount: 32 Ether\n Withdrawal credentials: 0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b\n Signature: 0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := outputBlockDeposits(context.Background(), test.verbose, test.deposits)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}
func TestOutputBlockETH1Data(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
verbose bool
eth1Data *spec.ETH1Data
res string
err string
}{
{
name: "Good",
eth1Data: &spec.ETH1Data{
DepositRoot: testutil.HexToRoot("0x92407b66d7daf4f30beb84820caae2cbba51add1c4648584101ff3c32151eb83"),
DepositCount: 109936,
BlockHash: testutil.HexToBytes("0x77b03ebaf0f2835b491cbd99a7f4649a03a6e7999678603030a014a3c48b32a4"),
},
res: "Ethereum 1 deposit count: 109936\nEthereum 1 deposit root: 0x92407b66d7daf4f30beb84820caae2cbba51add1c4648584101ff3c32151eb83\nEthereum 1 block hash: 0x77b03ebaf0f2835b491cbd99a7f4649a03a6e7999678603030a014a3c48b32a4\n",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := outputBlockETH1Data(context.Background(), test.eth1Data)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

215
cmd/block/info/process.go Normal file
View File

@@ -0,0 +1,215 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"encoding/json"
"fmt"
"time"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
var jsonOutput bool
var sszOutput bool
var results *dataOut
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
results = &dataOut{
debug: data.debug,
verbose: data.verbose,
eth2Client: data.eth2Client,
}
config, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
}
genesis, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
}
results.genesisTime = genesis.GenesisTime
results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration)
results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64)
signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain beacon block")
}
if signedBlock == nil {
return nil, errors.New("empty beacon block")
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(ctx, data.jsonOutput, signedBlock.Phase0); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionAltair:
if err := outputAltairBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Altair); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Bellatrix); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
return nil, errors.New("unknown block version")
}
if data.stream {
jsonOutput = data.jsonOutput
sszOutput = data.sszOutput
if !jsonOutput && !sszOutput {
fmt.Println("")
}
err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler)
if err != nil {
return nil, errors.Wrap(err, "failed to start block stream")
}
<-ctx.Done()
}
return &dataOut{}, nil
}
func headEventHandler(event *api.Event) {
// 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)
if err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to obtain block: %v\n", err)
}
return
}
if signedBlock == nil {
if !jsonOutput && !sszOutput {
fmt.Println("Empty beacon block")
}
return
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
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
}
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
default:
if !jsonOutput && !sszOutput {
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
}
return
}
if !jsonOutput && !sszOutput {
fmt.Println("")
}
}
func outputPhase0Block(ctx context.Context, jsonOutput bool, signedBlock *phase0.SignedBeaconBlock) error {
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))
default:
data, err := outputPhase0BlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Print(data)
}
return nil
}
func outputAltairBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *altair.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
case sszOutput:
data, err := signedBlock.MarshalSSZ()
if err != nil {
return errors.Wrap(err, "failed to generate SSZ")
}
fmt.Printf("%x\n", data)
default:
data, err := outputAltairBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Print(data)
}
return nil
}
func outputBellatrixBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *bellatrix.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
case sszOutput:
data, err := signedBlock.MarshalSSZ()
if err != nil {
return errors.Wrap(err, "failed to generate SSZ")
}
fmt.Printf("%x\n", data)
default:
data, err := outputBellatrixBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Print(data)
}
return nil
}

View File

@@ -0,0 +1,64 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package blockinfo
import (
"context"
"os"
"testing"
"github.com/attestantio/go-eth2-client/auto"
"github.com/rs/zerolog"
"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")
}
eth2Client, err := auto.New(context.Background(),
auto.WithLogLevel(zerolog.Disabled),
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
)
require.NoError(t, err)
tests := []struct {
name string
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Client",
dataIn: &dataIn{
eth2Client: eth2Client,
},
err: "empty beacon block",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}

50
cmd/block/info/run.go Normal file
View File

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

65
cmd/blockanalyze.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright © 2022 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
blockanalyze "github.com/wealdtech/ethdo/cmd/block/analyze"
)
var blockAnalyzeCmd = &cobra.Command{
Use: "analyze",
Short: "Analyze a block",
Long: `Analyze the contents of a block. For example:
ethdo block analyze --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := blockanalyze.Run(cmd)
if err != nil {
return err
}
if viper.GetBool("quiet") {
return nil
}
if res != "" {
fmt.Print(res)
}
return nil
},
}
func init() {
blockCmd.AddCommand(blockAnalyzeCmd)
blockFlags(blockAnalyzeCmd)
blockAnalyzeCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockAnalyzeCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockAnalyzeCmd.Flags().Bool("json", false, "output data in JSON format")
}
func blockAnalyzeBindings() {
if err := viper.BindPFlag("blockid", blockAnalyzeCmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockAnalyzeCmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockAnalyzeCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
}

View File

@@ -14,270 +14,56 @@
package cmd
import (
"bytes"
"encoding/hex"
"fmt"
"os"
"sort"
"strings"
"time"
"unicode/utf8"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/prysmaticlabs/go-bitfield"
"github.com/prysmaticlabs/go-ssz"
"github.com/spf13/cobra"
"github.com/wealdtech/ethdo/grpc"
string2eth "github.com/wealdtech/go-string2eth"
"github.com/spf13/viper"
blockinfo "github.com/wealdtech/ethdo/cmd/block/info"
)
var blockInfoSlot int64
var blockInfoStream bool
var blockInfoCmd = &cobra.Command{
Use: "info",
Short: "Obtain information about a block",
Long: `Obtain information about a block. For example:
ethdo block info --slot=12345
ethdo block info --blockid=12345
In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
err := connect()
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain block")
config, err := grpc.FetchChainConfig(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain configuration")
slotsPerEpoch := config["SlotsPerEpoch"].(uint64)
secondsPerSlot := config["SecondsPerSlot"].(uint64)
genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain genesis")
assert(blockInfoStream || blockInfoSlot != 0, "--slot or --stream is required")
assert(!blockInfoStream || blockInfoSlot == -1, "--slot and --stream are not supported together")
var slot uint64
if blockInfoSlot < 0 {
slot, err = grpc.FetchLatestFilledSlot(eth2GRPCConn)
errCheck(err, "Failed to obtain slot of latest block")
} else {
slot = uint64(blockInfoSlot)
RunE: func(cmd *cobra.Command, args []string) error {
res, err := blockinfo.Run(cmd)
if err != nil {
return err
}
assert(slot > 0, "slot must be greater than 0")
signedBlock, err := grpc.FetchBlock(eth2GRPCConn, slot)
errCheck(err, "Failed to obtain block")
if signedBlock == nil {
outputIf(!quiet, "No block at that slot")
os.Exit(_exitFailure)
if viper.GetBool("quiet") {
return nil
}
outputBlock(signedBlock, genesisTime, secondsPerSlot, slotsPerEpoch)
if blockInfoStream {
stream, err := grpc.StreamBlocks(eth2GRPCConn)
errCheck(err, "Failed to obtain block stream")
for {
fmt.Println()
signedBlock, err := stream.Recv()
errCheck(err, "Failed to obtain block")
if signedBlock != nil {
outputBlock(signedBlock, genesisTime, secondsPerSlot, slotsPerEpoch)
}
}
if res != "" {
fmt.Println(res)
}
os.Exit(_exitSuccess)
return nil
},
}
func outputBlock(signedBlock *ethpb.SignedBeaconBlock, genesisTime time.Time, secondsPerSlot uint64, slotsPerEpoch uint64) {
block := signedBlock.Block
body := block.Body
// General info.
bodyRoot, err := ssz.HashTreeRoot(block)
errCheck(err, "Failed to calculate block body root")
fmt.Printf("Slot: %d\n", block.Slot)
fmt.Printf("Epoch: %d\n", block.Slot/slotsPerEpoch)
fmt.Printf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(block.Slot*secondsPerSlot), 0))
fmt.Printf("Block root: %#x\n", bodyRoot)
outputIf(verbose, fmt.Sprintf("Parent root: %#x", block.ParentRoot))
outputIf(verbose, fmt.Sprintf("State root: %#x", block.StateRoot))
if len(body.Graffiti) > 0 && hex.EncodeToString(body.Graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" {
if utf8.Valid(body.Graffiti) {
fmt.Printf("Graffiti: %s\n", string(body.Graffiti))
} else {
fmt.Printf("Graffiti: %#x\n", body.Graffiti)
}
}
// Eth1 data.
eth1Data := body.Eth1Data
outputIf(verbose, fmt.Sprintf("Ethereum 1 deposit count: %d", eth1Data.DepositCount))
outputIf(verbose, fmt.Sprintf("Ethereum 1 deposit root: %#x", eth1Data.DepositRoot))
outputIf(verbose, fmt.Sprintf("Ethereum 1 block hash: %#x", eth1Data.BlockHash))
validatorCommittees := make(map[uint64][][]uint64)
// Attestations.
fmt.Printf("Attestations: %d\n", len(body.Attestations))
if verbose {
for i, att := range body.Attestations {
fmt.Printf("\t%d:\n", i)
// Fetch committees for this epoch if not already obtained.
committees, exists := validatorCommittees[att.Data.Slot]
if !exists {
attestationEpoch := att.Data.Slot / slotsPerEpoch
epochCommittees, err := grpc.FetchValidatorCommittees(eth2GRPCConn, attestationEpoch)
errCheck(err, "Failed to obtain committees")
for k, v := range epochCommittees {
validatorCommittees[k] = v
}
committees = validatorCommittees[att.Data.Slot]
}
fmt.Printf("\t\tCommittee index: %d\n", att.Data.CommitteeIndex)
fmt.Printf("\t\tAttesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len())
fmt.Printf("\t\tAggregation bits: %s\n", bitsToString(att.AggregationBits))
fmt.Printf("\t\tAttesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.CommitteeIndex]))
fmt.Printf("\t\tSlot: %d\n", att.Data.Slot)
fmt.Printf("\t\tBeacon block root: %#x\n", att.Data.BeaconBlockRoot)
fmt.Printf("\t\tSource epoch: %d\n", att.Data.Source.Epoch)
fmt.Printf("\t\tSource root: %#x\n", att.Data.Source.Root)
fmt.Printf("\t\tTarget epoch: %d\n", att.Data.Target.Epoch)
fmt.Printf("\t\tTarget root: %#x\n", att.Data.Target.Root)
}
}
// Attester slashings.
fmt.Printf("Attester slashings: %d\n", len(body.AttesterSlashings))
if verbose {
for i, slashing := range body.AttesterSlashings {
// Say what was slashed.
att1 := slashing.Attestation_1
outputIf(debug, fmt.Sprintf("Attestation 1 attesting indices are %v", att1.AttestingIndices))
att2 := slashing.Attestation_2
outputIf(debug, fmt.Sprintf("Attestation 2 attesting indices are %v", att2.AttestingIndices))
slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices)
if len(slashedIndices) == 0 {
continue
}
fmt.Printf("\t%d:\n", i)
fmt.Println("\t\tSlashed validators:")
for _, slashedIndex := range slashedIndices {
validator, err := grpc.FetchValidatorByIndex(eth2GRPCConn, slashedIndex)
errCheck(err, "Failed to obtain validator information")
fmt.Printf("\t\t\t%#x (%d)\n", validator.PublicKey, slashedIndex)
}
// Say what caused the slashing.
if att1.Data.Target.Epoch == att2.Data.Target.Epoch {
fmt.Printf("\t\tDouble voted for same target epoch (%d):\n", att1.Data.Target.Epoch)
if !bytes.Equal(att1.Data.Target.Root, att2.Data.Target.Root) {
fmt.Printf("\t\t\tAttestation 1 target epoch root: %#x\n", att1.Data.Target.Root)
fmt.Printf("\t\t\tAttestation 2target epoch root: %#x\n", att2.Data.Target.Root)
}
if !bytes.Equal(att1.Data.BeaconBlockRoot, att2.Data.BeaconBlockRoot) {
fmt.Printf("\t\t\tAttestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot)
fmt.Printf("\t\t\tAttestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot)
}
} else if att1.Data.Source.Epoch < att2.Data.Source.Epoch &&
att1.Data.Target.Epoch > att2.Data.Target.Epoch {
fmt.Printf("\t\tSurround voted:\n")
fmt.Printf("\t\t\tAttestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch)
fmt.Printf("\t\t\tAttestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch)
}
}
}
fmt.Printf("Proposer slashings: %d\n", len(body.ProposerSlashings))
// TODO verbose proposer slashings.
// Deposits.
fmt.Printf("Deposits: %d\n", len(body.Deposits))
if verbose {
for i, deposit := range body.Deposits {
data := deposit.Data
fmt.Printf("\t%d:\n", i)
fmt.Printf("\t\tPublic key: %#x\n", data.PublicKey)
fmt.Printf("\t\tAmount: %s\n", string2eth.GWeiToString(data.Amount, true))
fmt.Printf("\t\tWithdrawal credentials: %#x\n", data.WithdrawalCredentials)
fmt.Printf("\t\tSignature: %#x\n", data.Signature)
}
}
// Voluntary exits.
fmt.Printf("Voluntary exits: %d\n", len(body.VoluntaryExits))
if verbose {
for i, voluntaryExit := range body.VoluntaryExits {
fmt.Printf("\t%d:\n", i)
validator, err := grpc.FetchValidatorByIndex(eth2GRPCConn, voluntaryExit.Exit.ValidatorIndex)
errCheck(err, "Failed to obtain validator information")
fmt.Printf("\t\tValidator: %#x (%d)\n", validator.PublicKey, voluntaryExit.Exit.ValidatorIndex)
fmt.Printf("\t\tEpoch: %d\n", voluntaryExit.Exit.Epoch)
}
}
}
// intersection returns a list of items common between the two sets.
func intersection(set1 []uint64, set2 []uint64) []uint64 {
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })
sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] })
res := make([]uint64, 0)
set1Pos := 0
set2Pos := 0
for set1Pos < len(set1) && set2Pos < len(set2) {
switch {
case set1[set1Pos] < set2[set2Pos]:
set1Pos++
case set2[set2Pos] < set1[set1Pos]:
set2Pos++
default:
res = append(res, set1[set1Pos])
set1Pos++
set2Pos++
}
}
return res
}
func bitsToString(input bitfield.Bitlist) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s✓", res)
} else {
res = fmt.Sprintf("%s✕", res)
}
if i%8 == 7 {
res = fmt.Sprintf("%s ", res)
}
}
return strings.TrimSpace(res)
}
func attestingIndices(input bitfield.Bitlist, indices []uint64) string {
bits := int(input.Len())
res := ""
for i := 0; i < bits; i++ {
if input.BitAt(uint64(i)) {
res = fmt.Sprintf("%s%d ", res, indices[i])
}
}
return strings.TrimSpace(res)
}
func init() {
blockCmd.AddCommand(blockInfoCmd)
blockFlags(blockInfoCmd)
blockInfoCmd.Flags().Int64Var(&blockInfoSlot, "slot", -1, "the latest slot with a block")
blockInfoCmd.Flags().BoolVar(&blockInfoStream, "stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
}
func blockInfoBindings() {
if err := viper.BindPFlag("blockid", blockInfoCmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockInfoCmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
if err := viper.BindPFlag("ssz", blockInfoCmd.Flags().Lookup("ssz")); err != nil {
panic(err)
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,165 @@
// Copyright © 2022 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaineth1votes
import (
"context"
"encoding/json"
"fmt"
"strconv"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/util"
)
func (c *command) process(ctx context.Context) error {
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
var err error
if c.xperiod != "" {
period, err := strconv.ParseUint(c.xperiod, 10, 64)
if err != nil {
return err
}
c.epoch = phase0.Epoch(c.epochsPerEth1VotingPeriod*(period+1)) - 1
} else {
c.epoch, err = util.ParseEpoch(ctx, c.chainTime, c.xepoch)
if err != nil {
return err
}
}
// Do not fetch from the future.
if c.epoch > c.chainTime.CurrentEpoch() {
c.epoch = c.chainTime.CurrentEpoch()
}
// Need to fetch the state from the last slot of the epoch.
fetchSlot := c.chainTime.FirstSlotOfEpoch(c.epoch+1) - 1
// Do not fetch from the future.
if fetchSlot > c.chainTime.CurrentSlot() {
fetchSlot = c.chainTime.CurrentSlot()
}
state, err := c.beaconStateProvider.BeaconState(ctx, fmt.Sprintf("%d", fetchSlot))
if err != nil {
return errors.Wrap(err, "failed to obtain state")
}
if state == nil {
return errors.New("state not returned by beacon node")
}
if c.debug {
data, err := json.Marshal(state)
if err == nil {
fmt.Printf("%s\n", string(data))
}
}
switch state.Version {
case spec.DataVersionPhase0:
c.slot = phase0.Slot(state.Phase0.Slot)
c.incumbent = state.Phase0.ETH1Data
c.eth1DataVotes = state.Phase0.ETH1DataVotes
case spec.DataVersionAltair:
c.slot = state.Altair.Slot
c.incumbent = state.Altair.ETH1Data
c.eth1DataVotes = state.Altair.ETH1DataVotes
case spec.DataVersionBellatrix:
c.slot = state.Bellatrix.Slot
c.incumbent = state.Bellatrix.ETH1Data
c.eth1DataVotes = state.Bellatrix.ETH1DataVotes
case spec.DataVersionCapella:
c.slot = state.Capella.Slot
c.incumbent = state.Capella.ETH1Data
c.eth1DataVotes = state.Capella.ETH1DataVotes
default:
return fmt.Errorf("unhandled beacon state version %v", state.Version)
}
c.period = uint64(c.epoch) / c.epochsPerEth1VotingPeriod
c.votes = make(map[string]*vote)
for _, eth1Vote := range c.eth1DataVotes {
key := fmt.Sprintf("%#x:%d", eth1Vote.BlockHash, eth1Vote.DepositCount)
if _, exists := c.votes[key]; !exists {
c.votes[key] = &vote{
Vote: eth1Vote,
}
}
c.votes[key].Count++
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to set up chaintime service")
}
var isProvider bool
c.beaconStateProvider, isProvider = c.eth2Client.(eth2client.BeaconStateProvider)
if !isProvider {
return errors.New("connection does not provide beacon state")
}
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
if !isProvider {
return errors.New("connection does not provide spec information")
}
spec, err := specProvider.Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["SLOTS_PER_EPOCH"]
if !exists {
return errors.New("spec did not contain SLOTS_PER_EPOCH")
}
var good bool
c.slotsPerEpoch, good = tmp.(uint64)
if !good {
return errors.New("SLOTS_PER_EPOCH value invalid")
}
tmp, exists = spec["EPOCHS_PER_ETH1_VOTING_PERIOD"]
if !exists {
return errors.New("spec did not contain EPOCHS_PER_ETH1_VOTING_PERIOD")
}
c.epochsPerEth1VotingPeriod, good = tmp.(uint64)
if !good {
return errors.New("EPOCHS_PER_ETH1_VOTING_PERIOD value invalid")
}
return nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

78
cmd/chain/time/input.go Normal file
View File

@@ -0,0 +1,78 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
json bool
// Input
connection string
allowInsecureConnections bool
timestamp string
slot string
epoch string
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
data.json = viper.GetBool("json")
haveInput := false
if viper.GetString("timestamp") != "" {
data.timestamp = viper.GetString("timestamp")
haveInput = true
}
if viper.GetString("slot") != "" {
if haveInput {
return nil, errors.New("only one of timestamp, slot and epoch allowed")
}
data.slot = viper.GetString("slot")
haveInput = true
}
if viper.GetString("epoch") != "" {
if haveInput {
return nil, errors.New("only one of timestamp, slot and epoch allowed")
}
data.epoch = viper.GetString("epoch")
haveInput = true
}
if !haveInput {
return nil, errors.New("one of timestamp, slot or epoch required")
}
data.connection = viper.GetString("connection")
data.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return data, nil
}

View File

@@ -0,0 +1,89 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"os"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{},
err: "timeout is required",
},
{
name: "IDMissing",
vars: map[string]interface{}{
"timeout": "5s",
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
},
err: "one of timestamp, slot or epoch required",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
}
})
}
}

82
cmd/chain/time/output.go Normal file
View File

@@ -0,0 +1,82 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"fmt"
"strings"
"time"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type dataOut struct {
debug bool
quiet bool
verbose bool
epoch spec.Epoch
epochStart time.Time
epochEnd time.Time
slot spec.Slot
slotStart time.Time
slotEnd time.Time
syncCommitteePeriod uint64
syncCommitteePeriodStart time.Time
syncCommitteePeriodEpochStart spec.Epoch
syncCommitteePeriodEnd time.Time
syncCommitteePeriodEpochEnd spec.Epoch
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
if data.quiet {
return "", nil
}
builder := strings.Builder{}
builder.WriteString("Epoch ")
builder.WriteString(fmt.Sprintf("%d", data.epoch))
builder.WriteString("\n Epoch start ")
builder.WriteString(data.epochStart.Format("2006-01-02 15:04:05"))
builder.WriteString("\n Epoch end ")
builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05"))
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"))
builder.WriteString("\n Slot end ")
builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05"))
builder.WriteString("\nSync committee period ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod))
builder.WriteString("\n Sync committee period start ")
builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart))
builder.WriteString(")\n Sync committee period end ")
builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05"))
builder.WriteString(" (epoch ")
builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd))
builder.WriteString(")\n")
return builder.String(), nil
}

View File

@@ -0,0 +1,85 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
// import (
// "context"
// "testing"
//
// api "github.com/attestantio/go-eth2-client/api/v1"
// "github.com/stretchr/testify/require"
// "github.com/wealdtech/ethdo/testutil"
// )
//
// func TestOutput(t *testing.T) {
// tests := []struct {
// name string
// dataOut *dataOut
// res string
// err string
// }{
// {
// name: "Nil",
// err: "no data",
// },
// {
// name: "Empty",
// dataOut: &dataOut{},
// res: "No duties found",
// },
// {
// name: "Present",
// dataOut: &dataOut{
// duty: &api.AttesterDuty{
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
// Slot: 1,
// ValidatorIndex: 2,
// CommitteeIndex: 3,
// CommitteeLength: 4,
// CommitteesAtSlot: 5,
// ValidatorCommitteeIndex: 6,
// },
// },
// res: "Validator attesting in slot 1 committee 3",
// },
// {
// name: "JSON",
// dataOut: &dataOut{
// json: true,
// duty: &api.AttesterDuty{
// PubKey: testutil.HexToPubKey("0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"),
// Slot: 1,
// ValidatorIndex: 2,
// CommitteeIndex: 3,
// CommitteeLength: 4,
// CommitteesAtSlot: 5,
// ValidatorCommitteeIndex: 6,
// },
// },
// res: `{"pubkey":"0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95","slot":"1","validator_index":"2","committee_index":"3","committee_length":"4","committees_at_slot":"5","validator_committee_index":"6"}`,
// },
// }
//
// for _, test := range tests {
// t.Run(test.name, func(t *testing.T) {
// res, err := output(context.Background(), test.dataOut)
// if test.err != "" {
// require.EqualError(t, err, test.err)
// } else {
// require.NoError(t, err)
// require.Equal(t, test.res, res)
// }
// })
// }
// }

95
cmd/chain/time/process.go Normal file
View File

@@ -0,0 +1,95 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"strconv"
"time"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
eth2Client, err := util.ConnectToBeaconNode(ctx, data.connection, data.timeout, data.allowInsecureConnections)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx)
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")
}
results := &dataOut{
debug: data.debug,
quiet: data.quiet,
verbose: data.verbose,
}
// Calculate the slot given the input.
switch {
case data.slot != "":
slot, err := strconv.ParseUint(data.slot, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse slot")
}
results.slot = phase0.Slot(slot)
case data.epoch != "":
epoch, err := strconv.ParseUint(data.epoch, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse epoch")
}
results.slot = phase0.Slot(epoch * slotsPerEpoch)
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)
}
// 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)
return results, nil
}

View File

@@ -0,0 +1,120 @@
// Copyright © 2021 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chaintime
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
tests := []struct {
name string
dataIn *dataIn
expected *dataOut
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "Slot",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
slot: "1",
},
expected: &dataOut{
epochStart: time.Unix(1606824023, 0),
epochEnd: time.Unix(1606824407, 0),
slot: 1,
slotStart: time.Unix(1606824035, 0),
slotEnd: time.Unix(1606824047, 0),
syncCommitteePeriod: 0,
syncCommitteePeriodStart: time.Unix(1606824023, 0),
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
syncCommitteePeriodEpochStart: 0,
syncCommitteePeriodEpochEnd: 255,
},
},
{
name: "Epoch",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
epoch: "2",
},
expected: &dataOut{
epoch: 2,
epochStart: time.Unix(1606824791, 0),
epochEnd: time.Unix(1606825175, 0),
slot: 64,
slotStart: time.Unix(1606824791, 0),
slotEnd: time.Unix(1606824803, 0),
syncCommitteePeriod: 0,
syncCommitteePeriodStart: time.Unix(1606824023, 0),
syncCommitteePeriodEnd: time.Unix(1606921943, 0),
syncCommitteePeriodEpochStart: 0,
syncCommitteePeriodEpochEnd: 255,
},
},
{
name: "Timestamp",
dataIn: &dataIn{
connection: os.Getenv("ETHDO_TEST_CONNECTION"),
timeout: 10 * time.Second,
allowInsecureConnections: true,
timestamp: "2021-01-01T00:00:00+0000",
},
expected: &dataOut{
epoch: 6862,
epochStart: time.Unix(1609459031, 0),
epochEnd: time.Unix(1609459415, 0),
slot: 219598,
slotStart: time.Unix(1609459199, 0),
slotEnd: time.Unix(1609459211, 0),
syncCommitteePeriod: 26,
syncCommitteePeriodStart: time.Unix(1609379927, 0),
syncCommitteePeriodEnd: time.Unix(1609477847, 0),
syncCommitteePeriodEpochStart: 6656,
syncCommitteePeriodEpochEnd: 6911,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
fmt.Printf("****** %d %d\n", res.syncCommitteePeriodStart.Unix(), res.syncCommitteePeriodEnd.Unix())
require.Equal(t, test.expected, res)
}
})
}
}

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

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

View File

@@ -0,0 +1,83 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainverifysignedcontributionandproof
import (
"context"
"time"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/pkg/errors"
"github.com/spf13/viper"
)
type command struct {
quiet bool
verbose bool
debug bool
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Input.
data string
item *altair.SignedContributionAndProof
// Data access.
eth2Client eth2client.Service
validatorsProvider eth2client.ValidatorsProvider
// Data.
spec map[string]interface{}
validator *api.Validator
syncCommittee *api.SyncCommittee
// Output.
itemStructureValid bool
validatorKnown bool
validatorInSyncCommittee bool
validatorIsAggregator bool
contributionSignatureValidFormat bool
contributionAndProofSignatureValidFormat bool
contributionAndProofSignatureValid bool
additionalInfo string
}
func newCommand(ctx context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
}
// Timeout.
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
c.timeout = viper.GetDuration("timeout")
if viper.GetString("data") == "" {
return nil, errors.New("data is required")
}
c.data = viper.GetString("data")
c.connection = viper.GetString("connection")
c.allowInsecureConnections = viper.GetBool("allow-insecure-connections")
return c, nil
}

View File

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

View File

@@ -0,0 +1,127 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainverifysignedcontributionandproof
import (
"context"
"strings"
)
func (c *command) output(ctx context.Context) (string, error) {
if c.quiet {
return "", nil
}
builder := strings.Builder{}
builder.WriteString("Valid data structure: ")
if c.itemStructureValid {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Validator known: ")
if c.validatorKnown {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Validator in sync committee: ")
if c.validatorInSyncCommittee {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Validator is aggregator: ")
if c.validatorIsAggregator {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Contribution signature has valid format: ")
if c.contributionSignatureValidFormat {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Contribution and proof signature has valid format: ")
if c.contributionAndProofSignatureValidFormat {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
builder.WriteString("Contribution and proof signature is valid: ")
if c.contributionAndProofSignatureValid {
builder.WriteString("✓\n")
} else {
builder.WriteString("✕")
if c.additionalInfo != "" {
builder.WriteString(" (")
builder.WriteString(c.additionalInfo)
builder.WriteString(")")
}
builder.WriteString("\n")
return builder.String(), nil
}
return builder.String(), nil
}

View File

@@ -0,0 +1,303 @@
// Copyright © 2021 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package chainverifysignedcontributionandproof
import (
"context"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"fmt"
"os"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
)
func (c *command) process(ctx context.Context) error {
// Parse the data.
if c.data == "" {
return errors.New("no data supplied")
}
c.item = &altair.SignedContributionAndProof{}
err := json.Unmarshal([]byte(c.data), c.item)
if err != nil {
c.additionalInfo = err.Error()
return nil
}
c.itemStructureValid = true
// Obtain information we need to process.
if err := c.setup(ctx); err != nil {
return err
}
for _, validatorIndex := range c.syncCommittee.Validators {
if validatorIndex == c.item.Message.AggregatorIndex {
c.validatorInSyncCommittee = true
break
}
}
if !c.validatorInSyncCommittee {
return nil
}
// Ensure the validator is an aggregator.
isAggregator, err := c.isAggregator(ctx)
if err != nil {
return errors.Wrap(err, "failed to ascertain if sync committee member is aggregator")
}
if !isAggregator {
return nil
}
c.validatorIsAggregator = true
// Confirm the contribution signature.
if err := c.confirmContributionSignature(ctx); err != nil {
return errors.Wrap(err, "failed to confirm the contribution signature")
}
// Confirm the contribution and proof signature.
if err := c.confirmContributionAndProofSignature(ctx); err != nil {
return errors.Wrap(err, "failed to confirm the contribution and proof signature")
}
return nil
}
func (c *command) setup(ctx context.Context) error {
var err error
// Connect to the client.
c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to beacon node")
}
// Obtain the validator.
var isProvider bool
c.validatorsProvider, isProvider = c.eth2Client.(eth2client.ValidatorsProvider)
if !isProvider {
return errors.New("connection does not provide validator information")
}
stateID := fmt.Sprintf("%d", c.item.Message.Contribution.Slot)
validators, err := c.validatorsProvider.Validators(ctx,
stateID,
[]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 {
return nil
}
c.validatorKnown = true
c.validator = validators[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)
if err != nil {
return errors.Wrap(err, "failed to obtain sync committee information")
}
return nil
}
// 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)
if err != nil {
return false, errors.Wrap(err, "failed to obtain spec information")
}
tmp, exists := c.spec["SYNC_COMMITTEE_SIZE"]
if !exists {
return false, errors.New("spec does not contain SYNC_COMMITTEE_SIZE")
}
if _, isUint64 := tmp.(uint64); !isUint64 {
return false, errors.New("spec returned non-integer value for SYNC_COMMITTEE_SIZE")
}
syncCommitteeSize := tmp.(uint64)
if c.debug {
fmt.Fprintf(os.Stderr, "sync committee size is %d\n", syncCommitteeSize)
}
tmp, exists = c.spec["SYNC_COMMITTEE_SUBNET_COUNT"]
if !exists {
return false, errors.New("spec does not contain SYNC_COMMITTEE_SUBNET_COUNT")
}
if _, isUint64 := tmp.(uint64); !isUint64 {
return false, errors.New("spec returned non-integer value for SYNC_COMMITTEE_SUBNET_COUNT")
}
syncCommitteeSubnetCount := tmp.(uint64)
if c.debug {
fmt.Fprintf(os.Stderr, "sync committee subnet count is %d\n", syncCommitteeSubnetCount)
}
tmp, exists = c.spec["TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE"]
if !exists {
return false, errors.New("spec does not contain TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE")
}
if _, isUint64 := tmp.(uint64); !isUint64 {
return false, errors.New("spec returned non-integer value for TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE")
}
targetAggregatorsPerSyncSubcommittee := tmp.(uint64)
if c.debug {
fmt.Fprintf(os.Stderr, "target aggregators per sync subcommittee is %d\n", targetAggregatorsPerSyncSubcommittee)
}
modulo := syncCommitteeSize / syncCommitteeSubnetCount / targetAggregatorsPerSyncSubcommittee
if modulo < 1 {
modulo = 1
}
if c.debug {
fmt.Fprintf(os.Stderr, "modulo is %d\n", modulo)
}
// Hash the selection proof.
sigHash := sha256.New()
n, err := sigHash.Write(c.item.Message.SelectionProof[:])
if err != nil {
return false, errors.Wrap(err, "failed to hash the selection proof")
}
if n != len(c.item.Signature[:]) {
return false, errors.New("failed to write all bytes of the selection proof to the hash")
}
hash := sigHash.Sum(nil)
if c.debug {
fmt.Fprintf(os.Stderr, "hash of selection proof is %#x\n", hash)
}
return binary.LittleEndian.Uint64(hash[:8])%modulo == 0, nil
}
func (c *command) confirmContributionSignature(ctx context.Context) error {
sigBytes := make([]byte, 96)
copy(sigBytes, c.item.Message.Contribution.Signature[:])
_, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
c.additionalInfo = err.Error()
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++ {
if c.item.Message.Contribution.AggregationBits.BitAt(i) {
includedIndices = append(includedIndices, subCommittee[int(i)])
}
}
if c.debug {
fmt.Fprintf(os.Stderr, "Contribution validator indices: %v (%d)\n", includedIndices, len(includedIndices))
}
includedValidators, err := c.validatorsProvider.Validators(ctx, "head", includedIndices)
if err != nil {
return errors.Wrap(err, "failed to obtain subcommittee validators")
}
if len(includedValidators) == 0 {
return errors.New("obtained empty subcommittee validator list")
}
var aggregatePubKey *e2types.BLSPublicKey
for _, v := range includedValidators {
pubKeyBytes := make([]byte, 48)
copy(pubKeyBytes, v.Validator.PublicKey[:])
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
if err != nil {
return errors.Wrap(err, "failed to aggregate public key")
}
if aggregatePubKey == nil {
aggregatePubKey = pubKey
} else {
aggregatePubKey.Aggregate(pubKey)
}
}
if c.debug {
fmt.Fprintf(os.Stderr, "Aggregate public key is %#x\n", aggregatePubKey.Marshal())
}
// Don't have the ability to carry out the batch verification at current.
return nil
}
func (c *command) confirmContributionAndProofSignature(ctx context.Context) error {
sigBytes := make([]byte, 96)
copy(sigBytes, c.item.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
c.additionalInfo = err.Error()
return nil
}
c.contributionAndProofSignatureValidFormat = true
pubKeyBytes := make([]byte, 48)
copy(pubKeyBytes, c.validator.Validator.PublicKey[:])
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
if err != nil {
return errors.Wrap(err, "failed to configure public key")
}
objectRoot, err := c.item.Message.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to obtain signging root")
}
tmp, exists := c.spec["DOMAIN_CONTRIBUTION_AND_PROOF"]
if !exists {
return errors.New("spec does not contain DOMAIN_CONTRIBUTION_AND_PROOF")
}
if _, isUint64 := tmp.(phase0.DomainType); !isUint64 {
return errors.New("spec returned non-domain type value for DOMAIN_CONTRIBUTION_AND_PROOF")
}
contributionAndProofDomainType := tmp.(phase0.DomainType)
if c.debug {
fmt.Fprintf(os.Stderr, "contribution and proof domain type is %#x\n", contributionAndProofDomainType)
}
domain, err := c.eth2Client.(eth2client.DomainProvider).Domain(ctx, contributionAndProofDomainType, phase0.Epoch(c.item.Message.Contribution.Slot/32))
if err != nil {
return errors.Wrap(err, "failed to obtain domain")
}
container := &phase0.SigningData{
ObjectRoot: objectRoot,
Domain: domain,
}
signingRoot, err := container.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to obtain signging root")
}
c.contributionAndProofSignatureValid = sig.Verify(signingRoot[:], pubKey)
return nil
}

View File

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

View File

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

67
cmd/chaineth1votes.go Normal file
View File

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

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