Compare commits

...

20 Commits

Author SHA1 Message Date
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
60 changed files with 486 additions and 122 deletions

View File

@@ -15,10 +15,12 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.19
go-version: 1.18
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
# https://github.com/golangci/golangci-lint-action/issues/535
version: v1.47.3
# version: latest
args: --timeout=60m

169
.golangci.yml Normal file
View File

@@ -0,0 +1,169 @@
# 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
- nestif
- nilnil
- nlreturn
- nolintlint
- nosnakecase
- promlinter
- rowserrcheck
- scopelint
- sqlclosecheck
- structcheck
- unparam
- varcheck
- varnamelen
- wastedassign
- wrapcheck
- wsl

View File

@@ -1,3 +1,9 @@
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)

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

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

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

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

@@ -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,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 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 {
@@ -127,6 +135,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")
}
@@ -242,14 +255,14 @@ func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
}
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 +326,32 @@ func (c *command) generateOperationsFromValidatorAndPrivateKey(ctx context.Conte
return nil
}
func (c *command) generateOperationsFromPrivateKey(ctx context.Context) error {
// 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
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
}
}
return nil
}
func (c *command) obtainOperationsFromFileOrInput(ctx context.Context) error {
// Start off by attempting to use the provided signed operations.
if c.signedOperationsInput != "" {
@@ -420,7 +459,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)
@@ -617,6 +655,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)

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

@@ -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
@@ -435,6 +443,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)

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

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

@@ -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.1)"
var ReleaseVersion = "local build (latest release 1.28.0)"
// 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".
@@ -201,6 +210,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:

View File

@@ -20,6 +20,7 @@ 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

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