diff --git a/CHANGELOG.md b/CHANGELOG.md index 07b45f5..d7680f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +dev: + - add "chain spec" command + - add "validator withdrawal" command + 1.29.2: - fix regression where validator index could not be used as an account specifier diff --git a/cmd/accountcreate.go b/cmd/accountcreate.go index b9009fb..c1d2006 100644 --- a/cmd/accountcreate.go +++ b/cmd/accountcreate.go @@ -51,11 +51,11 @@ func init() { accountCreateCmd.Flags().Uint32("signing-threshold", 1, "Signing threshold (1 for non-distributed accounts)") } -func accountCreateBindings() { - if err := viper.BindPFlag("participants", accountCreateCmd.Flags().Lookup("participants")); err != nil { +func accountCreateBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("participants", cmd.Flags().Lookup("participants")); err != nil { panic(err) } - if err := viper.BindPFlag("signing-threshold", accountCreateCmd.Flags().Lookup("signing-threshold")); err != nil { + if err := viper.BindPFlag("signing-threshold", cmd.Flags().Lookup("signing-threshold")); err != nil { panic(err) } } diff --git a/cmd/accountderive.go b/cmd/accountderive.go index 270d654..2d9910c 100644 --- a/cmd/accountderive.go +++ b/cmd/accountderive.go @@ -51,11 +51,11 @@ func init() { accountDeriveCmd.Flags().Bool("show-withdrawal-credentials", false, "show withdrawal credentials for derived account") } -func accountDeriveBindings() { - if err := viper.BindPFlag("show-private-key", accountDeriveCmd.Flags().Lookup("show-private-key")); err != nil { +func accountDeriveBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("show-private-key", cmd.Flags().Lookup("show-private-key")); err != nil { panic(err) } - if err := viper.BindPFlag("show-withdrawal-credentials", accountDeriveCmd.Flags().Lookup("show-withdrawal-credentials")); err != nil { + if err := viper.BindPFlag("show-withdrawal-credentials", cmd.Flags().Lookup("show-withdrawal-credentials")); err != nil { panic(err) } } diff --git a/cmd/accountimport.go b/cmd/accountimport.go index 14ff9e1..13591bc 100644 --- a/cmd/accountimport.go +++ b/cmd/accountimport.go @@ -52,14 +52,14 @@ func init() { accountImportCmd.Flags().String("keystore-passphrase", "", "Passphrase of keystore") } -func accountImportBindings() { - if err := viper.BindPFlag("key", accountImportCmd.Flags().Lookup("key")); err != nil { +func accountImportBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("key", cmd.Flags().Lookup("key")); err != nil { panic(err) } - if err := viper.BindPFlag("keystore", accountImportCmd.Flags().Lookup("keystore")); err != nil { + if err := viper.BindPFlag("keystore", cmd.Flags().Lookup("keystore")); err != nil { panic(err) } - if err := viper.BindPFlag("keystore-passphrase", accountImportCmd.Flags().Lookup("keystore-passphrase")); err != nil { + if err := viper.BindPFlag("keystore-passphrase", cmd.Flags().Lookup("keystore-passphrase")); err != nil { panic(err) } } diff --git a/cmd/accountinfo.go b/cmd/accountinfo.go index 2269fa3..d81d781 100644 --- a/cmd/accountinfo.go +++ b/cmd/accountinfo.go @@ -44,11 +44,11 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`, // Disallow wildcards (for now) assert(fmt.Sprintf("%s/%s", wallet.Name(), account.Name()) == viper.GetString("account"), "Mismatched account name") - if quiet { + if viper.GetBool("quiet") { os.Exit(_exitSuccess) } - outputIf(verbose, fmt.Sprintf("UUID: %v", account.ID())) + outputIf(viper.GetBool("verbose"), fmt.Sprintf("UUID: %v", account.ID())) var withdrawalPubKey e2types.PublicKey if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok { fmt.Printf("Public key: %#x\n", pubKeyProvider.PublicKey().Marshal()) @@ -58,7 +58,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`, if distributedAccount, ok := account.(e2wtypes.DistributedAccount); ok { fmt.Printf("Composite public key: %#x\n", distributedAccount.CompositePublicKey().Marshal()) fmt.Printf("Signing threshold: %d/%d\n", distributedAccount.SigningThreshold(), len(distributedAccount.Participants())) - if verbose { + if viper.GetBool("verbose") { fmt.Printf("Participants:\n") for k, v := range distributedAccount.Participants() { fmt.Printf(" %d: %s\n", k, v) @@ -67,7 +67,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`, withdrawalPubKey = distributedAccount.CompositePublicKey() } - if verbose { + if viper.GetBool("verbose") { withdrawalCredentials := util.SHA256(withdrawalPubKey.Marshal()) withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX fmt.Printf("Withdrawal credentials: %#x\n", withdrawalCredentials) diff --git a/cmd/attesterduties.go b/cmd/attesterduties.go index 57e1853..672e8f5 100644 --- a/cmd/attesterduties.go +++ b/cmd/attesterduties.go @@ -51,11 +51,11 @@ func init() { attesterDutiesCmd.Flags().String("validator", "", "the index, public key, or acount of the validator") } -func attesterDutiesBindings() { - if err := viper.BindPFlag("epoch", attesterDutiesCmd.Flags().Lookup("epoch")); err != nil { +func attesterDutiesBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("validator", attesterDutiesCmd.Flags().Lookup("validator")); err != nil { + if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil { panic(err) } } diff --git a/cmd/attesterinclusion.go b/cmd/attesterinclusion.go index ab5e2da..de2a113 100644 --- a/cmd/attesterinclusion.go +++ b/cmd/attesterinclusion.go @@ -52,14 +52,14 @@ func init() { attesterInclusionCmd.Flags().String("index", "", "the index of the attester") } -func attesterInclusionBindings() { - if err := viper.BindPFlag("epoch", attesterInclusionCmd.Flags().Lookup("epoch")); err != nil { +func attesterInclusionBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("validator", attesterInclusionCmd.Flags().Lookup("validator")); err != nil { + if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil { panic(err) } - if err := viper.BindPFlag("index", attesterInclusionCmd.Flags().Lookup("index")); err != nil { + if err := viper.BindPFlag("index", cmd.Flags().Lookup("index")); err != nil { panic(err) } } diff --git a/cmd/blockanalyze.go b/cmd/blockanalyze.go index 51129f6..5bc4839 100644 --- a/cmd/blockanalyze.go +++ b/cmd/blockanalyze.go @@ -51,11 +51,11 @@ func init() { blockAnalyzeCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive") } -func blockAnalyzeBindings() { - if err := viper.BindPFlag("blockid", blockAnalyzeCmd.Flags().Lookup("blockid")); err != nil { +func blockAnalyzeBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil { panic(err) } - if err := viper.BindPFlag("stream", blockAnalyzeCmd.Flags().Lookup("stream")); err != nil { + if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil { panic(err) } } diff --git a/cmd/blockinfo.go b/cmd/blockinfo.go index aa18d54..a20e2e7 100644 --- a/cmd/blockinfo.go +++ b/cmd/blockinfo.go @@ -52,14 +52,14 @@ func init() { blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format") } -func blockInfoBindings() { - if err := viper.BindPFlag("blockid", blockInfoCmd.Flags().Lookup("blockid")); err != nil { +func blockInfoBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil { panic(err) } - if err := viper.BindPFlag("stream", blockInfoCmd.Flags().Lookup("stream")); err != nil { + if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil { panic(err) } - if err := viper.BindPFlag("ssz", blockInfoCmd.Flags().Lookup("ssz")); err != nil { + if err := viper.BindPFlag("ssz", cmd.Flags().Lookup("ssz")); err != nil { panic(err) } } diff --git a/cmd/chaineth1votes.go b/cmd/chaineth1votes.go index f4d3900..6565369 100644 --- a/cmd/chaineth1votes.go +++ b/cmd/chaineth1votes.go @@ -53,11 +53,11 @@ func init() { chainEth1VotesCmd.Flags().String("period", "", "period for which to fetch the votes") } -func chainEth1VotesBindings() { - if err := viper.BindPFlag("epoch", chainEth1VotesCmd.Flags().Lookup("epoch")); err != nil { +func chainEth1VotesBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("period", chainEth1VotesCmd.Flags().Lookup("period")); err != nil { + if err := viper.BindPFlag("period", cmd.Flags().Lookup("period")); err != nil { panic(err) } } diff --git a/cmd/chaininfo.go b/cmd/chaininfo.go index 724e9a5..149b51f 100644 --- a/cmd/chaininfo.go +++ b/cmd/chaininfo.go @@ -54,7 +54,7 @@ In quiet mode this will return 0 if the chain information can be obtained, other fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head") errCheck(err, "Failed to obtain current fork") - if quiet { + if viper.GetBool("quiet") { os.Exit(_exitSuccess) } @@ -62,12 +62,12 @@ In quiet mode this will return 0 if the chain information can be obtained, other fmt.Println("Genesis time: undefined") } else { fmt.Printf("Genesis time: %s\n", genesis.GenesisTime.Format(time.UnixDate)) - outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix())) + outputIf(viper.GetBool("verbose"), fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix())) } fmt.Printf("Genesis validators root: %#x\n", genesis.GenesisValidatorsRoot) fmt.Printf("Genesis fork version: %#x\n", config["GENESIS_FORK_VERSION"].(spec.Version)) fmt.Printf("Current fork version: %#x\n", fork.CurrentVersion) - if verbose { + if viper.GetBool("verbose") { forkData := &spec.ForkData{ CurrentVersion: fork.CurrentVersion, GenesisValidatorsRoot: genesis.GenesisValidatorsRoot, @@ -91,5 +91,5 @@ func init() { chainFlags(chainInfoCmd) } -func chainInfoBindings() { +func chainInfoBindings(_ *cobra.Command) { } diff --git a/cmd/chainqueues.go b/cmd/chainqueues.go index 27aa569..55e7bbe 100644 --- a/cmd/chainqueues.go +++ b/cmd/chainqueues.go @@ -50,8 +50,8 @@ func init() { chainQueuesCmd.Flags().String("epoch", "", "epoch for which to fetch the queues") } -func chainQueuesBindings() { - if err := viper.BindPFlag("epoch", chainQueuesCmd.Flags().Lookup("epoch")); err != nil { +func chainQueuesBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } } diff --git a/cmd/chainspec.go b/cmd/chainspec.go new file mode 100644 index 0000000..40f85d2 --- /dev/null +++ b/cmd/chainspec.go @@ -0,0 +1,97 @@ +// Copyright © 2023 Weald Technology Trading. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/wealdtech/ethdo/util" +) + +var chainSpecCmd = &cobra.Command{ + Use: "spec", + Short: "Obtain specification for a chain", + Long: `Obtain specification for a chain. For example: + + ethdo chain spec + +In quiet mode this will return 0 if the chain specification can be obtained, otherwise 1.`, + Run: func(cmd *cobra.Command, args []string) { + ctx := context.Background() + + eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !viper.GetBool("quiet"), + }) + errCheck(err, "Failed to connect to Ethereum consensus node") + + spec, err := eth2Client.(eth2client.SpecProvider).Spec(ctx) + errCheck(err, "Failed to obtain chain specification") + + if viper.GetBool("quiet") { + return + } + + // Tweak the spec for output. + for k, v := range spec { + switch t := v.(type) { + case phase0.Version: + spec[k] = fmt.Sprintf("%#x", t) + case phase0.DomainType: + spec[k] = fmt.Sprintf("%#x", t) + case time.Time: + spec[k] = fmt.Sprintf("%d", t.Unix()) + case time.Duration: + spec[k] = fmt.Sprintf("%d", uint64(t.Seconds())) + case []byte: + spec[k] = fmt.Sprintf("%#x", t) + case uint64: + spec[k] = fmt.Sprintf("%d", t) + } + } + + if viper.GetBool("json") { + data, err := json.Marshal(spec) + errCheck(err, "Failed to marshal JSON") + fmt.Printf("%s\n", string(data)) + } else { + keys := make([]string, 0, len(spec)) + for k := range spec { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { + fmt.Printf("%s: %v\n", key, spec[key]) + } + } + }, +} + +func init() { + chainCmd.AddCommand(chainSpecCmd) + chainFlags(chainSpecCmd) +} + +func chainSpecBindings(_ *cobra.Command) { +} diff --git a/cmd/chainstatus.go b/cmd/chainstatus.go index ce41c3c..4cc1a59 100644 --- a/cmd/chainstatus.go +++ b/cmd/chainstatus.go @@ -83,7 +83,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise res.WriteString(fmt.Sprintf("%d", epoch)) res.WriteString("\n") - if verbose { + if viper.GetBool("verbose") { res.WriteString("Epoch slots: ") res.WriteString(fmt.Sprintf("%d", epochStartSlot)) res.WriteString("-") @@ -106,7 +106,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise res.WriteString("Justified epoch: ") res.WriteString(fmt.Sprintf("%d", finality.Justified.Epoch)) res.WriteString("\n") - if verbose { + if viper.GetBool("verbose") { distance := epoch - finality.Justified.Epoch res.WriteString("Justified epoch distance: ") res.WriteString(fmt.Sprintf("%d", distance)) @@ -116,14 +116,14 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise res.WriteString("Finalized epoch: ") res.WriteString(fmt.Sprintf("%d", finality.Finalized.Epoch)) res.WriteString("\n") - if verbose { + if viper.GetBool("verbose") { distance := epoch - finality.Finalized.Epoch res.WriteString("Finalized epoch distance: ") res.WriteString(fmt.Sprintf("%d", distance)) res.WriteString("\n") } - if verbose { + if viper.GetBool("verbose") { validatorsProvider, isProvider := eth2Client.(eth2client.ValidatorsProvider) if isProvider { validators, err := validatorsProvider.Validators(ctx, "head", nil) @@ -165,7 +165,7 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise res.WriteString(fmt.Sprintf("%d", period)) res.WriteString("\n") - if verbose { + if viper.GetBool("verbose") { res.WriteString("Sync committee epochs: ") res.WriteString(fmt.Sprintf("%d", periodStartEpoch)) res.WriteString("-") diff --git a/cmd/chaintime.go b/cmd/chaintime.go index eed50c8..64c6f29 100644 --- a/cmd/chaintime.go +++ b/cmd/chaintime.go @@ -47,14 +47,14 @@ func init() { chainTimeCmd.Flags().String("timestamp", "", "The timestamp for which to obtain information (format YYYY-MM-DDTHH:MM:SS+ZZZZ)") } -func chainTimeBindings() { - if err := viper.BindPFlag("slot", chainTimeCmd.Flags().Lookup("slot")); err != nil { +func chainTimeBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("slot", cmd.Flags().Lookup("slot")); err != nil { panic(err) } - if err := viper.BindPFlag("epoch", chainTimeCmd.Flags().Lookup("epoch")); err != nil { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("timestamp", chainTimeCmd.Flags().Lookup("timestamp")); err != nil { + if err := viper.BindPFlag("timestamp", cmd.Flags().Lookup("timestamp")); err != nil { panic(err) } } diff --git a/cmd/depositverify.go b/cmd/depositverify.go index 59b156d..4a8cd2a 100644 --- a/cmd/depositverify.go +++ b/cmd/depositverify.go @@ -23,6 +23,7 @@ import ( "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/wealdtech/ethdo/util" e2types "github.com/wealdtech/go-eth2-types/v2" eth2util "github.com/wealdtech/go-eth2-util" @@ -92,7 +93,7 @@ In quiet mode this will return 0 if the data is verified correctly, otherwise 1. withdrawalCredentials[0] = 0x01 // ETH1_ADDRESS_WITHDRAWAL_PREFIX copy(withdrawalCredentials[12:], withdrawalAddressBytes) } - outputIf(debug, fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials)) + outputIf(viper.GetBool("debug"), fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials)) depositAmount := uint64(0) if depositVerifyDepositAmount != "" { @@ -120,9 +121,9 @@ In quiet mode this will return 0 if the data is verified correctly, otherwise 1. } if !verified { failures = true - outputIf(!quiet, fmt.Sprintf("%s failed verification", depositName)) + outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%s failed verification", depositName)) } else { - outputIf(!quiet, fmt.Sprintf("%s verified", depositName)) + outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%s verified", depositName)) } } @@ -190,34 +191,34 @@ func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) { func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, validatorPubKeys map[[48]byte]bool, amount uint64) (bool, error) { if withdrawalCredentials == nil { - outputIf(!quiet, "Withdrawal public key or address not supplied; withdrawal credentials NOT checked") + outputIf(!viper.GetBool("quiet"), "Withdrawal public key or address not supplied; withdrawal credentials NOT checked") } else { if !bytes.Equal(deposit.WithdrawalCredentials, withdrawalCredentials) { - outputIf(!quiet, "Withdrawal credentials incorrect") + outputIf(!viper.GetBool("quiet"), "Withdrawal credentials incorrect") return false, nil } - outputIf(!quiet, "Withdrawal credentials verified") + outputIf(!viper.GetBool("quiet"), "Withdrawal credentials verified") } if amount == 0 { - outputIf(!quiet, "Amount not supplied; NOT checked") + outputIf(!viper.GetBool("quiet"), "Amount not supplied; NOT checked") } else { if deposit.Amount != amount { - outputIf(!quiet, "Amount incorrect") + outputIf(!viper.GetBool("quiet"), "Amount incorrect") return false, nil } - outputIf(!quiet, "Amount verified") + outputIf(!viper.GetBool("quiet"), "Amount verified") } if len(validatorPubKeys) == 0 { - outputIf(!quiet, "Validator public key not suppled; NOT checked") + outputIf(!viper.GetBool("quiet"), "Validator public key not suppled; NOT checked") } else { var key [48]byte copy(key[:], deposit.PublicKey) if _, exists := validatorPubKeys[key]; !exists { - outputIf(!quiet, "Validator public key incorrect") + outputIf(!viper.GetBool("quiet"), "Validator public key incorrect") return false, nil } - outputIf(!quiet, "Validator public key verified") + outputIf(!viper.GetBool("quiet"), "Validator public key verified") } var pubKey phase0.BLSPubKey @@ -237,33 +238,33 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali } if bytes.Equal(deposit.DepositDataRoot, depositDataRoot[:]) { - outputIf(!quiet, "Deposit data root verified") + outputIf(!viper.GetBool("quiet"), "Deposit data root verified") } else { - outputIf(!quiet, "Deposit data root incorrect") + outputIf(!viper.GetBool("quiet"), "Deposit data root incorrect") return false, nil } if len(deposit.ForkVersion) == 0 { if depositVerifyForkVersion != "" { - outputIf(!quiet, "Data format does not contain fork version for verification; NOT verified") + outputIf(!viper.GetBool("quiet"), "Data format does not contain fork version for verification; NOT verified") } } else { if depositVerifyForkVersion == "" { - outputIf(!quiet, "fork version not supplied; not checked") + outputIf(!viper.GetBool("quiet"), "fork version not supplied; not checked") } else { forkVersion, err := hex.DecodeString(strings.TrimPrefix(depositVerifyForkVersion, "0x")) if err != nil { return false, errors.Wrap(err, "failed to decode fork version") } if bytes.Equal(deposit.ForkVersion, forkVersion) { - outputIf(!quiet, "Fork version verified") + outputIf(!viper.GetBool("quiet"), "Fork version verified") } else { - outputIf(!quiet, "Fork version incorrect") + outputIf(!viper.GetBool("quiet"), "Fork version incorrect") return false, nil } if len(deposit.DepositMessageRoot) != 32 { - outputIf(!quiet, "Deposit message root not supplied; not checked") + outputIf(!viper.GetBool("quiet"), "Deposit message root not supplied; not checked") } else { // We can also verify the deposit message signature. depositMessage := &phase0.DepositMessage{ @@ -277,9 +278,9 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali } if bytes.Equal(deposit.DepositMessageRoot, depositMessageRoot[:]) { - outputIf(!quiet, "Deposit message root verified") + outputIf(!viper.GetBool("quiet"), "Deposit message root verified") } else { - outputIf(!quiet, "Deposit message root incorrect") + outputIf(!viper.GetBool("quiet"), "Deposit message root incorrect") return false, nil } @@ -305,9 +306,9 @@ func verifyDeposit(deposit *util.DepositInfo, withdrawalCredentials []byte, vali } signatureVerified := blsSig.Verify(containerRoot[:], validatorPubKey) if signatureVerified { - outputIf(!quiet, "Deposit message signature verified") + outputIf(!viper.GetBool("quiet"), "Deposit message signature verified") } else { - outputIf(!quiet, "Deposit message signature NOT verified") + outputIf(!viper.GetBool("quiet"), "Deposit message signature NOT verified") return false, nil } } diff --git a/cmd/epoch.go b/cmd/epoch.go index 227a8ca..e566c62 100644 --- a/cmd/epoch.go +++ b/cmd/epoch.go @@ -33,8 +33,8 @@ func epochFlags(_ *cobra.Command) { epochSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information (default current, can be 'current', 'last' or a number)") } -func epochBindings() { - if err := viper.BindPFlag("epoch", epochSummaryCmd.Flags().Lookup("epoch")); err != nil { +func epochBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } } diff --git a/cmd/epochsummary.go b/cmd/epochsummary.go index 7f99b7a..bbc2886 100644 --- a/cmd/epochsummary.go +++ b/cmd/epochsummary.go @@ -49,6 +49,6 @@ func init() { epochFlags(epochSummaryCmd) } -func epochSummaryBindings() { - epochBindings() +func epochSummaryBindings(cmd *cobra.Command) { + epochBindings(cmd) } diff --git a/cmd/err.go b/cmd/err.go index 062a334..5b92a9e 100644 --- a/cmd/err.go +++ b/cmd/err.go @@ -16,12 +16,14 @@ package cmd import ( "fmt" "os" + + "github.com/spf13/viper" ) // errCheck checks for an error and quits if it is present. func errCheck(err error, msg string) { if err != nil { - if !quiet { + if !viper.GetBool("quiet") { if msg == "" { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) } else { @@ -57,7 +59,7 @@ func assert(condition bool, msg string) { // die prints an error and quits. func die(msg string) { - if msg != "" && !quiet { + if msg != "" && !viper.GetBool("quiet") { fmt.Fprintf(os.Stderr, "%s\n", msg) } os.Exit(_exitFailure) diff --git a/cmd/exitverify.go b/cmd/exitverify.go index 28578ad..79b5787 100644 --- a/cmd/exitverify.go +++ b/cmd/exitverify.go @@ -98,7 +98,7 @@ In quiet mode this will return 0 if the exit is verified correctly, otherwise 1. } assert(verified, "Voluntary exit failed to verify against current and previous fork versions") - outputIf(verbose, "Verified") + outputIf(viper.GetBool("verbose"), "Verified") os.Exit(_exitSuccess) }, } @@ -133,8 +133,8 @@ func init() { exitVerifyCmd.Flags().String("signed-operation", "", "JSON data, or path to JSON data") } -func exitVerifyBindings() { - if err := viper.BindPFlag("signed-operation", exitVerifyCmd.Flags().Lookup("signed-operation")); err != nil { +func exitVerifyBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("signed-operation", cmd.Flags().Lookup("signed-operation")); err != nil { panic(err) } } diff --git a/cmd/nodeevents.go b/cmd/nodeevents.go index 2adfe40..0896115 100644 --- a/cmd/nodeevents.go +++ b/cmd/nodeevents.go @@ -45,8 +45,8 @@ func init() { nodeEventsCmd.Flags().StringSlice("topics", nil, "The topics of events for which to listen (attestation,block,chain_reorg,finalized_checkpoint,head,voluntary_exit)") } -func nodeEventsBindings() { - if err := viper.BindPFlag("topics", nodeEventsCmd.Flags().Lookup("topics")); err != nil { +func nodeEventsBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("topics", cmd.Flags().Lookup("topics")); err != nil { panic(err) } } diff --git a/cmd/nodeinfo.go b/cmd/nodeinfo.go index 303d2ff..283ed4f 100644 --- a/cmd/nodeinfo.go +++ b/cmd/nodeinfo.go @@ -43,11 +43,11 @@ In quiet mode this will return 0 if the node information can be obtained, otherw }) errCheck(err, "Failed to connect to Ethereum 2 beacon node") - if quiet { + if viper.GetBool("quiet") { os.Exit(_exitSuccess) } - if verbose { + if viper.GetBool("verbose") { version, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx) errCheck(err, "Failed to obtain node version") fmt.Printf("Version: %s\n", version) diff --git a/cmd/proposerduties.go b/cmd/proposerduties.go index 61682d1..e3e8e0d 100644 --- a/cmd/proposerduties.go +++ b/cmd/proposerduties.go @@ -50,8 +50,8 @@ func init() { proposerDutiesCmd.Flags().String("epoch", "", "the epoch for which to fetch duties") } -func proposerDutiesBindings() { - if err := viper.BindPFlag("epoch", proposerDutiesCmd.Flags().Lookup("epoch")); err != nil { +func proposerDutiesBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } } diff --git a/cmd/root.go b/cmd/root.go index 59c5d97..fb0e811 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,4 +1,4 @@ -// Copyright © 2019 - 2021 Weald Technology Trading. +// Copyright © 2019 - 2023 Weald Technology Trading. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -33,21 +33,55 @@ import ( e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) -var ( - cfgFile string - quiet bool - verbose bool - debug bool -) +var cfgFile string // RootCmd represents the base command when called without any subcommands. var RootCmd = &cobra.Command{ Use: "ethdo", - Short: "Ethereum 2 CLI", - Long: `Manage common Ethereum 2 tasks from the command line.`, + Short: "Ethereum consensus layer CLI", + Long: `Manage common Ethereum consensus layer tasks from the command line.`, PersistentPreRunE: persistentPreRunE, } +// bindings are the command-specific bindings. +var bindings = map[string]func(cmd *cobra.Command){ + "account/create": accountCreateBindings, + "account/derive": accountDeriveBindings, + "account/import": accountImportBindings, + "attester/duties": attesterDutiesBindings, + "attester/inclusion": attesterInclusionBindings, + "block/analyze": blockAnalyzeBindings, + "block/info": blockInfoBindings, + "chain/eth1votes": chainEth1VotesBindings, + "chain/info": chainInfoBindings, + "chain/queues": chainQueuesBindings, + "chain/spec": chainSpecBindings, + "chain/time": chainTimeBindings, + "chain/verify/signedcontributionandproof": chainVerifySignedContributionAndProofBindings, + "epoch/summary": epochSummaryBindings, + "exit/verify": exitVerifyBindings, + "node/events": nodeEventsBindings, + "proposer/duties": proposerDutiesBindings, + "slot/time": slotTimeBindings, + "synccommittee/inclusion": synccommitteeInclusionBindings, + "synccommittee/members": synccommitteeMembersBindings, + "validator/credentials/get": validatorCredentialsGetBindings, + "validator/credentials/set": validatorCredentialsSetBindings, + "validator/depositdata": validatorDepositdataBindings, + "validator/duties": validatorDutiesBindings, + "validator/exit": validatorExitBindings, + "validator/info": validatorInfoBindings, + "validator/keycheck": validatorKeycheckBindings, + "validator/summary": validatorSummaryBindings, + "validator/yield": validatorYieldBindings, + "validator/expectation": validatorExpectationBindings, + "validator/withdrawal": validatorWithdrawalBindings, + "wallet/create": walletCreateBindings, + "wallet/import": walletImportBindings, + "wallet/sharedexport": walletSharedExportBindings, + "wallet/sharedimport": walletSharedImportBindings, +} + func persistentPreRunE(cmd *cobra.Command, _ []string) error { if cmd.Name() == "help" { // User just wants help @@ -63,11 +97,14 @@ func persistentPreRunE(cmd *cobra.Command, _ []string) error { zerolog.SetGlobalLevel(zerolog.Disabled) // We bind viper here so that we bind to the correct command. - quiet = viper.GetBool("quiet") - verbose = viper.GetBool("verbose") - debug = viper.GetBool("debug") + quiet := viper.GetBool("quiet") + verbose := viper.GetBool("verbose") + debug := viper.GetBool("debug") - includeCommandBindings(cmd) + // Command-specific bindings. + if bindingsFunc, exists := bindings[commandPath(cmd)]; exists { + bindingsFunc(cmd) + } if quiet && verbose { fmt.Println("Cannot supply both quiet and verbose flags") @@ -79,78 +116,6 @@ func persistentPreRunE(cmd *cobra.Command, _ []string) error { return util.SetupStore() } -// nolint:gocyclo -func includeCommandBindings(cmd *cobra.Command) { - switch commandPath(cmd) { - case "account/create": - accountCreateBindings() - case "account/derive": - accountDeriveBindings() - case "account/import": - accountImportBindings() - case "attester/duties": - attesterDutiesBindings() - case "attester/inclusion": - attesterInclusionBindings() - case "block/analyze": - blockAnalyzeBindings() - case "block/info": - blockInfoBindings() - case "chain/info": - chainInfoBindings() - case "chain/queues": - chainQueuesBindings() - case "chain/spec": - chainSpecBindings() - case "chain/time": - chainTimeBindings() - case "chain/verify/signedcontributionandproof": - chainVerifySignedContributionAndProofBindings(cmd) - case "epoch/summary": - epochSummaryBindings() - case "exit/verify": - exitVerifyBindings() - case "node/events": - nodeEventsBindings() - case "proposer/duties": - proposerDutiesBindings() - case "slot/time": - slotTimeBindings() - case "synccommittee/inclusion": - synccommitteeInclusionBindings() - case "synccommittee/members": - synccommitteeMembersBindings() - case "validator/credentials/get": - validatorCredentialsGetBindings() - case "validator/credentials/set": - validatorCredentialsSetBindings() - case "validator/depositdata": - validatorDepositdataBindings() - case "validator/duties": - validatorDutiesBindings() - case "validator/exit": - validatorExitBindings() - case "validator/info": - validatorInfoBindings() - case "validator/keycheck": - validatorKeycheckBindings() - case "validator/summary": - validatorSummaryBindings() - case "validator/yield": - validatorYieldBindings() - case "validator/expectation": - validatorExpectationBindings() - case "wallet/create": - walletCreateBindings() - case "wallet/import": - walletImportBindings() - case "wallet/sharedexport": - walletSharedExportBindings() - case "wallet/sharedimport": - walletSharedImportBindings() - } -} - // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { @@ -167,8 +132,12 @@ func init() { } cobra.OnInitialize(initConfig) + addPersistentFlags() +} +func addPersistentFlags() { RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.ethdo.yaml)") + RootCmd.PersistentFlags().String("log", "", "log activity to the named file (default $HOME/ethdo.log). Logs are written for every action that generates a transaction") if err := viper.BindPFlag("log", RootCmd.PersistentFlags().Lookup("log")); err != nil { panic(err) diff --git a/cmd/signatureaggregate.go b/cmd/signatureaggregate.go index db27eb7..31c7427 100644 --- a/cmd/signatureaggregate.go +++ b/cmd/signatureaggregate.go @@ -23,6 +23,7 @@ import ( "github.com/herumi/bls-eth-go-binary/bls" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/wealdtech/ethdo/util" ) @@ -50,7 +51,7 @@ In quiet mode this will return 0 if the signatures can be aggregated, otherwise } errCheck(err, "Failed to aggregate signature") - outputIf(!quiet, fmt.Sprintf("%#x", signature.Serialize())) + outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%#x", signature.Serialize())) os.Exit(_exitSuccess) }, } diff --git a/cmd/signaturesign.go b/cmd/signaturesign.go index 29f3cbb..f8379fb 100644 --- a/cmd/signaturesign.go +++ b/cmd/signaturesign.go @@ -51,7 +51,7 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`, errCheck(err, "Failed to parse domain") assert(len(domain) == 32, "Domain data invalid") } - outputIf(debug, fmt.Sprintf("Domain is %#x", domain)) + outputIf(viper.GetBool("debug"), fmt.Sprintf("Domain is %#x", domain)) var account e2wtypes.Account switch { @@ -70,7 +70,7 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`, signature, err := util.SignRoot(account, fixedSizeData, specDomain) errCheck(err, "Failed to sign") - outputIf(!quiet, fmt.Sprintf("%#x", signature.Marshal())) + outputIf(!viper.GetBool("quiet"), fmt.Sprintf("%#x", signature.Marshal())) os.Exit(_exitSuccess) }, } diff --git a/cmd/signatureverify.go b/cmd/signatureverify.go index c1058e4..fd45ba8 100644 --- a/cmd/signatureverify.go +++ b/cmd/signatureverify.go @@ -73,7 +73,7 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`, account, err = util.ParseAccount(ctx, viper.GetString("public-key"), nil, false) } errCheck(err, "Failed to obtain account") - outputIf(debug, fmt.Sprintf("Public key is %#x", account.PublicKey().Marshal())) + outputIf(viper.GetBool("debug"), fmt.Sprintf("Public key is %#x", account.PublicKey().Marshal())) var specDomain spec.Domain copy(specDomain[:], domain) @@ -83,7 +83,7 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`, errCheck(err, "Failed to verify data") assert(verified, "Failed to verify") - outputIf(verbose, "Verified") + outputIf(viper.GetBool("verbose"), "Verified") os.Exit(_exitSuccess) }, } diff --git a/cmd/slottime.go b/cmd/slottime.go index f847a98..b83ffdf 100644 --- a/cmd/slottime.go +++ b/cmd/slottime.go @@ -50,8 +50,8 @@ func init() { slotTimeCmd.Flags().String("slot", "", "the ID of the slot to fetch") } -func slotTimeBindings() { - if err := viper.BindPFlag("slot", slotTimeCmd.Flags().Lookup("slot")); err != nil { +func slotTimeBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("slot", cmd.Flags().Lookup("slot")); err != nil { panic(err) } } diff --git a/cmd/synccommitteeinclusion.go b/cmd/synccommitteeinclusion.go index 8dc692a..e0756e8 100644 --- a/cmd/synccommitteeinclusion.go +++ b/cmd/synccommitteeinclusion.go @@ -53,11 +53,11 @@ func init() { synccommitteeInclusionCmd.Flags().String("validator", "", "the index, public key, or acount of the validator") } -func synccommitteeInclusionBindings() { - if err := viper.BindPFlag("epoch", synccommitteeInclusionCmd.Flags().Lookup("epoch")); err != nil { +func synccommitteeInclusionBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("validator", synccommitteeInclusionCmd.Flags().Lookup("validator")); err != nil { + if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil { panic(err) } } diff --git a/cmd/synccommitteemembers.go b/cmd/synccommitteemembers.go index 8063a54..9706e4b 100644 --- a/cmd/synccommitteemembers.go +++ b/cmd/synccommitteemembers.go @@ -53,11 +53,11 @@ func init() { synccommitteeMembersCmd.Flags().String("period", "", "the sync committee period for which to fetch sync committees ('current', 'next')") } -func synccommitteeMembersBindings() { - if err := viper.BindPFlag("epoch", synccommitteeMembersCmd.Flags().Lookup("epoch")); err != nil { +func synccommitteeMembersBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("period", synccommitteeMembersCmd.Flags().Lookup("period")); err != nil { + if err := viper.BindPFlag("period", cmd.Flags().Lookup("period")); err != nil { panic(err) } } diff --git a/cmd/validator/withdrawal/command.go b/cmd/validator/withdrawal/command.go new file mode 100644 index 0000000..65c068b --- /dev/null +++ b/cmd/validator/withdrawal/command.go @@ -0,0 +1,108 @@ +// 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 validatorwithdrawl + +import ( + "context" + "encoding/json" + "time" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/spf13/viper" + "github.com/wealdtech/ethdo/services/chaintime" +) + +type command struct { + quiet bool + verbose bool + debug bool + offline bool + json bool + + // Input. + validator string + + // Beacon node connection. + timeout time.Duration + connection string + allowInsecureConnections bool + + // Processing. + consensusClient consensusclient.Service + chainTime chaintime.Service + maxWithdrawalsPerPayload uint64 + maxEffectiveBalance phase0.Gwei + + // Output. + res *res +} + +func newCommand(_ context.Context) (*command, error) { + c := &command{ + quiet: viper.GetBool("quiet"), + verbose: viper.GetBool("verbose"), + debug: viper.GetBool("debug"), + offline: viper.GetBool("offline"), + json: viper.GetBool("json"), + timeout: viper.GetDuration("timeout"), + connection: viper.GetString("connection"), + allowInsecureConnections: viper.GetBool("allow-insecure-connections"), + validator: viper.GetString("validator"), + res: &res{}, + } + + // Timeout is required. + if c.timeout == 0 { + return nil, errors.New("timeout is required") + } + + if c.validator == "" { + return nil, errors.New("validator is required") + } + + return c, nil +} + +type res struct { + WithdrawalsToGo uint64 + BlocksToGo uint64 + Block uint64 + Wait time.Duration + Expected time.Time +} + +type resJSON struct { + WithdrawalsToGo uint64 `json:"withdrawals_to_go"` + BlocksToGo uint64 `json:"blocks_to_go"` + Block uint64 `json:"block"` + Wait string `json:"wait"` + WaitSecs uint64 `json:"wait_secs"` + Expected string `json:"expected"` + ExpectedTimestamp int64 `json:"expected_timestamp"` +} + +func (r *res) MarshalJSON() ([]byte, error) { + data := resJSON{ + WithdrawalsToGo: r.WithdrawalsToGo, + BlocksToGo: r.BlocksToGo, + Block: r.Block, + Wait: r.Wait.Round(time.Second).String(), + WaitSecs: uint64(r.Wait.Round(time.Second).Seconds()), + Expected: r.Expected.Format("2006-01-02T15:04:05"), + ExpectedTimestamp: r.Expected.Unix(), + } + return json.Marshal(data) +} diff --git a/cmd/validator/withdrawal/output.go b/cmd/validator/withdrawal/output.go new file mode 100644 index 0000000..1e84d39 --- /dev/null +++ b/cmd/validator/withdrawal/output.go @@ -0,0 +1,39 @@ +// 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 validatorwithdrawl + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/pkg/errors" +) + +//nolint:unparam +func (c *command) output(_ context.Context) (string, error) { + if c.quiet { + return "", nil + } + + if c.json { + data, err := json.Marshal(c.res) + if err != nil { + return "", errors.Wrap(err, "failed to marshal results") + } + return string(data), nil + } + + return fmt.Sprintf("Withdrawal expected at %s in block %d", c.res.Expected.Format("2006-01-02T15:04:05"), c.res.Block), nil +} diff --git a/cmd/validator/withdrawal/process.go b/cmd/validator/withdrawal/process.go new file mode 100644 index 0000000..1e7066c --- /dev/null +++ b/cmd/validator/withdrawal/process.go @@ -0,0 +1,155 @@ +// 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 validatorwithdrawl + +import ( + "context" + "fmt" + "os" + "time" + + consensusclient "github.com/attestantio/go-eth2-client" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard" + "github.com/wealdtech/ethdo/util" +) + +const ( + ethWithdrawalPrefix = 0x01 +) + +func (c *command) process(ctx context.Context) error { + if err := c.setup(ctx); err != nil { + return err + } + + validator, err := util.ParseValidator(ctx, c.consensusClient.(consensusclient.ValidatorsProvider), c.validator, "head") + if err != nil { + return errors.Wrap(err, "failed to parse validator") + } + + if validator.Validator.WithdrawalCredentials[0] != ethWithdrawalPrefix { + return errors.New("validator does not have suitable withdrawal credentials") + } + if validator.Balance == 0 { + return errors.New("validator has nothing to withdraw") + } + + block, err := c.consensusClient.(consensusclient.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, "head") + if err != nil { + return errors.Wrap(err, "failed to obtain block") + } + slot, err := block.Slot() + if err != nil { + return errors.Wrap(err, "failed to obtain block slot") + } + if c.debug { + fmt.Fprintf(os.Stderr, "Slot is %d\n", slot) + } + + validatorsMap, err := c.consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, fmt.Sprintf("%d", slot), nil) + if err != nil { + return errors.Wrap(err, "failed to obtain validators") + } + validators := make([]*apiv1.Validator, len(validatorsMap)) + for _, validator := range validatorsMap { + validators[validator.Index] = validator + } + + var nextWithdrawalValidatorIndex phase0.ValidatorIndex + switch block.Version { + case spec.DataVersionCapella: + withdrawals := block.Capella.Message.Body.ExecutionPayload.Withdrawals + if len(withdrawals) == 0 { + return errors.New("block without withdrawals; cannot obtain next withdrawal validator index") + } + nextWithdrawalValidatorIndex = phase0.ValidatorIndex((int(withdrawals[len(withdrawals)-1].ValidatorIndex) + 1) % len(validators)) + default: + return fmt.Errorf("unhandled block version %v", block.Version) + } + + if c.debug { + fmt.Fprintf(os.Stderr, "Next withdrawal validator index is %d\n", nextWithdrawalValidatorIndex) + } + + index := int(nextWithdrawalValidatorIndex) + for { + if index == len(validators) { + index = 0 + } + if index == int(validator.Index) { + break + } + if validators[index].Validator.WithdrawalCredentials[0] == ethWithdrawalPrefix && + validators[index].Validator.EffectiveBalance == c.maxEffectiveBalance { + c.res.WithdrawalsToGo++ + } + index++ + } + + c.res.BlocksToGo = c.res.WithdrawalsToGo / c.maxWithdrawalsPerPayload + if c.res.WithdrawalsToGo%c.maxWithdrawalsPerPayload != 0 { + c.res.BlocksToGo++ + } + c.res.Block = uint64(slot) + c.res.BlocksToGo + c.res.Expected = c.chainTime.StartOfSlot(phase0.Slot(c.res.Block)) + c.res.Wait = time.Until(c.res.Expected) + + return nil +} + +func (c *command) setup(ctx context.Context) error { + // Connect to the consensus node. + var err error + c.consensusClient, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) + if err != nil { + return err + } + + // Set up chaintime. + c.chainTime, err = standardchaintime.New(ctx, + standardchaintime.WithGenesisTimeProvider(c.consensusClient.(consensusclient.GenesisTimeProvider)), + standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)), + ) + if err != nil { + return errors.Wrap(err, "failed to create chaintime service") + } + + spec, err := c.consensusClient.(consensusclient.SpecProvider).Spec(ctx) + if err != nil { + return errors.Wrap(err, "failed to obtain spec") + } + + if val, exists := spec["MAX_WITHDRAWALS_PER_PAYLOAD"]; !exists { + c.maxWithdrawalsPerPayload = 16 + } else { + c.maxWithdrawalsPerPayload = val.(uint64) + } + + if val, exists := spec["MAX_EFFECTIVE_BALANCE"]; !exists { + c.maxEffectiveBalance = 32000000000 + } else { + c.maxEffectiveBalance = phase0.Gwei(val.(uint64)) + } + + return nil +} diff --git a/cmd/validator/withdrawal/run.go b/cmd/validator/withdrawal/run.go new file mode 100644 index 0000000..066923e --- /dev/null +++ b/cmd/validator/withdrawal/run.go @@ -0,0 +1,50 @@ +// 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 validatorwithdrawl + +import ( + "context" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Run runs the command. +func Run(cmd *cobra.Command) (string, error) { + ctx := context.Background() + + c, err := newCommand(ctx) + if err != nil { + return "", errors.Wrap(err, "failed to set up command") + } + + // Further errors do not need a usage report. + cmd.SilenceUsage = true + + if err := c.process(ctx); err != nil { + return "", errors.Wrap(err, "failed to process") + } + + if viper.GetBool("quiet") { + return "", nil + } + + results, err := c.output(ctx) + if err != nil { + return "", errors.Wrap(err, "failed to obtain output") + } + + return results, nil +} diff --git a/cmd/validatorcredentialsget.go b/cmd/validatorcredentialsget.go index 7cd0344..b731540 100644 --- a/cmd/validatorcredentialsget.go +++ b/cmd/validatorcredentialsget.go @@ -50,8 +50,8 @@ func init() { validatorCredentialsGetCmd.Flags().String("validator", "", "Validator for which to get validator credentials") } -func validatorCredentialsGetBindings() { - if err := viper.BindPFlag("validator", validatorCredentialsGetCmd.Flags().Lookup("validator")); err != nil { +func validatorCredentialsGetBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil { panic(err) } } diff --git a/cmd/validatorcredentialsset.go b/cmd/validatorcredentialsset.go index 2d8dd23..bcb3afa 100644 --- a/cmd/validatorcredentialsset.go +++ b/cmd/validatorcredentialsset.go @@ -66,29 +66,29 @@ func init() { validatorCredentialsSetCmd.Flags().String("genesis-validators-root", "", "Genesis validators root to use for signing (overrides fetching from beacon node)") } -func validatorCredentialsSetBindings() { - if err := viper.BindPFlag("prepare-offline", validatorCredentialsSetCmd.Flags().Lookup("prepare-offline")); err != nil { +func validatorCredentialsSetBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("prepare-offline", cmd.Flags().Lookup("prepare-offline")); err != nil { panic(err) } - if err := viper.BindPFlag("validator", validatorCredentialsSetCmd.Flags().Lookup("validator")); err != nil { + if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil { panic(err) } - if err := viper.BindPFlag("signed-operations", validatorCredentialsSetCmd.Flags().Lookup("signed-operations")); err != nil { + if err := viper.BindPFlag("signed-operations", cmd.Flags().Lookup("signed-operations")); err != nil { panic(err) } - if err := viper.BindPFlag("withdrawal-account", validatorCredentialsSetCmd.Flags().Lookup("withdrawal-account")); err != nil { + if err := viper.BindPFlag("withdrawal-account", cmd.Flags().Lookup("withdrawal-account")); err != nil { panic(err) } - if err := viper.BindPFlag("withdrawal-address", validatorCredentialsSetCmd.Flags().Lookup("withdrawal-address")); err != nil { + if err := viper.BindPFlag("withdrawal-address", cmd.Flags().Lookup("withdrawal-address")); err != nil { panic(err) } - if err := viper.BindPFlag("offline", validatorCredentialsSetCmd.Flags().Lookup("offline")); err != nil { + if err := viper.BindPFlag("offline", cmd.Flags().Lookup("offline")); err != nil { panic(err) } - if err := viper.BindPFlag("fork-version", validatorCredentialsSetCmd.Flags().Lookup("fork-version")); err != nil { + if err := viper.BindPFlag("fork-version", cmd.Flags().Lookup("fork-version")); err != nil { panic(err) } - if err := viper.BindPFlag("genesis-validators-root", validatorCredentialsSetCmd.Flags().Lookup("genesis-validators-root")); err != nil { + if err := viper.BindPFlag("genesis-validators-root", cmd.Flags().Lookup("genesis-validators-root")); err != nil { panic(err) } } diff --git a/cmd/validatordepositdata.go b/cmd/validatordepositdata.go index e25d72d..7e9f037 100644 --- a/cmd/validatordepositdata.go +++ b/cmd/validatordepositdata.go @@ -59,29 +59,29 @@ func init() { validatorDepositDataCmd.Flags().Bool("launchpad", false, "Print launchpad-compatible JSON") } -func validatorDepositdataBindings() { - if err := viper.BindPFlag("validatoraccount", validatorDepositDataCmd.Flags().Lookup("validatoraccount")); err != nil { +func validatorDepositdataBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("validatoraccount", cmd.Flags().Lookup("validatoraccount")); err != nil { panic(err) } - if err := viper.BindPFlag("withdrawalaccount", validatorDepositDataCmd.Flags().Lookup("withdrawalaccount")); err != nil { + if err := viper.BindPFlag("withdrawalaccount", cmd.Flags().Lookup("withdrawalaccount")); err != nil { panic(err) } - if err := viper.BindPFlag("withdrawalpubkey", validatorDepositDataCmd.Flags().Lookup("withdrawalpubkey")); err != nil { + if err := viper.BindPFlag("withdrawalpubkey", cmd.Flags().Lookup("withdrawalpubkey")); err != nil { panic(err) } - if err := viper.BindPFlag("withdrawaladdress", validatorDepositDataCmd.Flags().Lookup("withdrawaladdress")); err != nil { + if err := viper.BindPFlag("withdrawaladdress", cmd.Flags().Lookup("withdrawaladdress")); err != nil { panic(err) } - if err := viper.BindPFlag("depositvalue", validatorDepositDataCmd.Flags().Lookup("depositvalue")); err != nil { + if err := viper.BindPFlag("depositvalue", cmd.Flags().Lookup("depositvalue")); err != nil { panic(err) } - if err := viper.BindPFlag("raw", validatorDepositDataCmd.Flags().Lookup("raw")); err != nil { + if err := viper.BindPFlag("raw", cmd.Flags().Lookup("raw")); err != nil { panic(err) } - if err := viper.BindPFlag("forkversion", validatorDepositDataCmd.Flags().Lookup("forkversion")); err != nil { + if err := viper.BindPFlag("forkversion", cmd.Flags().Lookup("forkversion")); err != nil { panic(err) } - if err := viper.BindPFlag("launchpad", validatorDepositDataCmd.Flags().Lookup("launchpad")); err != nil { + if err := viper.BindPFlag("launchpad", cmd.Flags().Lookup("launchpad")); err != nil { panic(err) } } diff --git a/cmd/validatorduties.go b/cmd/validatorduties.go index deb8b82..bade2be 100644 --- a/cmd/validatorduties.go +++ b/cmd/validatorduties.go @@ -51,11 +51,11 @@ func init() { validatorDutiesCmd.Flags().String("index", "", "validator index for duties") } -func validatorDutiesBindings() { - if err := viper.BindPFlag("pubkey", validatorDutiesCmd.Flags().Lookup("pubkey")); err != nil { +func validatorDutiesBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("pubkey", cmd.Flags().Lookup("pubkey")); err != nil { panic(err) } - if err := viper.BindPFlag("index", validatorDutiesCmd.Flags().Lookup("index")); err != nil { + if err := viper.BindPFlag("index", cmd.Flags().Lookup("index")); err != nil { panic(err) } } diff --git a/cmd/validatorexit.go b/cmd/validatorexit.go index f1166ba..b6748c1 100644 --- a/cmd/validatorexit.go +++ b/cmd/validatorexit.go @@ -63,26 +63,26 @@ func init() { validatorExitCmd.Flags().String("genesis-validators-root", "", "Genesis validators root to use for signing (overrides fetching from beacon node)") } -func validatorExitBindings() { - if err := viper.BindPFlag("epoch", validatorExitCmd.Flags().Lookup("epoch")); err != nil { +func validatorExitBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("prepare-offline", validatorExitCmd.Flags().Lookup("prepare-offline")); err != nil { + if err := viper.BindPFlag("prepare-offline", cmd.Flags().Lookup("prepare-offline")); err != nil { panic(err) } - if err := viper.BindPFlag("validator", validatorExitCmd.Flags().Lookup("validator")); err != nil { + if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil { panic(err) } - if err := viper.BindPFlag("signed-operation", validatorExitCmd.Flags().Lookup("signed-operation")); err != nil { + if err := viper.BindPFlag("signed-operation", cmd.Flags().Lookup("signed-operation")); err != nil { panic(err) } - if err := viper.BindPFlag("offline", validatorExitCmd.Flags().Lookup("offline")); err != nil { + if err := viper.BindPFlag("offline", cmd.Flags().Lookup("offline")); err != nil { panic(err) } - if err := viper.BindPFlag("fork-version", validatorExitCmd.Flags().Lookup("fork-version")); err != nil { + if err := viper.BindPFlag("fork-version", cmd.Flags().Lookup("fork-version")); err != nil { panic(err) } - if err := viper.BindPFlag("genesis-validators-root", validatorExitCmd.Flags().Lookup("genesis-validators-root")); err != nil { + if err := viper.BindPFlag("genesis-validators-root", cmd.Flags().Lookup("genesis-validators-root")); err != nil { panic(err) } } diff --git a/cmd/validatorexpectation.go b/cmd/validatorexpectation.go index 7c10f49..4b5549e 100644 --- a/cmd/validatorexpectation.go +++ b/cmd/validatorexpectation.go @@ -48,8 +48,8 @@ func init() { validatorExpectationCmd.Flags().Int64("validators", 1, "Number of validators") } -func validatorExpectationBindings() { - if err := viper.BindPFlag("validators", validatorExpectationCmd.Flags().Lookup("validators")); err != nil { +func validatorExpectationBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil { panic(err) } } diff --git a/cmd/validatorinfo.go b/cmd/validatorinfo.go index e0bff5a..1e3da04 100644 --- a/cmd/validatorinfo.go +++ b/cmd/validatorinfo.go @@ -61,10 +61,10 @@ In quiet mode this will return 0 if the validator information can be obtained, o validator, err := util.ParseValidator(ctx, eth2Client.(eth2client.ValidatorsProvider), viper.GetString("validator"), "head") errCheck(err, "Failed to obtain validator") - if verbose { + if viper.GetBool("verbose") { network, err := util.Network(ctx, eth2Client) errCheck(err, "Failed to obtain network") - outputIf(debug, fmt.Sprintf("Network is %s", network)) + outputIf(viper.GetBool("debug"), fmt.Sprintf("Network is %s", network)) pubKey, err := validator.PubKey(ctx) if err == nil { deposits, totalDeposited, err := graphData(network, pubKey[:]) @@ -75,14 +75,14 @@ In quiet mode this will return 0 if the validator information can be obtained, o } } - if quiet { + if viper.GetBool("quiet") { os.Exit(_exitSuccess) } if validator.Status.IsPending() || validator.Status.HasActivated() { fmt.Printf("Index: %d\n", validator.Index) } - if verbose { + if viper.GetBool("verbose") { if validator.Status.IsPending() { fmt.Printf("Activation eligibility epoch: %d\n", validator.Validator.ActivationEligibilityEpoch) } @@ -102,7 +102,7 @@ In quiet mode this will return 0 if the validator information can be obtained, o if validator.Status.IsActive() { fmt.Printf("Effective balance: %s\n", string2eth.GWeiToString(uint64(validator.Validator.EffectiveBalance), true)) } - if verbose { + if viper.GetBool("verbose") { fmt.Printf("Withdrawal credentials: %#x\n", validator.Validator.WithdrawalCredentials) } @@ -174,8 +174,8 @@ func init() { validatorFlags(validatorInfoCmd) } -func validatorInfoBindings() { - if err := viper.BindPFlag("validator", validatorInfoCmd.Flags().Lookup("validator")); err != nil { +func validatorInfoBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil { panic(err) } } diff --git a/cmd/validatorkeycheck.go b/cmd/validatorkeycheck.go index 928998f..bde51a0 100644 --- a/cmd/validatorkeycheck.go +++ b/cmd/validatorkeycheck.go @@ -53,11 +53,11 @@ func init() { validatorKeycheckCmd.Flags().String("privkey", "", "Private key from which to generate withdrawal credentials") } -func validatorKeycheckBindings() { - if err := viper.BindPFlag("withdrawal-credentials", validatorKeycheckCmd.Flags().Lookup("withdrawal-credentials")); err != nil { +func validatorKeycheckBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("withdrawal-credentials", cmd.Flags().Lookup("withdrawal-credentials")); err != nil { panic(err) } - if err := viper.BindPFlag("privkey", validatorKeycheckCmd.Flags().Lookup("privkey")); err != nil { + if err := viper.BindPFlag("privkey", cmd.Flags().Lookup("privkey")); err != nil { panic(err) } } diff --git a/cmd/validatorsummary.go b/cmd/validatorsummary.go index 4ae0c33..56fc429 100644 --- a/cmd/validatorsummary.go +++ b/cmd/validatorsummary.go @@ -51,12 +51,12 @@ func init() { validatorSummaryCmd.Flags().StringSlice("validators", nil, "the list of validators for which to obtain information") } -func validatorSummaryBindings() { +func validatorSummaryBindings(cmd *cobra.Command) { validatorBindings() - if err := viper.BindPFlag("epoch", validatorSummaryCmd.Flags().Lookup("epoch")); err != nil { + if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("validators", validatorSummaryCmd.Flags().Lookup("validators")); err != nil { + if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil { panic(err) } } diff --git a/cmd/validatorwithdrawal.go b/cmd/validatorwithdrawal.go new file mode 100644 index 0000000..34dd883 --- /dev/null +++ b/cmd/validatorwithdrawal.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Weald Technology Trading +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + validatorwithdrawal "github.com/wealdtech/ethdo/cmd/validator/withdrawal" +) + +var validatorWithdrawalCmd = &cobra.Command{ + Use: "withdrawal", + Short: "Obtain next withdrawal for a validator", + Long: `Obtain next withdrawal for a validator. For example: + + ethdo validator withdrawal --validator=primary/validator + +In quiet mode this will return 0 if the validator exists, otherwise 1.`, + RunE: func(cmd *cobra.Command, args []string) error { + res, err := validatorwithdrawal.Run(cmd) + if err != nil { + return err + } + if viper.GetBool("quiet") { + return nil + } + if res != "" { + fmt.Println(res) + } + return nil + }, +} + +func init() { + validatorCmd.AddCommand(validatorWithdrawalCmd) + validatorFlags(validatorWithdrawalCmd) + validatorWithdrawalCmd.Flags().String("validator", "", "Validator for which to get withdrawal") +} + +func validatorWithdrawalBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil { + panic(err) + } +} diff --git a/cmd/validatoryield.go b/cmd/validatoryield.go index f3c4335..8f4fc0d 100644 --- a/cmd/validatoryield.go +++ b/cmd/validatoryield.go @@ -50,8 +50,8 @@ func init() { validatorYieldCmd.Flags().String("validators", "", "Number of active validators (default fetches from chain)") } -func validatorYieldBindings() { - if err := viper.BindPFlag("validators", validatorYieldCmd.Flags().Lookup("validators")); err != nil { +func validatorYieldBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil { panic(err) } } diff --git a/cmd/walletaccounts.go b/cmd/walletaccounts.go index b61d5ec..589f9bf 100644 --- a/cmd/walletaccounts.go +++ b/cmd/walletaccounts.go @@ -86,8 +86,8 @@ In quiet mode this will return 0 if the wallet holds any addresses, otherwise 1. } for _, account := range accounts { - outputIf(!quiet, account.Name()) - if verbose { + outputIf(!viper.GetBool("quiet"), account.Name()) + if viper.GetBool("verbose") { fmt.Printf(" UUID: %v\n", account.ID()) if pathProvider, isProvider := account.(e2wtypes.AccountPathProvider); isProvider { if pathProvider.Path() != "" { diff --git a/cmd/walletcreate.go b/cmd/walletcreate.go index 8ed0835..0f992a8 100644 --- a/cmd/walletcreate.go +++ b/cmd/walletcreate.go @@ -47,8 +47,8 @@ func init() { walletCreateCmd.Flags().String("type", "non-deterministic", "Type of wallet to create (non-deterministic or hierarchical deterministic)") } -func walletCreateBindings() { - if err := viper.BindPFlag("type", walletCreateCmd.Flags().Lookup("type")); err != nil { +func walletCreateBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("type", cmd.Flags().Lookup("type")); err != nil { panic(err) } } diff --git a/cmd/walletimport.go b/cmd/walletimport.go index b718a87..d778d5e 100644 --- a/cmd/walletimport.go +++ b/cmd/walletimport.go @@ -48,11 +48,11 @@ func init() { walletImportCmd.Flags().Bool("verify", false, "Verify the wallet can be imported, but do not import it") } -func walletImportBindings() { - if err := viper.BindPFlag("data", walletImportCmd.Flags().Lookup("data")); err != nil { +func walletImportBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("data", cmd.Flags().Lookup("data")); err != nil { panic(err) } - if err := viper.BindPFlag("verify", walletImportCmd.Flags().Lookup("verify")); err != nil { + if err := viper.BindPFlag("verify", cmd.Flags().Lookup("verify")); err != nil { panic(err) } } diff --git a/cmd/walletinfo.go b/cmd/walletinfo.go index fb513a4..3e6b72a 100644 --- a/cmd/walletinfo.go +++ b/cmd/walletinfo.go @@ -42,13 +42,13 @@ In quiet mode this will return 0 if the wallet exists, otherwise 1.`, wallet, err := walletFromPath(ctx, viper.GetString("wallet")) errCheck(err, "unknown wallet") - if quiet { + if viper.GetBool("quiet") { os.Exit(0) } - outputIf(verbose, fmt.Sprintf("UUID: %v", wallet.ID())) + outputIf(viper.GetBool("verbose"), fmt.Sprintf("UUID: %v", wallet.ID())) fmt.Printf("Type: %s\n", wallet.Type()) - if verbose { + if viper.GetBool("verbose") { if storeProvider, ok := wallet.(wtypes.StoreProvider); ok { store := storeProvider.Store() fmt.Printf("Store: %s\n", store.Name()) diff --git a/cmd/walletlist.go b/cmd/walletlist.go index 7772276..07f0e2b 100644 --- a/cmd/walletlist.go +++ b/cmd/walletlist.go @@ -37,8 +37,8 @@ In quiet mode this will return 0 if any wallets are found, otherwise 1.`, walletsFound := false for w := range e2wallet.Wallets() { walletsFound = true - outputIf(!quiet && !verbose, w.Name()) - outputIf(verbose, fmt.Sprintf("%s\n UUID: %s", w.Name(), w.ID().String())) + outputIf(!viper.GetBool("quiet") && !viper.GetBool("verbose"), w.Name()) + outputIf(viper.GetBool("verbose"), fmt.Sprintf("%s\n UUID: %s", w.Name(), w.ID().String())) } if !walletsFound { diff --git a/cmd/walletsharedexport.go b/cmd/walletsharedexport.go index a9ea23b..c7c21a2 100644 --- a/cmd/walletsharedexport.go +++ b/cmd/walletsharedexport.go @@ -47,14 +47,14 @@ func init() { walletSharedExportCmd.Flags().String("file", "", "Name of the file that stores the export") } -func walletSharedExportBindings() { - if err := viper.BindPFlag("participants", walletSharedExportCmd.Flags().Lookup("participants")); err != nil { +func walletSharedExportBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("participants", cmd.Flags().Lookup("participants")); err != nil { panic(err) } - if err := viper.BindPFlag("threshold", walletSharedExportCmd.Flags().Lookup("threshold")); err != nil { + if err := viper.BindPFlag("threshold", cmd.Flags().Lookup("threshold")); err != nil { panic(err) } - if err := viper.BindPFlag("file", walletSharedExportCmd.Flags().Lookup("file")); err != nil { + if err := viper.BindPFlag("file", cmd.Flags().Lookup("file")); err != nil { panic(err) } } diff --git a/cmd/walletsharedimport.go b/cmd/walletsharedimport.go index b03ba9c..d2eaaa3 100644 --- a/cmd/walletsharedimport.go +++ b/cmd/walletsharedimport.go @@ -48,11 +48,11 @@ func init() { walletSharedImportCmd.Flags().String("shares", "", "Shares required to decrypt the export, separated with spaces") } -func walletSharedImportBindings() { - if err := viper.BindPFlag("file", walletSharedImportCmd.Flags().Lookup("file")); err != nil { +func walletSharedImportBindings(cmd *cobra.Command) { + if err := viper.BindPFlag("file", cmd.Flags().Lookup("file")); err != nil { panic(err) } - if err := viper.BindPFlag("shares", walletSharedImportCmd.Flags().Lookup("shares")); err != nil { + if err := viper.BindPFlag("shares", cmd.Flags().Lookup("shares")); err != nil { panic(err) } } diff --git a/docs/usage.md b/docs/usage.md index 9d4a3bb..4da0b6c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,6 +1,6 @@ # ethdo commands -ethdo provides features to manage wallets and accounts, as well as interacting with Ethereum 2 nodes and remote signers. Below are a list of all available commands. +ethdo provides features to manage wallets and accounts, as well as interacting with Ethereum consensus nodes and remote signers. Below are a list of all available commands. Note that the below provides a list of commands rather than a howto guide. Please follow the @@ -272,9 +272,9 @@ $ ethdo version ### `block` commands -Block commands focus on providing information about Ethereum 2 blocks. +Block commands focus on providing information about Ethereum consensus blocks. #### `analyze` -`ethdo block info` obtains information about a block in Ethereum 2. Options include: +`ethdo block info` obtains information about a block in the Ethereum consensus chain. Options include: - `blockid`: the ID (slot, root, 'head') of the block to obtain ```sh @@ -297,7 +297,7 @@ Value for block 80: 488.531 #### `info` -`ethdo block info` obtains information about a block in Ethereum 2. Options include: +`ethdo block info` obtains information about a block in the Ethereum consensus chain. Options include: - `blockid`: the ID (slot, root, 'head') of the block to obtain ```sh @@ -335,7 +335,7 @@ Voluntary exits: 0 ### `chain` commands -Chain commands focus on providing information about Ethereum 2 chains. +Chain commands focus on providing information about Ethereum consensus chains. #### `eth1votes` @@ -355,7 +355,7 @@ Additional information is supplied when using `--verbose` #### `info` -`ethdo chain info` obtains information about an Ethereum 2 chain. +`ethdo chain info` obtains information about an Ethereum consensus chain. ```sh $ ethdo chain info @@ -383,9 +383,21 @@ $ ethdo chain queues Activation queue: 14798 ``` +#### `spec` + +`ethdo chain spec` obtains the specification of an Ethereum consensus chain from the nod. + +```sh +$ ethdo chain spec +ALTAIR_FORK_EPOCH: 74240 +ALTAIR_FORK_VERSION: 0x01000000 +BASE_REWARD_FACTOR: 64 +... +``` + #### `status` -`ethdo chain status` obtains the status of an Ethereum 2 chain from the node's point of view. Options include: +`ethdo chain status` obtains the status of an Ethereum consensus chain from the node's point of view. Options include: - `slot` show output in terms of slots rather than epochs ```sh @@ -410,7 +422,7 @@ Prior justified epoch distance: 4 #### `time` -`ethdo chain time` calculates the time period of Ethereum 2 epochs and slots. Options include: +`ethdo chain time` calculates the time period of Ethereum consensus epochs and slots. Options include: - `epoch` show epoch and slot times for the given epoch - `slot` show epoch and slot times for the given slot - `timestamp` show epoch and slot times for the given timestamp @@ -489,11 +501,11 @@ $ ethdo exit verify --signed-operation=${HOME}/exit.json ### `node` commands -Node commands focus on information from an Ethereum 2 node. +Node commands focus on information from an Ethereum consensus node. #### `events` -`ethdo node events` displays events emitted by an Ethereum 2 node. +`ethdo node events` displays events emitted by an Ethereum consensus node. ```sh $ ethdo node events --topics=head,chain_reorg @@ -504,7 +516,7 @@ $ ethdo node events --topics=head,chain_reorg #### `info` -`ethdo node info` obtains the information about an Ethereum 2 node. +`ethdo node info` obtains the information about an Ethereum consensus node. ```sh $ ethdo node info @@ -526,7 +538,7 @@ Genesis timestamp: 1587020563 ### `slot` commands -Slot commands focus on information about Ethereum 2 slots. +Slot commands focus on information about Ethereum consensus slots. #### `slottime` @@ -572,7 +584,7 @@ $ ethdo synccommittee members ### `validator` commands -Validator commands focus on interaction with Ethereum 2 validators. +Validator commands focus on interaction with Ethereum consensus validators. #### `credentials get` @@ -593,7 +605,7 @@ $ ethdo validator credentials set --validator=Validators/1 --withdrawal-address= #### `depositdata` -`ethdo validator depositdata` generates the data required to deposit one or more Ethereum 2 validators. Options include: +`ethdo validator depositdata` generates the data required to deposit one or more Ethereum consensus validators. Options include: - `withdrawalaccount` specify the account to be used for the withdrawal credentials (if withdrawalpubkey is not supplied) - `withdrawaladdress` specify the Ethereum execution address to be used for the withdrawal credentials (if withdrawalpubkey is not supplied) - `withdrawalpubkey` specify the public key to be used for the withdrawal credentials (if withdrawalaccount is not supplied) @@ -676,7 +688,7 @@ Expected time between sync committees: 1 year 27 weeks ### `attester` commands -Attester commands focus on Ethereum 2 validators' actions as attesters. +Attester commands focus on Ethereum consensus validators' actions as attesters. #### `duties` @@ -700,6 +712,16 @@ $ ethdo attester inclusion --validator=Validators/1 --epoch=6484 Attestation included in block 207492 (inclusion delay 1) ``` +#### `withdrawal` +`ethdo validator withdrawal` provides information about the next withdrawal for the given validator. Options include: + - `validator`: the list of validators for which to provide a summary + - `json`: provide JSON output + +```sh +$ ethdo validator withdrawal --validator=12345 +Withdrawal expected at 2023-04-17T15:08:35 in block 6243041 +``` + #### `yield` `ethdo validator yield` calculates the expected yield given the number of validators. Options include: @@ -719,7 +741,7 @@ Yield: 4.64% ### `proposer` commands -Proposer commands focus on Ethereum 2 validators' actions as proposers. +Proposer commands focus on Ethereum consensus validators' actions as proposers. #### `duties`