Compare commits

...

55 Commits

Author SHA1 Message Date
Jim McDonald
76b9010bc8 Update docs re public endpoint. 2023-02-21 22:12:38 +00:00
Jim McDonald
15d58e20d4 Use public endpoint if necessary.
Provide access to public endpoint if no other connection available.
2023-02-21 20:59:09 +00:00
Jim McDonald
ec9f5b8012 Show extra data as text if possible. 2023-02-19 21:50:36 +00:00
Jim McDonald
5c907bb8f8 Allow import of accounts with null name.
Fixes #59.
2023-02-18 22:40:45 +00:00
Jim McDonald
5e2cf9697c Merge pull request #61 from ladidan/master
fixing typo
2023-02-16 19:22:18 +00:00
Jim McDonald
7d3201826d Merge pull request #63 from infosecual/ethdo_bls_tests
BLSToExecutionChange message generation error reporting and tests
2023-02-16 18:58:58 +00:00
David Theodore
5e0a341eb1 fixed linting issues and failing tests 2023-02-16 11:36:02 -06:00
Jim McDonald
954a972a36 Update dependencies. 2023-02-16 13:36:34 +00:00
Jim McDonald
c0dd5dcfc6 Update testing. 2023-02-16 13:36:26 +00:00
Jim McDonald
fd394e3475 Reduce code duplication. 2023-02-16 13:35:51 +00:00
Jim McDonald
5dcdf9c11f Handle account-based validator exit correctly. 2023-02-16 13:20:11 +00:00
David Theodore
14f559ab8b Merge branch 'wealdtech:master' into ethdo_bls_tests 2023-02-13 11:10:24 -06:00
infosecual
6bb79f821c more linting 2023-02-13 11:08:42 -06:00
infosecual
6dcd3c9978 Addressed issues for PR63 2023-02-13 10:51:06 -06:00
Jim McDonald
d5acd2f842 Update workflow. 2023-02-12 23:15:51 +00:00
Jim McDonald
1395b7159f Linting. 2023-02-12 21:22:39 +00:00
David Theodore
548442c33b renamed some tests 2023-02-12 12:59:01 -06:00
David Theodore
f8f7eb26e8 added/removed comments 2023-02-12 12:52:18 -06:00
David Theodore
ca10ba7411 generateOperationsFromPrivateKey errs and test++ 2023-02-10 14:47:13 -06:00
David Theodore
d8ccf67be8 spelling error in comments 2023-02-10 13:43:55 -06:00
David Theodore
8fba19597e more err reporting and tests 2023-02-10 13:42:25 -06:00
David Theodore
c034dfaf53 Merge branch 'wealdtech:master' into ethdo_bls_tests 2023-02-09 21:09:03 -06:00
David Theodore
4576978347 add tesing coverage for generateOperationsFromMnem 2023-02-09 21:07:56 -06:00
Jim McDonald
97c409fde6 Linting. 2023-02-09 19:40:19 +00:00
Jim McDonald
5b2e62c29e Error if no credential change operations generated. 2023-02-09 19:37:26 +00:00
David Theodore
c7025d99dd generateOperationFromMnemonicAndValidator err++ 2023-02-09 11:42:55 -06:00
David Theodore
111f5bf627 TestGenerateOperationFromMnemonicAndValidator++ 2023-02-08 17:07:15 -06:00
David Theodore
7de8ad8a59 Merge branch 'ethdo_bls_tests' of github.com:infosecual/ethdo into ethdo_bls_tests 2023-02-08 16:16:27 -06:00
David Theodore
31336dd5ce added to TestGenerateOperationFromMnemonicAndPath 2023-02-08 16:16:00 -06:00
ladidan
47104b31a4 Update validatorexit.go 2023-02-08 18:41:30 +01:00
Jim McDonald
59200e796a Fix issue obtaining capella epoch. 2023-02-02 09:28:44 +00:00
Jim McDonald
c91538644f Bump version. 2023-01-31 15:36:16 +00:00
Jim McDonald
3140fc5b8a Merge pull request #56 from joaocenoura/master
Allow ethdo credential set to use all validators from offline-preparation.json
2023-01-31 15:34:42 +00:00
Jim McDonald
a2afd37a97 Provide better error messages when offline preparation file cannot be read. 2023-01-31 15:25:40 +00:00
Joao Rodrigues
d453ba9303 post review changes: moving code outside for loop 2023-01-31 13:32:28 +00:00
Joao Rodrigues
d0b278c0ec removing return err 2023-01-31 11:17:28 +00:00
Joao Rodrigues
27a59c031b align comment with the rest of the code block 2023-01-30 17:45:02 +00:00
Joao Rodrigues
1bc139c591 post review changes 2023-01-30 17:40:37 +00:00
Jim McDonald
7e8db1cd2e Handle false positive linting issue. 2023-01-28 00:55:28 +00:00
Jim McDonald
864bb30244 Tweak linting. 2023-01-28 00:53:20 +00:00
Jim McDonald
4209f725ba Tweak linting. 2023-01-28 00:51:24 +00:00
Jim McDonald
d01e789c8a Downgrade golangci-lint. 2023-01-28 00:49:34 +00:00
Jim McDonald
85a0590d55 Linting. 2023-01-28 00:42:24 +00:00
Jim McDonald
b9ba1ec1c2 Linting. 2023-01-28 00:34:23 +00:00
Jim McDonald
29bffd0dbe Support additional word list languages.
Fixes #58
2023-01-27 23:28:36 +00:00
Jim McDonald
e7a2c600f1 Increase minimum timeout for some commands to 2 minutes. 2023-01-27 21:53:03 +00:00
Joao Rodrigues
f2a5a93195 document usage for changing withdrawal credentials with private key only 2023-01-27 12:54:46 +00:00
Jim McDonald
095c246efb Merge pull request #57 from fredriksvantes/patch-1
Fixing typo
2023-01-26 15:37:21 +00:00
Fredrik Svantes
581d22c7d7 Fixing typo
Changing "Not broadcasting credentials operations" to "Not broadcasting exit operation"
2023-01-26 13:48:39 +01:00
Joao Rodrigues
1ca1e1f2d6 skip validators from which validator credentials do not match supplied withdrawal credentials 2023-01-25 20:57:44 +00:00
Joao Rodrigues
7c88b7c082 Allow ethdo credential set to use all validators from offline-preparation.json 2023-01-25 19:33:11 +00:00
Jim McDonald
4d351e6d3d Update changelog 2023-01-25 06:55:55 +00:00
Jim McDonald
c46e9740d4 Merge pull request #55 from 0xTylerHolmes/master
Correct domain type for exits
2023-01-25 06:52:51 +00:00
z3n
f9cb1054c0 Correct domain type for exits 2023-01-24 17:08:43 -06:00
Jim McDonald
4e2fa63f30 Bump version. 2023-01-17 13:49:01 +00:00
72 changed files with 1066 additions and 191 deletions

View File

@@ -15,10 +15,9 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: '1.20'
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: --timeout=60m

View File

@@ -10,6 +10,6 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: '1.20'
- uses: actions/checkout@v3
- uses: n8maninger/action-golang-test@v1

170
.golangci.yml Normal file
View File

@@ -0,0 +1,170 @@
# This file contains all available configuration options
# with their default values (in comments).
#
# This file is not a configuration example,
# it contains the exhaustive configuration with explanations of the options.
# Options for analysis running.
run:
# The default concurrency value is the number of available CPU.
# concurrency: 4
# Timeout for analysis, e.g. 30s, 5m.
# Default: 1m
timeout: 10m
# Exit code when at least one issue was found.
# Default: 1
# issues-exit-code: 2
# Include test files or not.
# Default: true
tests: false
# List of build tags, all linters use it.
# Default: [].
# build-tags:
# - mytag
# Which dirs to skip: issues from them won't be reported.
# Can use regexp here: `generated.*`, regexp is applied on full path.
# Default value is empty list,
# but default dirs are skipped independently of this option's value (see skip-dirs-use-default).
# "/" will be replaced by current OS file path separator to properly work on Windows.
# skip-dirs:
# - autogenerated_by_my_lib
# Enables skipping of directories:
# - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
# Default: true
# skip-dirs-use-default: false
# Which files to skip: they will be analyzed, but issues from them won't be reported.
# Default value is empty list,
# but there is no need to include all autogenerated files,
# we confidently recognize autogenerated files.
# If it's not please let us know.
# "/" will be replaced by current OS file path separator to properly work on Windows.
skip-files:
- ".*_ssz\\.go$"
# If set we pass it to "go list -mod={option}". From "go help modules":
# If invoked with -mod=readonly, the go command is disallowed from the implicit
# automatic updating of go.mod described above. Instead, it fails when any changes
# to go.mod are needed. This setting is most useful to check that go.mod does
# not need updates, such as in a continuous integration and testing system.
# If invoked with -mod=vendor, the go command assumes that the vendor
# directory holds the correct copies of dependencies and ignores
# the dependency descriptions in go.mod.
#
# Allowed values: readonly|vendor|mod
# By default, it isn't set.
modules-download-mode: readonly
# Allow multiple parallel golangci-lint instances running.
# If false (default) - golangci-lint acquires file lock on start.
allow-parallel-runners: true
# Define the Go version limit.
# Mainly related to generics support since go1.18.
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18
go: '1.19'
# output configuration options
output:
# Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions
#
# Multiple can be specified by separating them by comma, output can be provided
# for each of them by separating format name and path by colon symbol.
# Output path can be either `stdout`, `stderr` or path to the file to write to.
# Example: "checkstyle:report.json,colored-line-number"
#
# Default: colored-line-number
# format: json
# Print lines of code with issue.
# Default: true
# print-issued-lines: false
# Print linter name in the end of issue text.
# Default: true
# print-linter-name: false
# Make issues output unique by line.
# Default: true
# uniq-by-line: false
# Add a prefix to the output file references.
# Default is no prefix.
# path-prefix: ""
# Sort results by: filepath, line and column.
# sort-results: true
# All available settings of specific linters.
linters-settings:
lll:
line-length: 132
stylecheck:
checks: [ "all", "-ST1000" ]
tagliatelle:
case:
# use-field-name: true
rules:
json: snake
yaml: snake
linters:
# Enable all available linters.
# Default: false
enable-all: true
# Disable specific linter
# https://golangci-lint.run/usage/linters/#disabled-by-default
disable:
- contextcheck
- cyclop
- deadcode
- dupl
- errorlint
- exhaustive
- exhaustivestruct
- exhaustruct
- forbidigo
- forcetypeassert
- funlen
- gci
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- goerr113
- goheader
- golint
- gomnd
- ifshort
- interfacer
- ireturn
- lll
- maintidx
- maligned
- musttag
- nestif
- nilnil
- nlreturn
- nolintlint
- nosnakecase
- promlinter
- rowserrcheck
- scopelint
- sqlclosecheck
- structcheck
- unparam
- varcheck
- varnamelen
- wastedassign
- wrapcheck
- wsl

View File

@@ -1,3 +1,17 @@
dev:
- generate error message if "validator credentials set" process fails to generate any credentials
- allow import of accounts with null name field in their keystore
- show text of execution payload extra data if available
1.28.0:
- support additional mnemonic word list languages
- increase minimum timeout for commands that fetch all validators to 2 minutes
- provide better error messages when offline preparation file cannot be read
- allow creation of all credential change operations related to a private key (thanks to @joaocenoura)
1.27.1:
- fix issue with voluntary exits using incorrect domain (thanks to @0xTylerHolmes)
1.27.0:
- use new build system
- support S3 credentials

View File

@@ -176,7 +176,11 @@ func (c *ChainInfo) FetchValidatorInfo(ctx context.Context, id string) (*Validat
case id == "":
return nil, errors.New("no validator specified")
case strings.HasPrefix(id, "0x"):
// A public key.
// ID is a public key.
// Check that the key is the correct length.
if len(id) != 98 {
return nil, errors.New("invalid public key: incorrect length")
}
for _, validator := range c.Validators {
if strings.EqualFold(id, fmt.Sprintf("%#x", validator.Pubkey)) {
validatorInfo = validator

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/cobra"
)
// accountCmd represents the account command
// accountCmd represents the account command.
var accountCmd = &cobra.Command{
Use: "account",
Short: "Manage account",

View File

@@ -15,6 +15,7 @@ package accountimport
import (
"context"
"encoding/json"
"fmt"
"github.com/pkg/errors"
@@ -66,7 +67,7 @@ func processFromKey(ctx context.Context, data *dataIn) (*dataOut, error) {
}
account, err := importer.ImportAccount(ctx, data.accountName, data.key, []byte(data.passphrase))
if err != nil {
return nil, errors.Wrap(err, "failed to import account")
return nil, errors.Wrap(err, "failed to import wallet")
}
results.account = account
@@ -79,15 +80,25 @@ func processFromKeystore(ctx context.Context, data *dataIn) (*dataOut, error) {
encryptor := keystorev4.New()
// Need to add a couple of fields to the keystore to make it compliant.
keystoreData := fmt.Sprintf(`{"name":"Import","encryptor":"keystore",%s`, string(data.keystore[1:]))
walletData := fmt.Sprintf(`{"wallet":{"name":"ImportTest","type":"non-deterministic","uuid":"e1526407-1dc7-4f3f-9d05-ab696f40707c","version":1},"accounts":[%s]}`, keystoreData)
var keystore map[string]any
if err := json.Unmarshal(data.keystore, &keystore); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal keystore")
}
keystore["name"] = data.accountName
keystore["encryptor"] = "keystore"
keystoreData, err := json.Marshal(keystore)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal keystore")
}
walletData := fmt.Sprintf(`{"wallet":{"name":"Import","type":"non-deterministic","uuid":"e1526407-1dc7-4f3f-9d05-ab696f40707c","version":1},"accounts":[%s]}`, keystoreData)
encryptedData, err := ecodec.Encrypt([]byte(walletData), data.keystorePassphrase)
if err != nil {
return nil, err
}
wallet, err := nd.Import(ctx, encryptedData, data.keystorePassphrase, store, encryptor)
if err != nil {
return nil, errors.Wrap(err, "failed to import wallet")
return nil, errors.Wrap(err, "failed to import account")
}
account := <-wallet.Accounts(ctx)

View File

@@ -21,7 +21,7 @@ import (
accountkey "github.com/wealdtech/ethdo/cmd/account/key"
)
// accountKeyCmd represents the account key command
// accountKeyCmd represents the account key command.
var accountKeyCmd = &cobra.Command{
Use: "key",
Short: "Obtain the private key of an account.",

View File

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

View File

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

View File

@@ -100,7 +100,6 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if attestation.Data.Slot == duty.Slot &&
attestation.Data.Index == duty.CommitteeIndex &&
attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) {
headCorrect := false
targetCorrect := false
if data.verbose {
@@ -138,7 +137,7 @@ func calcHeadCorrect(ctx context.Context, data *dataIn, attestation *phase0.Atte
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.
@@ -160,7 +159,7 @@ func calcTargetCorrect(ctx context.Context, data *dataIn, attestation *phase0.At
for {
header, err := data.eth2Client.(eth2client.BeaconBlockHeadersProvider).BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.

View File

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

View File

@@ -363,7 +363,7 @@ func (c *command) calcHeadCorrect(ctx context.Context, attestation *phase0.Attes
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.
@@ -392,7 +392,7 @@ func (c *command) calcTargetCorrect(ctx context.Context, attestation *phase0.Att
for {
header, err := c.blockHeadersProvider.BeaconBlockHeader(ctx, fmt.Sprintf("%d", slot))
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.

View File

@@ -690,7 +690,11 @@ func outputCapellaBlockExecutionPayload(ctx context.Context,
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
if utf8.Valid(payload.ExtraData) {
res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData)))
} else {
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
}
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
res.WriteString(" Transactions: ")

View File

@@ -29,9 +29,11 @@ import (
"github.com/pkg/errors"
)
var jsonOutput bool
var sszOutput bool
var results *dataOut
var (
jsonOutput bool
sszOutput bool
results *dataOut
)
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {

View File

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

View File

@@ -75,7 +75,7 @@ func (c *command) process(ctx context.Context) error {
switch state.Version {
case spec.DataVersionPhase0:
c.slot = phase0.Slot(state.Phase0.Slot)
c.slot = state.Phase0.Slot
c.incumbent = state.Phase0.ETH1Data
c.eth1DataVotes = state.Phase0.ETH1DataVotes
case spec.DataVersionAltair:

View File

@@ -38,6 +38,7 @@ func (c *command) process(ctx context.Context) error {
err := json.Unmarshal([]byte(c.data), c.item)
if err != nil {
c.additionalInfo = err.Error()
//nolint:nilerr
return nil
}
c.itemStructureValid = true
@@ -124,7 +125,7 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
// isAggregator returns true if the given
// isAggregator returns true if the given.
func (c *command) isAggregator(ctx context.Context) (bool, error) {
// Calculate the modulo.
specProvider, isProvider := c.eth2Client.(eth2client.SpecProvider)
@@ -204,6 +205,7 @@ func (c *command) confirmContributionSignature(ctx context.Context) error {
_, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
c.additionalInfo = err.Error()
//nolint:nilerr
return nil
}
c.contributionSignatureValidFormat = true
@@ -256,6 +258,7 @@ func (c *command) confirmContributionAndProofSignature(ctx context.Context) erro
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
c.additionalInfo = err.Error()
//nolint:nilerr
return nil
}
c.contributionAndProofSignatureValidFormat = true

View File

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

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/cobra"
)
// depositCmd represents the deposit command
// depositCmd represents the deposit command.
var depositCmd = &cobra.Command{
Use: "deposit",
Short: "Manage Ethereum 2 deposits",

View File

@@ -29,12 +29,14 @@ import (
string2eth "github.com/wealdtech/go-string2eth"
)
var depositVerifyData string
var depositVerifyWithdrawalPubKey string
var depositVerifyWithdrawalAddress string
var depositVerifyValidatorPubKey string
var depositVerifyDepositAmount string
var depositVerifyForkVersion string
var (
depositVerifyData string
depositVerifyWithdrawalPubKey string
depositVerifyWithdrawalAddress string
depositVerifyValidatorPubKey string
depositVerifyDepositAmount string
depositVerifyForkVersion string
)
var depositVerifyCmd = &cobra.Command{
Use: "verify",
@@ -45,7 +47,7 @@ var depositVerifyCmd = &cobra.Command{
The deposit data is compared to the supplied withdrawal account/public key, validator public key, and value to ensure they match.
In quiet mode this will return 0 if the the data is verified correctly, otherwise 1.`,
In quiet mode this will return 0 if the data is verified correctly, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(depositVerifyData != "", "--data is required")
var data []byte

View File

@@ -18,7 +18,7 @@ import (
"github.com/spf13/viper"
)
// epochCmd represents the epoch command
// epochCmd represents the epoch command.
var epochCmd = &cobra.Command{
Use: "epoch",
Short: "Obtain information about Ethereum 2 epochs",

View File

@@ -93,6 +93,7 @@ func (c *command) activeValidators(ctx context.Context) (map[phase0.ValidatorInd
return activeValidators, nil
}
func (c *command) processAttesterDuties(ctx context.Context) error {
activeValidators, err := c.activeValidators(ctx)
if err != nil {
@@ -202,7 +203,6 @@ func (c *command) processSlots(ctx context.Context,
Slot: beaconCommittee.Slot,
Committee: beaconCommittee.Index,
}
}
}
slotCommittees = allCommittees[attestation.Data.Slot]

View File

@@ -18,7 +18,7 @@ import (
"os"
)
// errCheck checks for an error and quits if it is present
// errCheck checks for an error and quits if it is present.
func errCheck(err error, msg string) {
if err != nil {
if !quiet {
@@ -48,14 +48,14 @@ func errCheck(err error, msg string) {
// }
// }
// assert checks a condition and quits if it is false
// assert checks a condition and quits if it is false.
func assert(condition bool, msg string) {
if !condition {
die(msg)
}
}
// die prints an error and quits
// die prints an error and quits.
func die(msg string) {
if msg != "" && !quiet {
fmt.Fprintf(os.Stderr, "%s\n", msg)

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/cobra"
)
// exitCmd represents the exit command
// exitCmd represents the exit command.
var exitCmd = &cobra.Command{
Use: "exit",
Short: "Manage Ethereum 2 voluntary exits",

View File

@@ -41,7 +41,7 @@ var exitVerifyCmd = &cobra.Command{
ethdo exit verify --data=exitdata.json --account=primary/current
In quiet mode this will return 0 if the the exit is verified correctly, otherwise 1.`,
In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()

View File

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

View File

@@ -24,6 +24,9 @@ import (
"github.com/wealdtech/ethdo/util"
)
// defaultBeaconNode is used if no other connection is supplied.
var defaultBeaconNode = "http://mainnet-consensus.attestant.io/"
var nodeInfoCmd = &cobra.Command{
Use: "info",
Short: "Obtain information about a node",
@@ -36,7 +39,26 @@ In quiet mode this will return 0 if the node information can be obtained, otherw
ctx := context.Background()
eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
errCheck(err, "Failed to connect to Ethereum 2 beacon node")
if err != nil {
if viper.GetString("connection") != "" {
// The user provided a connection, so don't second-guess them by using a different node.
fmt.Fprintln(os.Stderr, err.Error())
return
}
// The user did not provide a connection, so attempt to use the default node.
if viper.GetBool("debug") {
fmt.Fprintf(os.Stderr, "No node connection, attempting to use %s\n", defaultBeaconNode)
}
eth2Client, err = util.ConnectToBeaconNode(ctx, defaultBeaconNode, viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
return
}
if !viper.GetBool("quiet") {
fmt.Fprintf(os.Stderr, "No connection supplied; using mainnet public access endpoint\n")
}
}
if quiet {
os.Exit(_exitSuccess)

View File

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

View File

@@ -33,12 +33,14 @@ import (
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
var cfgFile string
var quiet bool
var verbose bool
var debug bool
var (
cfgFile string
quiet bool
verbose bool
debug bool
)
// RootCmd represents the base command when called without any subcommands
// RootCmd represents the base command when called without any subcommands.
var RootCmd = &cobra.Command{
Use: "ethdo",
Short: "Ethereum 2 CLI",

View File

@@ -19,7 +19,7 @@ import (
"github.com/spf13/viper"
)
// signatureCmd represents the signature command
// signatureCmd represents the signature command.
var signatureCmd = &cobra.Command{
Use: "signature",
Aliases: []string{"sig"},
@@ -31,8 +31,10 @@ func init() {
RootCmd.AddCommand(signatureCmd)
}
var dataFlag *pflag.Flag
var domainFlag *pflag.Flag
var (
dataFlag *pflag.Flag
domainFlag *pflag.Flag
)
func signatureFlags(cmd *cobra.Command) {
if dataFlag == nil {

View File

@@ -28,7 +28,7 @@ import (
var signatureAggregateSignatures []string
// signatureAggregateCmd represents the signature aggregate command
// signatureAggregateCmd represents the signature aggregate command.
var signatureAggregateCmd = &cobra.Command{
Use: "aggregate",
Short: "Aggregate signatures",

View File

@@ -27,7 +27,7 @@ import (
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// signatureSignCmd represents the signature sign command
// signatureSignCmd represents the signature sign command.
var signatureSignCmd = &cobra.Command{
Use: "sign",
Short: "Sign a 32-byte piece of data",

View File

@@ -27,10 +27,12 @@ import (
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
var signatureVerifySignature string
var signatureVerifySigner string
var (
signatureVerifySignature string
signatureVerifySigner string
)
// signatureVerifyCmd represents the signature verify command
// signatureVerifyCmd represents the signature verify command.
var signatureVerifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify signed data",

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/cobra"
)
// validatorCmd represents the validator command
// validatorCmd represents the validator command.
var validatorCmd = &cobra.Command{
Use: "validator",
Short: "Manage Ethereum 2 validators",

View File

@@ -19,22 +19,22 @@ import (
"fmt"
"os"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/beacon"
)
// obtainChainInfo obtains the chain information required to create a withdrawal credentials change operation.
func (c *command) obtainChainInfo(ctx context.Context) error {
var err error
// Use the offline preparation file if present (and we haven't been asked to recreate it).
if !c.prepareOffline {
err := c.obtainChainInfoFromFile(ctx)
if err == nil {
if err = c.obtainChainInfoFromFile(ctx); err == nil {
return nil
}
}
if c.offline {
return fmt.Errorf("%s is unavailable or outdated; this is required to have been previously generated using --offline-preparation on an online machine and be readable in the directory in which this command is being run", offlinePreparationFilename)
// If we are here it means that we are offline without chain information, and cannot continue.
return fmt.Errorf("failed to obtain offline preparation file: %w", err)
}
if err := c.obtainChainInfoFromNode(ctx); err != nil {
@@ -51,7 +51,7 @@ func (c *command) obtainChainInfoFromFile(_ context.Context) error {
if c.debug {
fmt.Fprintf(os.Stderr, "Failed to read offline preparation file: %v\n", err)
}
return errors.Wrap(err, fmt.Sprintf("cannot find %s", offlinePreparationFilename))
return err
}
if c.debug {
@@ -60,16 +60,16 @@ func (c *command) obtainChainInfoFromFile(_ context.Context) error {
data, err := os.ReadFile(offlinePreparationFilename)
if err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "failed to load chain state: %v\n", err)
fmt.Fprintf(os.Stderr, "failed to load offline preparation file: %v\n", err)
}
return errors.Wrap(err, "failed to read offline preparation file")
return err
}
c.chainInfo = &beacon.ChainInfo{}
if err := json.Unmarshal(data, c.chainInfo); err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "chain state invalid: %v\n", err)
fmt.Fprintf(os.Stderr, "offline preparation file invalid: %v\n", err)
}
return errors.Wrap(err, "failed to parse offline preparation file")
return err
}
return nil
@@ -97,7 +97,7 @@ func (c *command) writeChainInfoToFile(_ context.Context) error {
if err != nil {
return err
}
if err := os.WriteFile(offlinePreparationFilename, data, 0600); err != nil {
if err := os.WriteFile(offlinePreparationFilename, data, 0o600); err != nil {
return err
}

View File

@@ -40,7 +40,7 @@ func (c *command) output(_ context.Context) (string, error) {
if c.json {
return string(data), nil
}
if err := os.WriteFile(changeOperationsFilename, data, 0600); err != nil {
if err := os.WriteFile(changeOperationsFilename, data, 0o600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", changeOperationsFilename))
}
return "", nil

View File

@@ -22,6 +22,7 @@ import (
"os"
"regexp"
"strings"
"time"
consensusclient "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
@@ -38,11 +39,21 @@ import (
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// minTimeout is the minimum timeout for this command.
// It needs to be set here as we want timeouts to be low in general, but this can be pulling
// a lot of data for an unsophisticated audience so it's easier to set a higher timeout..
var minTimeout = 2 * time.Minute
// defaultBeaconNode is used if no other connection is supplied.
var defaultBeaconNode = "http://mainnet-consensus.attestant.io/"
// validatorPath is the regular expression that matches a validator path.
var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$")
var offlinePreparationFilename = "offline-preparation.json"
var changeOperationsFilename = "change-operations.json"
var (
offlinePreparationFilename = "offline-preparation.json"
changeOperationsFilename = "change-operations.json"
)
func (c *command) process(ctx context.Context) error {
if err := c.setup(ctx); err != nil {
@@ -65,6 +76,10 @@ func (c *command) process(ctx context.Context) error {
return err
}
if len(c.signedOperations) == 0 {
return errors.New("no suitable validators found; no operations generated")
}
if validated, reason := c.validateOperations(ctx); !validated {
return fmt.Errorf("operation failed validation: %s", reason)
}
@@ -127,6 +142,11 @@ func (c *command) obtainOperations(ctx context.Context) error {
return c.generateOperationsFromValidatorAndPrivateKey(ctx)
}
if c.privateKey != "" {
// Have a private key.
return c.generateOperationsFromPrivateKey(ctx)
}
return errors.New("unsupported combination of inputs; see help for details of supported combinations")
}
@@ -148,9 +168,18 @@ func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) erro
return fmt.Errorf("path %s does not match EIP-2334 format for a validator", c.path)
}
if _, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath); err != nil {
found, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath)
if err != nil {
return errors.Wrap(err, "failed to generate operation from seed and path")
}
// Function `c.generateOperationFromSeedAndPath()` will not return errors
// in non-serious cases since it is called in a loop when searching a
// mnemonic's key space without a specific path, so we need to check if a
// validator was not found in our case (it should be found if a path is
// provided) and return an error if not.
if !found {
return errors.New("no validator found with the provided path and mnemonic, please run with --debug to see more information")
}
return nil
}
@@ -175,7 +204,7 @@ func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context)
if c.debug {
fmt.Fprintf(os.Stderr, "Gone %d indices without finding the validator, not scanning any further\n", maxDistance)
}
break
return fmt.Errorf("failed to find validator using the provided mnemonic, validator=%s, pubkey=%#x", c.validator, validatorInfo.Pubkey)
}
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, validatorKeyPath)
@@ -221,10 +250,13 @@ func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
maxDistance := 1024
// Start scanning the validator keys.
lastFoundIndex := 0
foundValidatorCount := 0
for i := 0; ; i++ {
// If no validators have been found in the last maxDistance indices, stop scanning.
if i-lastFoundIndex > maxDistance {
if c.debug {
fmt.Fprintf(os.Stderr, "Gone %d indices without finding a validator, not scanning any further\n", maxDistance)
// If no validators were found at all, return an error.
if foundValidatorCount == 0 {
return fmt.Errorf("failed to find validators using the provided mnemonic: searched %d indices without finding a validator", maxDistance)
}
break
}
@@ -236,20 +268,21 @@ func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
}
if found {
lastFoundIndex = i
foundValidatorCount++
}
}
return nil
}
func (c *command) generateOperationsFromAccountAndWithdrawalAccount(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, false)
if err != nil {
return err
return errors.Wrap(err, "failed to obtain validator account")
}
withdrawalAccount, err := util.ParseAccount(ctx, c.withdrawalAccount, c.passphrases, true)
if err != nil {
return err
return errors.Wrap(err, "failed to obtain withdrawal account")
}
validatorPubkey, err := util.BestPublicKey(validatorAccount)
@@ -313,6 +346,49 @@ func (c *command) generateOperationsFromValidatorAndPrivateKey(ctx context.Conte
return nil
}
func (c *command) generateOperationsFromPrivateKey(ctx context.Context) error {
// Verify that the user provided a private key.
if strings.HasPrefix(c.privateKey, "0x") {
data, err := hex.DecodeString(strings.TrimPrefix(c.privateKey, "0x"))
if err != nil {
return errors.Wrap(err, "failed to parse account key")
}
if len(data) != 32 {
return errors.New("account key must be 32 bytes")
}
} else {
return errors.New("account key must be a hex string")
}
// Extract withdrawal account public key from supplied private key.
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return err
}
pubkey, err := util.BestPublicKey(withdrawalAccount)
if err != nil {
return err
}
withdrawalCredentials := ethutil.SHA256(pubkey.Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
found := false
for _, validatorInfo := range c.chainInfo.Validators {
// Skip validators which withdrawal key don't match with supplied withdrawal account public key.
if !bytes.Equal(withdrawalCredentials, validatorInfo.WithdrawalCredentials) {
continue
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
found = true
}
if !found {
return fmt.Errorf("no validator found with withdrawal credentials %#x", withdrawalCredentials)
}
return nil
}
func (c *command) obtainOperationsFromFileOrInput(ctx context.Context) error {
// Start off by attempting to use the provided signed operations.
if c.signedOperationsInput != "" {
@@ -390,7 +466,7 @@ func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
validator, exists := validators[validatorPubkey]
if !exists {
if c.debug {
fmt.Fprintf(os.Stderr, "No validator found with public key %s at path %s\n", validatorPubkey, path)
fmt.Fprintf(os.Stderr, "no validator found with public key %s at path %s\n", validatorPubkey, path)
}
return false, nil
}
@@ -420,7 +496,6 @@ func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
if err != nil {
return false, errors.Wrap(err, "failed to create withdrawal account")
}
} else {
// Need the withdrawal credentials from the private key.
withdrawalAccount, err = util.ParseAccount(ctx, c.privateKey, nil, true)
@@ -509,6 +584,14 @@ func (c *command) createSignedOperation(ctx context.Context,
}
func (c *command) parseWithdrawalAddress(_ context.Context) error {
// Check that a withdrawal address has been provided.
if c.withdrawalAddressStr == "" {
return errors.New("no withdrawal address provided")
}
// Check that the withdrawal address contains a 0x prefix.
if !strings.HasPrefix(c.withdrawalAddressStr, "0x") {
return fmt.Errorf("withdrawal address %s does not contain a 0x prefix", c.withdrawalAddressStr)
}
withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(c.withdrawalAddressStr, "0x"))
if err != nil {
return errors.Wrap(err, "failed to obtain execution address")
@@ -617,11 +700,34 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
// Ensure timeout is at least the minimum.
if c.timeout < minTimeout {
if c.debug {
fmt.Fprintf(os.Stderr, "Increasing timeout to %v\n", minTimeout)
}
c.timeout = minTimeout
}
// Connect to the consensus node.
var err error
c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to consensus node")
if c.connection != "" {
// The user provided a connection, so don't second-guess them by using a different node.
return err
}
// The user did not provide a connection, so attempt to use the default node.
if c.debug {
fmt.Fprintf(os.Stderr, "No node connection, attempting to use %s\n", defaultBeaconNode)
}
c.consensusClient, err = util.ConnectToBeaconNode(ctx, defaultBeaconNode, c.timeout, c.allowInsecureConnections)
if err != nil {
return err
}
if !c.quiet {
fmt.Fprintf(os.Stderr, "No connection supplied; using mainnet public access endpoint\n")
}
}
// Set up chaintime.

View File

@@ -26,6 +26,247 @@ import (
e2types "github.com/wealdtech/go-eth2-types/v2"
)
func TestGenerateOperationsFromPrivateKey(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
chainInfo := &beacon.ChainInfo{
Version: 1,
Validators: []*beacon.ValidatorInfo{
{
Index: 0,
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
},
{
Index: 1,
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
},
{
Index: 2,
Pubkey: phase0.BLSPubKey{0xaf, 0x9c, 0xe4, 0x4f, 0x50, 0x14, 0x8d, 0xb4, 0x12, 0x19, 0x4a, 0xf0, 0xba, 0xf0, 0xba, 0xb3, 0x6b, 0xd5, 0xc3, 0xe0, 0xc4, 0x93, 0x89, 0x11, 0xa4, 0xe5, 0x02, 0xe3, 0x98, 0xb5, 0x9e, 0x5c, 0xca, 0x7c, 0x78, 0xe3, 0xfe, 0x03, 0x41, 0x95, 0x47, 0x88, 0x79, 0xee, 0xb2, 0x3d, 0xb0, 0xa6},
WithdrawalCredentials: []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
},
{
Index: 3,
Pubkey: phase0.BLSPubKey{0x86, 0xd3, 0x30, 0xaf, 0x51, 0xfa, 0x59, 0x3f, 0xa9, 0xf9, 0x3e, 0xdb, 0x9d, 0x16, 0x64, 0x01, 0x86, 0xbe, 0x2e, 0x93, 0xea, 0x94, 0xd2, 0x59, 0x78, 0x1e, 0x1e, 0xb3, 0x4d, 0xeb, 0x84, 0x4c, 0x39, 0x68, 0xd7, 0x5e, 0xa9, 0x1d, 0x19, 0xf1, 0x59, 0xdb, 0xd0, 0x52, 0x3c, 0x6c, 0x5b, 0xa5},
WithdrawalCredentials: []byte{0x00, 0x81, 0x68, 0x45, 0x6b, 0x6d, 0x9a, 0x32, 0x83, 0x93, 0x1f, 0xea, 0x52, 0x10, 0xda, 0x12, 0x2d, 0x1e, 0x65, 0xe8, 0xed, 0x50, 0xb8, 0xe8, 0xf5, 0x91, 0x11, 0x83, 0xb0, 0x2f, 0xd1, 0x25},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
validators := make(map[string]*beacon.ValidatorInfo, len(chainInfo.Validators))
for i := range chainInfo.Validators {
validators[fmt.Sprintf("%#x", chainInfo.Validators[i].Pubkey)] = chainInfo.Validators[i]
}
tests := []struct {
name string
command *command
expected []*capella.SignedBLSToExecutionChange
err string
}{
{
name: "WithdrawalAddressNotHexBadChar",
command: &command{
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0xhc1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
privateKey: "0x67775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44638",
},
err: "invalid withdrawal address: failed to obtain execution address: encoding/hex: invalid byte: U+0068 'h'",
},
{
name: "WithdrawalAddressNo0xPrefix",
command: &command{
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
privateKey: "0x67775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44638",
},
err: "invalid withdrawal address: withdrawal address 8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15 does not contain a 0x prefix",
},
{
name: "ValidatorDoesNotExistInChain",
command: &command{
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
privateKey: "0x67775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44635",
},
err: "no validator found with withdrawal credentials 0x00afa1b7f669e09ba5a57ffdd6b140a4c30bc897202d6a8c14d694e361eeb5d3",
},
{
name: "InvalidKey",
command: &command{
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
privateKey: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
err: "failed to create account from private key: invalid private key: err blsSecretKeyDeserialize ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
name: "PrivateKeyAddressBadChar",
command: &command{
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
privateKey: "0xh7775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44638",
},
err: "failed to parse account key: encoding/hex: invalid byte: U+0068 'h'",
},
{
name: "PrivateKeyAddressNo0xPrefix",
command: &command{
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
privateKey: "67775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44638",
},
err: "account key must be a hex string",
},
{
name: "PrivateKeyAddressWrongLength",
command: &command{
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
privateKey: "0x775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44638",
},
err: "account key must be 32 bytes",
},
{
name: "Good",
command: &command{
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
privateKey: "0x67775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44638",
},
expected: []*capella.SignedBLSToExecutionChange{
{
Message: &capella.BLSToExecutionChange{
ValidatorIndex: 3,
FromBLSPubkey: phase0.BLSPubKey{0x86, 0x71, 0x0a, 0xbb, 0x44, 0xb6, 0xcd, 0xa6, 0x66, 0x57, 0x7b, 0xbb, 0x25, 0x5e, 0x16, 0xd9, 0x8b, 0xf2, 0x52, 0x51, 0x76, 0x22, 0x3f, 0x35, 0x35, 0xc7, 0xdf, 0xf8, 0xe7, 0x0b, 0x3b, 0xc8, 0x92, 0xbb, 0x36, 0x11, 0x33, 0x95, 0x2b, 0x03, 0xd2, 0xb0, 0x78, 0xcd, 0x07, 0x18, 0xca, 0xf3},
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
},
Signature: phase0.BLSSignature{0x8d, 0x92, 0xb9, 0x1c, 0x5d, 0xfd, 0x98, 0xc7, 0x98, 0xfc, 0x94, 0xe1, 0xe6, 0x69, 0xf3, 0xaa, 0xae, 0x72, 0xb2, 0x36, 0x47, 0xde, 0x88, 0x54, 0xea, 0x16, 0x74, 0x7f, 0xfe, 0xf0, 0x4d, 0x46, 0x5c, 0x07, 0x56, 0x34, 0x03, 0x30, 0x2f, 0xbc, 0x26, 0xa2, 0x6d, 0xec, 0x10, 0x20, 0xe7, 0x67, 0x10, 0xb0, 0x4a, 0x7e, 0x4e, 0x25, 0x89, 0x7e, 0x87, 0x88, 0xda, 0xaf, 0x2b, 0xb5, 0xb7, 0x73, 0x25, 0x64, 0x80, 0xc1, 0xba, 0xf3, 0x1d, 0x33, 0x8f, 0x17, 0xa5, 0x35, 0x74, 0x80, 0xf3, 0x37, 0x0e, 0xea, 0x19, 0x15, 0xd5, 0x69, 0x7e, 0xf6, 0x68, 0xaa, 0x9c, 0x3d, 0x47, 0x19, 0x75, 0xfc},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.generateOperationsFromPrivateKey(ctx)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, test.command.signedOperations)
}
})
}
}
func TestGenerateOperationsFromMnemonic(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
chainInfo := &beacon.ChainInfo{
Version: 1,
Validators: []*beacon.ValidatorInfo{
{
Index: 0,
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
},
{
Index: 1,
Pubkey: phase0.BLSPubKey{0xb4, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
tests := []struct {
name string
command *command
expected []*capella.SignedBLSToExecutionChange
err string
}{
{
name: "MnemonicInvalid",
command: &command{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "mnemonic is invalid",
},
{
name: "NoWithdrawalAddressProvided",
command: &command{
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",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
},
err: "failed to generate operation from seed and path: invalid withdrawal address: no withdrawal address provided",
},
{
name: "NoValidatorFound",
command: &command{
mnemonic: "struggle kangaroo horn sniff cradle soft ethics thunder cycle illegal flock unaware dynamic cinnamon play enforce card tennis inform parent surprise bring relax tail",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "failed to find validators using the provided mnemonic: searched 1024 indices without finding a validator",
},
{
name: "Good",
command: &command{
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",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
expected: []*capella.SignedBLSToExecutionChange{
{
Message: &capella.BLSToExecutionChange{
ValidatorIndex: 0,
FromBLSPubkey: phase0.BLSPubKey{0x99, 0xb1, 0xf1, 0xd8, 0x4d, 0x76, 0x18, 0x54, 0x66, 0xd8, 0x6c, 0x34, 0xbd, 0xe1, 0x10, 0x13, 0x16, 0xaf, 0xdd, 0xae, 0x76, 0x21, 0x7a, 0xa8, 0x6c, 0xd0, 0x66, 0x97, 0x9b, 0x19, 0x85, 0x8c, 0x2c, 0x9d, 0x9e, 0x56, 0xee, 0xbc, 0x1e, 0x06, 0x7a, 0xc5, 0x42, 0x77, 0xa6, 0x17, 0x90, 0xdb},
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
},
Signature: phase0.BLSSignature{0xb7, 0x8a, 0x05, 0xba, 0xd9, 0x27, 0xfc, 0x89, 0x6f, 0x14, 0x06, 0xb3, 0x2d, 0x64, 0x4a, 0xe1, 0x69, 0xce, 0xcd, 0x89, 0x86, 0xc1, 0xef, 0x8c, 0x0d, 0x03, 0x7d, 0x70, 0x86, 0xf8, 0x5f, 0x13, 0xe1, 0xe1, 0x88, 0xb4, 0x30, 0x96, 0x43, 0xa2, 0xc1, 0x3f, 0xfe, 0xfb, 0x0a, 0xe8, 0x05, 0x11, 0x09, 0x98, 0x53, 0xa0, 0x58, 0x1f, 0x4b, 0x2b, 0xd2, 0xe1, 0x45, 0x41, 0x04, 0x79, 0x01, 0xe2, 0x2a, 0x94, 0x0a, 0x9c, 0x7e, 0x3a, 0xc0, 0xa8, 0x82, 0xd1, 0xa8, 0xaf, 0x6b, 0xfa, 0xea, 0x81, 0x3a, 0x6a, 0x6b, 0xe7, 0x21, 0xf9, 0x26, 0x22, 0x04, 0xaa, 0x9d, 0xa4, 0xe4, 0x77, 0x27, 0xd0},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.generateOperationsFromMnemonic(ctx)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, test.command.signedOperations)
}
})
}
}
func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
ctx := context.Background()
@@ -68,7 +309,7 @@ func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
err: "mnemonic is invalid",
},
{
name: "PathInvalid",
name: "PathInvalidNoIndex",
command: &command{
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",
@@ -78,6 +319,68 @@ func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
},
err: "path m/12381/3600/0/0 does not match EIP-2334 format for a validator",
},
{
name: "PathInvlaidIndexNot2334Format",
command: &command{
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: "1",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "path 1 does not match EIP-2334 format for a validator",
},
{
name: "WithdrawalAddressNo0xPrefix",
command: &command{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/0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "failed to generate operation from seed and path: invalid withdrawal address: withdrawal address 8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15 does not contain a 0x prefix",
},
{
name: "WithdrawalAddressInvalidLength",
command: &command{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/0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac",
},
err: "failed to generate operation from seed and path: invalid withdrawal address: withdrawal address must be exactly 20 bytes in length",
},
{
name: "WithdrawalAddressMissing",
command: &command{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/0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
},
err: "failed to generate operation from seed and path: invalid withdrawal address: no withdrawal address provided",
},
{
name: "InvalidWithdrawalAddressNotHexBarChar",
command: &command{
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/0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0xrc1Ff978036F2e9d7CC382Eff7B4c8c53C22acaa",
},
err: "failed to generate operation from seed and path: invalid withdrawal address: failed to obtain execution address: encoding/hex: invalid byte: U+0072 'r'",
},
{
name: "NoValidatorFoundAtGivenPath",
command: &command{
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/10/0/0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0xrc1Ff978036F2e9d7CC382Eff7B4c8c53C22acaa",
},
err: "no validator found with the provided path and mnemonic, please run with --debug to see more information",
},
{
name: "Good",
command: &command{
@@ -165,6 +468,96 @@ func TestGenerateOperationFromMnemonicAndValidator(t *testing.T) {
},
err: "no validator specified",
},
{
name: "WithdrawalAddressMissing",
command: &command{
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",
validator: "0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
},
err: "invalid withdrawal address: no withdrawal address provided",
},
{
name: "InvalidWithdrawalAddressLength",
command: &command{
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",
validator: "0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac",
},
err: "invalid withdrawal address: withdrawal address must be exactly 20 bytes in length",
},
{
name: "InvalidWithdrawalAddressNo0xPrefix",
command: &command{
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",
validator: "0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "8c1Ff978036F2e9d7CC382Eff7B4c8c53C22acaa",
},
err: "invalid withdrawal address: withdrawal address 8c1Ff978036F2e9d7CC382Eff7B4c8c53C22acaa does not contain a 0x prefix",
},
{
name: "InvalidWithdrawalAddressNotHexBadChar",
command: &command{
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",
validator: "0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0xrc1Ff978036F2e9d7CC382Eff7B4c8c53C22acaa",
},
err: "invalid withdrawal address: failed to obtain execution address: encoding/hex: invalid byte: U+0072 'r'",
},
{
name: "ValidatorBeyondMaxDistance",
command: &command{
mnemonic: "struggle kangaroo horn sniff cradle soft ethics thunder cycle illegal flock unaware dynamic cinnamon play enforce card tennis inform parent surprise bring relax tail",
validator: "1",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "failed to find validator using the provided mnemonic, validator=1, pubkey=0xb3d89e2f29c712c6a9f8e5a269b97617c4a94dd6f6662ab3b07ce9e5434573f15b5c988cd14bbd5804f77156a8af1cfa",
},
{
name: "UnknownValidatorPubKey",
command: &command{
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",
validator: "0xb384f767d964e100c8a9b21018d08c25ffebae268b3ab6d610353897541971726dbfc3c7463884c68a531515aab94c80",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "unknown validator",
},
{
name: "UnknownValidatorIndex",
command: &command{
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",
validator: "10",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "unknown validator",
},
{
name: "InvalidPubkeyLength",
command: &command{
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",
validator: "0xb384f767d964e100c8a9b21018d08c25ffebae268b3ab6d610353897541971726dbfc3c7463884c68a531515aab94c",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "invalid public key: incorrect length",
},
{
name: "Good",
command: &command{
@@ -285,8 +678,9 @@ func TestGenerateOperationFromSeedAndPath(t *testing.T) {
chainInfo: chainInfo,
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/999/0/0",
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/999/0/0",
generated: false,
},
{
name: "ValidatorCredentialsAlreadySet",
@@ -295,8 +689,9 @@ func TestGenerateOperationFromSeedAndPath(t *testing.T) {
chainInfo: chainInfo,
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/2/0/0",
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/2/0/0",
generated: false,
},
{
name: "PrivateKeyInvalid",
@@ -310,6 +705,18 @@ func TestGenerateOperationFromSeedAndPath(t *testing.T) {
path: "m/12381/3600/0/0/0",
err: "failed to create account from private key: invalid private key: err blsSecretKeyDeserialize ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
name: "PrivateKeyDoesNotMatch",
command: &command{
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",
chainInfo: chainInfo,
privateKey: "0x67775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44638",
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/4/0/0",
generated: false,
},
{
name: "Good",
command: &command{

View File

@@ -65,7 +65,6 @@ func output(ctx context.Context, data *dataOut) (string, error) {
} else {
builder.WriteString("\n")
}
}
}

View File

@@ -19,22 +19,22 @@ import (
"fmt"
"os"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/beacon"
)
// obtainChainInfo obtains the chain information required to create an exit operation.
func (c *command) obtainChainInfo(ctx context.Context) error {
var err error
// Use the offline preparation file if present (and we haven't been asked to recreate it).
if !c.prepareOffline {
err := c.obtainChainInfoFromFile(ctx)
if err == nil {
if err = c.obtainChainInfoFromFile(ctx); err == nil {
return nil
}
}
if c.offline {
return fmt.Errorf("%s is unavailable or outdated; this is required to have been previously generated using --offline-preparation on an online machine and be readable in the directory in which this command is being run", offlinePreparationFilename)
// If we are here it means that we are offline without chain information, and cannot continue.
return fmt.Errorf("failed to obtain offline preparation file: %w", err)
}
if err := c.obtainChainInfoFromNode(ctx); err != nil {
@@ -51,7 +51,7 @@ func (c *command) obtainChainInfoFromFile(_ context.Context) error {
if c.debug {
fmt.Fprintf(os.Stderr, "Failed to read offline preparation file: %v\n", err)
}
return errors.Wrap(err, fmt.Sprintf("cannot find %s", offlinePreparationFilename))
return err
}
if c.debug {
@@ -60,16 +60,16 @@ func (c *command) obtainChainInfoFromFile(_ context.Context) error {
data, err := os.ReadFile(offlinePreparationFilename)
if err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "failed to load chain state: %v\n", err)
fmt.Fprintf(os.Stderr, "failed to load offline preparation file: %v\n", err)
}
return errors.Wrap(err, "failed to read offline preparation file")
return err
}
c.chainInfo = &beacon.ChainInfo{}
if err := json.Unmarshal(data, c.chainInfo); err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "chain state invalid: %v\n", err)
fmt.Fprintf(os.Stderr, "offline preparation file invalid: %v\n", err)
}
return errors.Wrap(err, "failed to parse offline preparation file")
return err
}
return nil
@@ -97,7 +97,7 @@ func (c *command) writeChainInfoToFile(_ context.Context) error {
if err != nil {
return err
}
if err := os.WriteFile(offlinePreparationFilename, data, 0600); err != nil {
if err := os.WriteFile(offlinePreparationFilename, data, 0o600); err != nil {
return err
}

View File

@@ -82,6 +82,11 @@ func newCommand(_ context.Context) (*command, error) {
genesisValidatorsRoot: viper.GetString("genesis-validators-root"),
}
// Account and validator are synonymous.
if c.validator == "" {
c.validator = viper.GetString("account")
}
// Timeout is required.
if c.timeout == 0 {
return nil, errors.New("timeout is required")

View File

@@ -40,7 +40,7 @@ func (c *command) output(_ context.Context) (string, error) {
if c.json {
return string(data), nil
}
if err := os.WriteFile(exitOperationFilename, data, 0600); err != nil {
if err := os.WriteFile(exitOperationFilename, data, 0o600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", exitOperationFilename))
}
return "", nil

View File

@@ -22,6 +22,7 @@ import (
"os"
"regexp"
"strings"
"time"
consensusclient "github.com/attestantio/go-eth2-client"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
@@ -37,11 +38,18 @@ import (
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// minTimeout is the minimum timeout for this command.
// It needs to be set here as we want timeouts to be low in general, but this can be pulling
// a lot of data for an unsophisticated audience so it's easier to set a higher timeout..
var minTimeout = 2 * time.Minute
// validatorPath is the regular expression that matches a validator path.
var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$")
var offlinePreparationFilename = "offline-preparation.json"
var exitOperationFilename = "exit-operation.json"
var (
offlinePreparationFilename = "offline-preparation.json"
exitOperationFilename = "exit-operation.json"
)
func (c *command) process(ctx context.Context) error {
if err := c.setup(ctx); err != nil {
@@ -70,7 +78,7 @@ func (c *command) process(ctx context.Context) error {
if c.json || c.offline {
if c.debug {
fmt.Fprintf(os.Stderr, "Not broadcasting credentials change operations\n")
fmt.Fprintf(os.Stderr, "Not broadcasting exit operation\n")
}
// Want JSON output, or cannot broadcast.
return nil
@@ -175,8 +183,7 @@ func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context)
return errors.Wrap(err, "failed to create withdrawal account")
}
err = c.generateOperationFromAccount(ctx, validatorInfo, validatorAccount, c.chainInfo.Epoch)
if err != nil {
if err := c.generateOperationFromAccount(ctx, validatorAccount); err != nil {
return err
}
break
@@ -189,24 +196,10 @@ func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context)
func (c *command) generateOperationFromPrivateKey(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return errors.Wrap(err, "failed to create validator account")
return errors.Wrap(err, "failed to parse validator account")
}
validatorPubkey, err := util.BestPublicKey(validatorAccount)
if err != nil {
return err
}
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%#x", validatorPubkey.Marshal()))
if err != nil {
return err
}
if c.verbose {
fmt.Fprintf(os.Stderr, "Validator %d found with public key %s\n", validatorInfo.Index, validatorPubkey)
}
if err = c.generateOperationFromAccount(ctx, validatorInfo, validatorAccount, c.chainInfo.Epoch); err != nil {
if err = c.generateOperationFromAccount(ctx, validatorAccount); err != nil {
return err
}
@@ -214,17 +207,12 @@ func (c *command) generateOperationFromPrivateKey(ctx context.Context) error {
}
func (c *command) generateOperationFromValidator(ctx context.Context) error {
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, c.validator)
if err != nil {
return err
}
validatorAccount, err := util.ParseAccount(ctx, c.validator, nil, true)
if err != nil {
return err
return errors.Wrap(err, "failed to parse validator account")
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, validatorAccount, c.chainInfo.Epoch); err != nil {
if err := c.generateOperationFromAccount(ctx, validatorAccount); err != nil {
return err
}
@@ -299,12 +287,19 @@ func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
}
func (c *command) generateOperationFromAccount(ctx context.Context,
validator *beacon.ValidatorInfo,
account e2wtypes.Account,
epoch phase0.Epoch,
) error {
var err error
c.signedOperation, err = c.createSignedOperation(ctx, validator, account, epoch)
pubKey, err := util.BestPublicKey(account)
if err != nil {
return err
}
info, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%#x", pubKey.Marshal()))
if err != nil {
return err
}
c.signedOperation, err = c.createSignedOperation(ctx, info, account, c.chainInfo.Epoch)
return err
}
@@ -435,6 +430,14 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
// Ensure timeout is at least the minimum.
if c.timeout < minTimeout {
if c.debug {
fmt.Fprintf(os.Stderr, "Increasing timeout to %v\n", minTimeout)
c.timeout = minTimeout
}
}
// Connect to the consensus node.
var err error
c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
@@ -472,7 +475,7 @@ func (c *command) generateDomain(ctx context.Context) error {
return errors.Wrap(err, "failed to calculate signature domain")
}
copy(c.domain[:], c.chainInfo.BLSToExecutionChangeDomainType[:])
copy(c.domain[:], c.chainInfo.VoluntaryExitDomainType[:])
copy(c.domain[4:], root[:])
if c.debug {
fmt.Fprintf(os.Stderr, "Domain is %#x\n", c.domain)

View File

@@ -40,13 +40,14 @@ func (c *command) process(ctx context.Context) error {
return c.calculateYield(ctx)
}
var weiPerGwei = decimal.New(1e9, 0)
var one = decimal.New(1, 0)
var epochsPerYear = decimal.New(225*365, 0)
var (
weiPerGwei = decimal.New(1e9, 0)
one = decimal.New(1, 0)
epochsPerYear = decimal.New(225*365, 0)
)
// calculateYield calculates yield from the number of active validators.
func (c *command) calculateYield(ctx context.Context) error {
spec, err := c.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return err

View File

@@ -17,7 +17,7 @@ import (
"github.com/spf13/cobra"
)
// validatorCredentialsCmd represents the validator credentials command
// validatorCredentialsCmd represents the validator credentials command.
var validatorCredentialsCmd = &cobra.Command{
Use: "credentials",
Short: "Manage Ethereum consensus validator credentials",

View File

@@ -32,7 +32,7 @@ If validatoraccount is provided with an account path it will generate deposit da
The information generated can be passed to ethereal to create a deposit from the Ethereum 1 chain.
In quiet mode this will return 0 if the the data can be generated correctly, otherwise 1.`,
In quiet mode this will return 0 if the data can be generated correctly, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatordepositdata.Run(cmd)
if err != nil {

View File

@@ -30,7 +30,7 @@ var validatorDutiesCmd = &cobra.Command{
Attester duties are known for the current and next epoch. Proposer duties are known for the current epoch.
In quiet mode this will return 0 if the the duties have been obtained, otherwise 1.`,
In quiet mode this will return 0 if the duties have been obtained, otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorduties.Run(cmd)
if err != nil {

View File

@@ -57,7 +57,7 @@ func init() {
validatorExitCmd.Flags().Int64("epoch", -1, "Epoch at which to exit (defaults to current epoch)")
validatorExitCmd.Flags().Bool("prepare-offline", false, "Create files for offline use")
validatorExitCmd.Flags().String("validator", "", "Validator to exit")
validatorExitCmd.Flags().String("signed-operation", "", "Use pre-defined JSON signed operation as created by --json to transmit the exit operation (reads from exit-operations.json if not present)")
validatorExitCmd.Flags().String("signed-operation", "", "Use pre-defined JSON signed operation as created by --json to transmit the exit operation (reads from exit-operation.json if not present)")
validatorExitCmd.Flags().Bool("json", false, "Generate JSON data containing a signed operation rather than broadcast it to the network (implied when offline)")
validatorExitCmd.Flags().Bool("offline", false, "Do not attempt to connect to a beacon node to obtain information for the operation")
validatorExitCmd.Flags().String("fork-version", "", "Fork version to use for signing (overrides fetching from beacon node)")

View File

@@ -109,7 +109,7 @@ In quiet mode this will return 0 if the validator information can be obtained, o
},
}
// graphData returns data from the graph about number and amount of deposits
// graphData returns data from the graph about number and amount of deposits.
func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error) {
subgraph := ""
if network == "Mainnet" {
@@ -119,8 +119,12 @@ func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error
}
query := fmt.Sprintf(`{"query": "{deposits(where: {validatorPubKey:\"%#x\"}) { id amount withdrawalCredentials }}"}`, validatorPubKey)
url := fmt.Sprintf("https://api.thegraph.com/subgraphs/name/%s", subgraph)
// #nosec G107
graphResp, err := http.Post(url, "application/json", bytes.NewBufferString(query))
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewBufferString(query))
if err != nil {
return 0, 0, errors.Wrap(err, "failed to start request")
}
req.Header.Set("Accept", "application/json")
graphResp, err := http.DefaultClient.Do(req)
if err != nil {
return 0, 0, errors.Wrap(err, "failed to check if there is already a deposit for this validator")
}
@@ -131,8 +135,10 @@ func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error
}
type graphDeposit struct {
Index string `json:"index"`
Amount string `json:"amount"`
Index string `json:"index"`
Amount string `json:"amount"`
// Using graph API JSON names in camel case.
//nolint:tagliatelle
WithdrawalCredentials string `json:"withdrawalCredentials"`
}
type graphData struct {

View File

@@ -1,4 +1,4 @@
// Copyright © 2019 - 2022 Weald Technology Trading.
// Copyright © 2019 - 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -24,9 +24,9 @@ import (
// ReleaseVersion is the release version of the codebase.
// Usually overridden by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.27.0)"
var ReleaseVersion = "local build (latest release 1.28.1)"
// versionCmd represents the version command
// versionCmd represents the version command.
var versionCmd = &cobra.Command{
Use: "version",
Short: "Version of ethdo",

View File

@@ -19,7 +19,7 @@ import (
"github.com/spf13/viper"
)
// walletCmd represents the wallet command
// walletCmd represents the wallet command.
var walletCmd = &cobra.Command{
Use: "wallet",
Short: "Manage wallets",

View File

@@ -74,7 +74,7 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
return nil, errors.Wrap(err, "failed to marshal shamir export")
}
if err := os.WriteFile(data.file, sharedFile, 0600); err != nil {
if err := os.WriteFile(data.file, sharedFile, 0o600); err != nil {
return nil, errors.Wrap(err, "failed to write export file")
}

View File

@@ -37,7 +37,7 @@ func TestInput(t *testing.T) {
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
datFile := filepath.Join(dir, "backup.dat")
require.NoError(t, os.WriteFile(datFile, []byte("dummy"), 0600))
require.NoError(t, os.WriteFile(datFile, []byte("dummy"), 0o600))
defer os.RemoveAll(dir)
store := scratch.New()

View File

@@ -39,7 +39,7 @@ func TestProcess(t *testing.T) {
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
datFile := filepath.Join(dir, "backup.dat")
require.NoError(t, os.WriteFile(datFile, export, 0600))
require.NoError(t, os.WriteFile(datFile, export, 0o600))
defer os.RemoveAll(dir)
tests := []struct {

View File

@@ -16,7 +16,16 @@ Withdrawal credentials, held as part of a validator's on-chain definition, defin
A private key is a hexadecimal string (_e.g._ 0x010203…a1a2a3) that can be used to generate a public key and (in the case of the execution chain) Ethereum address.
### Mnemonic
A mnemonic is a 24-word phrase that can be used to generate multiple private keys with the use of _paths_.
A mnemonic is a 24-word phrase that can be used to generate multiple private keys with the use of _paths_. Mnemonics are supported in the following languages:
* chinese simplified
* chinese traditional
* czech
* english
* french
* italian
* japanese
* korean
* spanish
### Path
A path is a string starting with "m" and containing a number of components separated by "/", for example "m/12381/3600/0/0". The process to obtain a key from a mnemonic and path is known as "hierarchical derivation".
@@ -50,27 +59,33 @@ Here the copy of `ethdo` with access to private keys is on an offline computer,
## Preparation
Regardless of the method selected, preparation must take place on the online computer to ensure that `ethdo` can access your consensus node. `ethdo` will attempt to find a local consensus node automatically, but if not then an explicit connection value will be required. To find out if `ethdo` has access to the consensus node run:
```
ethdo node info --verbose
```sh
ethdo node info
```
The result should be something similar to the following:
```
Version: teku/v22.9.1/linux-x86_64/-privatebuild-openjdk64bitservervm-java-14
Syncing: false
```
It is important to confirm that the "Syncing" value is "false". If this is "true" it means that the node is currently syncing, and you will need to wait for the process to finish before proceeding.
Alternatively, the result may look like this:
If this command instead returns an error you will need to add an explicit connection string. For example, if your consensus node is serving its REST API on port 12345 then you should add `--connection=http://localhost:12345` to all `ethdo` commands in this process, for example:
```
No connection supplied; using mainnet public access endpoint
Syncing: false
```
which means that a local consensus node was not accessed and instead a public endpoint specifically assigned to handle these operations was used instead. If you do have a local consensus node but see this message it means that the local node could not be accessed, usually because it is running on a non-standard port. If this is the case for your configuration, you need to let `ethdo` know where the consensus node's REST API is. For example, if your consensus node is serving its REST API on port 12345 then you should add `--connection=http://localhost:12345` to all `ethdo` commands in this process, for example:
```sh
ethdo --connection=http://localhost:12345 node info --verbose
ethdo --connection=http://localhost:12345 node info
```
Note that some consensus nodes may require configuration to serve their REST API. Please refer to the documentation of your specific consensus node to enable this.
Regardless of your method used above, it is important to confirm that the "Syncing" value is "false". If this is "true" it means that the node is currently syncing, and you will need to wait for the process to finish before proceeding.
Once the preparation is complete you should select either basic or advanced operation, depending on your requirements.
## Basic operation
@@ -201,6 +216,14 @@ ethdo validator credentials set --mnemonic="abandon abandon abandon … art" --p
Note that it is possible for there to be multiple validators that use the provided private key for a withdrawal address, in which case an operation will be generated for each validator that is eligible for change.
#### Using withdrawal private key only.
Similar to the previous section, however instead of specifying the mnemonic, it will select multiple validators that use provided private key for their withdrawal address.
```
ethdo validator credentials set --private-key=0x3b…9c --withdrawal-address=0x8f…9F
```
#### Using an account
If you used `ethdo` to generate your validator deposit data you will likely have used a separate account to generate the withdrawal credentials. You can specify the accout of the validator and the accout of the withdrawal credentials to generate and broadcast the credentials change operation with the following command:

7
go.mod
View File

@@ -1,9 +1,9 @@
module github.com/wealdtech/ethdo
go 1.18
go 1.20
require (
github.com/attestantio/go-eth2-client v0.15.2
github.com/attestantio/go-eth2-client v0.15.3
github.com/ferranbt/fastssz v0.1.2
github.com/gofrs/uuid v4.2.0+incompatible
github.com/google/uuid v1.3.0
@@ -21,7 +21,6 @@ require (
github.com/spf13/viper v1.13.0
github.com/stretchr/testify v1.8.1
github.com/tyler-smith/go-bip39 v1.1.0
github.com/wealdtech/chaind v0.6.17
github.com/wealdtech/go-bytesutil v1.2.0
github.com/wealdtech/go-ecodec v1.1.2
github.com/wealdtech/go-eth2-types/v2 v2.8.0
@@ -33,7 +32,7 @@ require (
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.6.0
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.4.0
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.17.0
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.1
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.2
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.0
github.com/wealdtech/go-eth2-wallet-types/v2 v2.10.0
github.com/wealdtech/go-string2eth v1.2.0

9
go.sum
View File

@@ -67,8 +67,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/attestantio/go-eth2-client v0.15.2 h1:4EYeA5IBSBypkUMhkkFALzMddaFDdb5PvCl7ORXEl6w=
github.com/attestantio/go-eth2-client v0.15.2/go.mod h1:/Oh6YTuHmHhgLN/ZnQRKHGc7HdIzGlDkI2vjNZvOsvA=
github.com/attestantio/go-eth2-client v0.15.3 h1:a4uLBkTkXr5Tq3elxT6IPlDH4910+boQdDS2hTg8THk=
github.com/attestantio/go-eth2-client v0.15.3/go.mod h1:/Oh6YTuHmHhgLN/ZnQRKHGc7HdIzGlDkI2vjNZvOsvA=
github.com/aws/aws-sdk-go v1.40.41/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.44.152 h1:L9aaepO8wHB67gwuGD8VgIYH/cmQDxieCt7FeLa0+fI=
github.com/aws/aws-sdk-go v1.44.152/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
@@ -500,8 +500,6 @@ github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNG
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/wealdtech/chaind v0.6.17 h1:HBlmzKj9Egy9rnZKGGIwvM6mUHJ+64163hNhSwjg/FQ=
github.com/wealdtech/chaind v0.6.17/go.mod h1:g8XOXrrRtwjD6mlpn9TydRPJD+gy4iFZMlPkQrBxxQA=
github.com/wealdtech/eth2-signer-api v1.7.1 h1:XdwFuv3VWCwcPPPrfa77sUXL1GSvxDtsUZxlByz//b0=
github.com/wealdtech/eth2-signer-api v1.7.1/go.mod h1:fX8XtN9Svyjs+e7TgoOfOcwRTHeblR5SXftAVV3T1ZA=
github.com/wealdtech/go-bytesutil v1.0.1/go.mod h1:jENeMqeTEU8FNZyDFRVc7KqBdRKSnJ9CCh26TcuNb9s=
@@ -541,6 +539,8 @@ github.com/wealdtech/go-eth2-wallet-store-filesystem v1.17.0/go.mod h1:Fiw5If3/m
github.com/wealdtech/go-eth2-wallet-store-s3 v1.10.0/go.mod h1:DhAm7si8N/5qU1sZ/RLavm87LsOthnWuRyQaGWNFiyI=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.1 h1:q9/p/UfrT7AfR6MYZfr3nQOKdhcLCuKqldqplNHo3Ws=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.1/go.mod h1:azzsylTwr1hnLisDWZJbUz3HToRsrG7ADpLG8TXJgOU=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.2 h1:Lxxfu5YKTzfNlymI8kF04BbC8hIKPr3in06zgHDmow8=
github.com/wealdtech/go-eth2-wallet-store-s3 v1.11.2/go.mod h1:azzsylTwr1hnLisDWZJbUz3HToRsrG7ADpLG8TXJgOU=
github.com/wealdtech/go-eth2-wallet-store-scratch v1.6.3/go.mod h1:V4NUofSBIyzoqc5cNZaGciaDm2WFAgSQikRslOyh5Tg=
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.0 h1:1dMKx9jtw1v9JrwOPFf2JaOQKmvpMp1GEeuMRiNfq5o=
github.com/wealdtech/go-eth2-wallet-store-scratch v1.7.0/go.mod h1:O7BitrDeQVtBFNnvYmOYLzJCZAiCf5ur/4IRucIz+S0=
@@ -679,7 +679,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=

View File

@@ -20,6 +20,8 @@ import (
)
// Service provides a number of functions for calculating chain-related times.
//
//nolint:interfacebloat
type Service interface {
// GenesisTime provides the time of the chain's genesis.
GenesisTime() time.Time

View File

@@ -285,14 +285,14 @@ func fetchCapellaForkEpoch(ctx context.Context,
if err != nil {
return 0, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["CAPELLAELLATRIX_FORK_EPOCH"]
tmp, exists := spec["CAPELLA_FORK_EPOCH"]
if !exists {
return 0, errors.New("capella fork version not known by chain")
}
epoch, isEpoch := tmp.(uint64)
if !isEpoch {
//nolint:revive
return 0, errors.New("CAPELLAELLATRIX_FORK_EPOCH is not a uint64!")
return 0, errors.New("CAPELLA_FORK_EPOCH is not a uint64!")
}
return phase0.Epoch(epoch), nil

View File

@@ -15,7 +15,7 @@ const (
ShareOverhead = 1
)
// polynomial represents a polynomial of arbitrary degree
// polynomial represents a polynomial of arbitrary degree.
type polynomial struct {
coefficients []uint8
}
@@ -53,7 +53,7 @@ func makePolynomial(intercept, degree uint8) (polynomial, error) {
return p, nil
}
// evaluate returns the value of the polynomial for the given x
// evaluate returns the value of the polynomial for the given x.
func (p *polynomial) evaluate(x uint8) uint8 {
// Special case the origin
if x == 0 {
@@ -92,7 +92,7 @@ func interpolatePolynomial(xSamples, ySamples []uint8, x uint8) uint8 {
return result
}
// div divides two numbers in GF(2^8)
// div divides two numbers in GF(2^8).
func div(a, b uint8) uint8 {
if b == 0 {
// leaks some timing information but we don't care anyways as this
@@ -111,8 +111,8 @@ func div(a, b uint8) uint8 {
return uint8(ret)
}
// mult multiplies two numbers in GF(2^8)
func mult(a, b uint8) (out uint8) {
// mult multiplies two numbers in GF(2^8).
func mult(a, b uint8) uint8 {
logA := logTable[a]
logB := logTable[b]
sum := (int(logA) + int(logB)) % 255
@@ -157,6 +157,7 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) {
}
// Generate random list of x coordinates
//nolint
rnd := mathrand.New(&cryptoSource{})
xCoordinates := rnd.Perm(255)

View File

@@ -4,7 +4,7 @@ package shamir
// They use 0xe5 (229) as the generator
var (
// logTable provides the log(X)/log(g) at each index X
// logTable provides the log(X)/log(g) at each index X.
logTable = [256]uint8{
0x00, 0xff, 0xc8, 0x08, 0x91, 0x10, 0xd0, 0x36,
0x5a, 0x3e, 0xd8, 0x43, 0x99, 0x77, 0xfe, 0x18,
@@ -41,7 +41,7 @@ var (
}
// expTable provides the anti-log or exponentiation value
// for the equivalent index
// for the equivalent index.
expTable = [256]uint8{
0x01, 0xe5, 0x4c, 0xb5, 0xfb, 0x9f, 0xfc, 0x12,
0x03, 0x34, 0xd4, 0xc4, 0x16, 0xba, 0x1f, 0x36,

View File

@@ -66,7 +66,7 @@ func ParseAccount(ctx context.Context,
if unlock {
_, err = UnlockAccount(ctx, account, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to unlock account")
return nil, err
}
}
default:
@@ -82,7 +82,7 @@ func ParseAccount(ctx context.Context,
// Supplementary will be the unlock passphrase(s).
_, err = UnlockAccount(ctx, account, supplementary)
if err != nil {
return nil, errors.Wrap(err, "failed to unlock account")
return nil, err
}
}
case strings.Contains(accountStr, " "):

View File

@@ -33,7 +33,7 @@ func AttestationHeadCorrect(ctx context.Context,
for {
header, err := headersCache.Fetch(ctx, slot)
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.
@@ -63,7 +63,7 @@ func AttestationTargetCorrect(ctx context.Context,
for {
header, err := headersCache.Fetch(ctx, slot)
if err != nil {
return false, nil
return false, err
}
if header == nil {
// No block.

View File

@@ -1,4 +1,4 @@
// Copyright © 2032 Weald Technology Trading.
// Copyright © 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

View File

@@ -30,7 +30,7 @@ var Log zerolog.Logger
func InitLogging() error {
// Change the output file.
if viper.GetString("log-file") != "" {
f, err := os.OpenFile(viper.GetString("log-file"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
f, err := os.OpenFile(viper.GetString("log-file"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return errors.Wrap(err, "failed to open log file")
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020, 2022 Weald Technology Trading
// Copyright © 2020 - 2023 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -19,12 +19,25 @@ import (
"github.com/pkg/errors"
"github.com/tyler-smith/go-bip39"
"github.com/tyler-smith/go-bip39/wordlists"
"golang.org/x/text/unicode/norm"
)
// hdPathRegex is the regular expression that matches an HD path.
var hdPathRegex = regexp.MustCompile("^m/[0-9]+/[0-9]+(/[0-9+])+")
var mnemonicWordLists = [][]string{
wordlists.English,
wordlists.ChineseSimplified,
wordlists.ChineseTraditional,
wordlists.Czech,
wordlists.French,
wordlists.Italian,
wordlists.Japanese,
wordlists.Korean,
wordlists.Spanish,
}
// SeedFromMnemonic creates a seed from a mnemonic.
func SeedFromMnemonic(mnemonic string) ([]byte, error) {
// If there are more than 24 words we treat the additional characters as the passphrase.
@@ -38,10 +51,14 @@ func SeedFromMnemonic(mnemonic string) ([]byte, error) {
mnemonic = string(norm.NFKD.Bytes([]byte(mnemonic)))
mnemonicPassphrase = string(norm.NFKD.Bytes([]byte(mnemonicPassphrase)))
if !bip39.IsMnemonicValid(mnemonic) {
return nil, errors.New("mnemonic is invalid")
// Try with the various word lists.
for _, wl := range mnemonicWordLists {
bip39.SetWordList(wl)
seed, err := bip39.NewSeedWithErrorChecking(mnemonic, mnemonicPassphrase)
if err == nil {
return seed, nil
}
}
// Create seed from mnemonic and passphrase.
return bip39.NewSeed(mnemonic, mnemonicPassphrase), nil
return nil, errors.New("mnemonic is invalid")
}

72
util/mnemonic_test.go Normal file
View File

@@ -0,0 +1,72 @@
// Copyright © 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package util_test
import (
"encoding/hex"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/util"
)
func bytesStr(input string) []byte {
bytes, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
if err != nil {
panic(err)
}
return bytes
}
func TestSeedFromMnemonic(t *testing.T) {
tests := []struct {
name string
mnemonic string
seed []byte
err string
}{
{
name: "Empty",
err: "mnemonic is invalid",
},
{
name: "Default",
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",
seed: bytesStr("0x408b285c123836004f4b8842c89324c1f01382450c0d439af345ba7fc49acf705489c6fc77dbd4e3dc1dd8cc6bc9f043db8ada1e243c4a0eafb290d399480840"),
},
{
name: "English",
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",
seed: bytesStr("0x408b285c123836004f4b8842c89324c1f01382450c0d439af345ba7fc49acf705489c6fc77dbd4e3dc1dd8cc6bc9f043db8ada1e243c4a0eafb290d399480840"),
},
{
name: "Spanish",
mnemonic: "ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ábaco ancla",
seed: bytesStr("0x1e0de8aa97db3c7988f692d9c6151968be89debdbd71b1e34cab15d15ec10eed33412891129e1274fb84624565fd835f7e56df22a997439fca3da05c9c82a156"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
seed, err := util.SeedFromMnemonic(test.mnemonic)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.seed, seed)
}
})
}
}

View File

@@ -24,7 +24,7 @@ import (
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// SignRoot signs the hash tree root of a data structure
// SignRoot signs the hash tree root of a data structure.
func SignRoot(account e2wtypes.Account, root spec.Root, domain spec.Domain) (e2types.Signature, error) {
if _, isProtectingSigner := account.(e2wtypes.AccountProtectingSigner); isProtectingSigner {
// Signer builds the signing data.

View File

@@ -22,7 +22,6 @@ import (
)
func TestUnmarshal(t *testing.T) {
tests := []struct {
name string
in []byte