Compare commits

...

11 Commits

Author SHA1 Message Date
Jim McDonald
bf1d265742 Support 12 and 18-word mnemonics with passphrases.
Only supports single-word (no whitespace) passphrases for 12 and 18 word
mnemonics.

Fixes #87
2023-05-18 23:54:26 +01:00
Jim McDonald
7857e97057 Generate multiple validator exits.
Fixes #88
2023-05-18 23:34:40 +01:00
Jim McDonald
545665a79f Add generate-keystore to account derive. 2023-05-08 09:21:20 +01:00
Jim McDonald
394fd2bb04 Use String() for execution address output. 2023-05-01 15:44:29 +01:00
Jim McDonald
a7631a6a7f Initial support for deneb. 2023-04-29 12:20:29 +01:00
Jim McDonald
d174219ddc Fix test. 2023-04-16 19:43:02 +01:00
Jim McDonald
854f3061b9 Update docs. 2023-04-16 19:42:07 +01:00
Jim McDonald
6c34d25ebb Update docs to include validator specifier. 2023-04-16 19:25:59 +01:00
Jim McDonald
df34cef2bd Bump version. 2023-04-16 18:59:38 +01:00
Jim McDonald
e8513e60b2 Add validator withdrawal. 2023-04-15 10:17:11 +01:00
Jim McDonald
4aadee3fad Promote --json to global flag. 2023-04-15 09:29:23 +01:00
80 changed files with 1867 additions and 611 deletions

View File

@@ -1,3 +1,13 @@
dev:
- initial support for deneb
- add "--generate-keystore" option for "account derive"
- update "validator exit" command to be able to generate multiple exits
- support for 12-word and 18-word mnemonics with single-word (no whitespace) passphrases
1.30.0:
- add "chain spec" command
- add "validator withdrawal" command
1.29.2:
- fix regression where validator index could not be used as an account specifier

View File

@@ -171,6 +171,15 @@ If set, the `--debug` argument will output additional information about the oper
Commands will have an exit status of 0 on success and 1 on failure. The specific definition of success is specified in the help for each command.
### Validator specifier
Ethereum validators can be specified in a number of different ways. The options are:
- an `ethdo` account, in the format _wallet_/_account_. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied, with a passphrase (for local accounts) or authority (for remote accounts)
- the validator's 48-byte public key. It is not possible to use the a validator specified in this way to sign validator-related operations
- a keystore, supplied either as direct JSON or as a path to a keystore on the local filesystem. It is possible to use the validator specified in this way to sign validator-related operations, if the passphrase is also supplied
- the validator's numeric index. It is not possible to use a validator specified in this way to sign validator-related operations. Note that this only works with on-chain operations, as the validator's index must be resolved to its public key
## Passphrase strength
`ethdo` will by default not allow creation or export of accounts or wallets with weak passphrases. If a weak pasphrase is used then `ethdo` will refuse to continue.

View File

@@ -269,7 +269,7 @@ func ObtainChainInfoFromNode(ctx context.Context,
}
tmp, exists := spec["GENESIS_FORK_VERSION"]
if !exists {
return nil, errors.New("capella fork version not known by chain")
return nil, errors.New("genesis fork version not known by chain")
}
var isForkVersion bool
res.GenesisForkVersion, isForkVersion = tmp.(phase0.Version)

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2023 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -28,6 +28,7 @@ type dataIn struct {
// Output options.
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
}
func input(_ context.Context) (*dataIn, error) {
@@ -54,5 +55,8 @@ func input(_ context.Context) (*dataIn, error) {
// Show withdrawal credentials.
data.showWithdrawalCredentials = viper.GetBool("show-withdrawal-credentials")
// Generate keystore.
data.generateKeystore = viper.GetBool("generate-keystore")
return data, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2023 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -15,21 +15,29 @@ package accountderive
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
ethutil "github.com/wealdtech/go-eth2-util"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
type dataOut struct {
showPrivateKey bool
showWithdrawalCredentials bool
generateKeystore bool
key *e2types.BLSPrivateKey
path string
}
func output(_ context.Context, data *dataOut) (string, error) {
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
}
@@ -37,13 +45,17 @@ func output(_ context.Context, data *dataOut) (string, error) {
return "", errors.New("no key")
}
if data.generateKeystore {
return outputKeystore(ctx, data)
}
builder := strings.Builder{}
if data.showPrivateKey {
builder.WriteString(fmt.Sprintf("Private key: %#x\n", data.key.Marshal()))
}
if data.showWithdrawalCredentials {
withdrawalCredentials := util.SHA256(data.key.PublicKey().Marshal())
withdrawalCredentials := ethutil.SHA256(data.key.PublicKey().Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
builder.WriteString(fmt.Sprintf("Withdrawal credentials: %#x\n", withdrawalCredentials))
}
@@ -53,3 +65,38 @@ func output(_ context.Context, data *dataOut) (string, error) {
return builder.String(), nil
}
func outputKeystore(_ context.Context, data *dataOut) (string, error) {
passphrase, err := util.GetPassphrase()
if err != nil {
return "", errors.New("no passphrase supplied")
}
encryptor := keystorev4.New()
crypto, err := encryptor.Encrypt(data.key.Marshal(), passphrase)
if err != nil {
return "", errors.New("failed to encrypt private key")
}
uuid, err := uuid.NewRandom()
if err != nil {
return "", errors.New("failed to generate UUID")
}
ks := make(map[string]interface{})
ks["uuid"] = uuid.String()
ks["pubkey"] = fmt.Sprintf("%x", data.key.PublicKey().Marshal())
ks["version"] = 4
ks["path"] = data.path
ks["crypto"] = crypto
out, err := json.Marshal(ks)
if err != nil {
return "", errors.Wrap(err, "failed to marshal keystore JSON")
}
keystoreFilename := fmt.Sprintf("keystore-%s-%d.json", strings.ReplaceAll(data.path, "/", "_"), time.Now().Unix())
if err := os.WriteFile(keystoreFilename, out, 0o600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", keystoreFilename))
}
return "", errors.New("not implemented")
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2023 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -40,7 +40,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
results := &dataOut{
showPrivateKey: data.showPrivateKey,
showWithdrawalCredentials: data.showWithdrawalCredentials,
generateKeystore: data.generateKeystore,
key: key.(*e2types.BLSPrivateKey),
path: data.path,
}
return results, nil

View File

@@ -51,11 +51,11 @@ func init() {
accountCreateCmd.Flags().Uint32("signing-threshold", 1, "Signing threshold (1 for non-distributed accounts)")
}
func accountCreateBindings() {
if err := viper.BindPFlag("participants", accountCreateCmd.Flags().Lookup("participants")); err != nil {
func accountCreateBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("participants", cmd.Flags().Lookup("participants")); err != nil {
panic(err)
}
if err := viper.BindPFlag("signing-threshold", accountCreateCmd.Flags().Lookup("signing-threshold")); err != nil {
if err := viper.BindPFlag("signing-threshold", cmd.Flags().Lookup("signing-threshold")); err != nil {
panic(err)
}
}

View File

@@ -49,13 +49,17 @@ func init() {
accountFlags(accountDeriveCmd)
accountDeriveCmd.Flags().Bool("show-private-key", false, "show private key for derived account")
accountDeriveCmd.Flags().Bool("show-withdrawal-credentials", false, "show withdrawal credentials for derived account")
accountDeriveCmd.Flags().Bool("generate-keystore", false, "generate a keystore for the derived account")
}
func accountDeriveBindings() {
if err := viper.BindPFlag("show-private-key", accountDeriveCmd.Flags().Lookup("show-private-key")); err != nil {
func accountDeriveBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("show-private-key", cmd.Flags().Lookup("show-private-key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("show-withdrawal-credentials", accountDeriveCmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
if err := viper.BindPFlag("show-withdrawal-credentials", cmd.Flags().Lookup("show-withdrawal-credentials")); err != nil {
panic(err)
}
if err := viper.BindPFlag("generate-keystore", cmd.Flags().Lookup("generate-keystore")); err != nil {
panic(err)
}
}

View File

@@ -52,14 +52,14 @@ func init() {
accountImportCmd.Flags().String("keystore-passphrase", "", "Passphrase of keystore")
}
func accountImportBindings() {
if err := viper.BindPFlag("key", accountImportCmd.Flags().Lookup("key")); err != nil {
func accountImportBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("key", cmd.Flags().Lookup("key")); err != nil {
panic(err)
}
if err := viper.BindPFlag("keystore", accountImportCmd.Flags().Lookup("keystore")); err != nil {
if err := viper.BindPFlag("keystore", cmd.Flags().Lookup("keystore")); err != nil {
panic(err)
}
if err := viper.BindPFlag("keystore-passphrase", accountImportCmd.Flags().Lookup("keystore-passphrase")); err != nil {
if err := viper.BindPFlag("keystore-passphrase", cmd.Flags().Lookup("keystore-passphrase")); err != nil {
panic(err)
}
}

View File

@@ -44,11 +44,11 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
// Disallow wildcards (for now)
assert(fmt.Sprintf("%s/%s", wallet.Name(), account.Name()) == viper.GetString("account"), "Mismatched account name")
if quiet {
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
outputIf(verbose, fmt.Sprintf("UUID: %v", account.ID()))
outputIf(viper.GetBool("verbose"), fmt.Sprintf("UUID: %v", account.ID()))
var withdrawalPubKey e2types.PublicKey
if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok {
fmt.Printf("Public key: %#x\n", pubKeyProvider.PublicKey().Marshal())
@@ -58,7 +58,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
if distributedAccount, ok := account.(e2wtypes.DistributedAccount); ok {
fmt.Printf("Composite public key: %#x\n", distributedAccount.CompositePublicKey().Marshal())
fmt.Printf("Signing threshold: %d/%d\n", distributedAccount.SigningThreshold(), len(distributedAccount.Participants()))
if verbose {
if viper.GetBool("verbose") {
fmt.Printf("Participants:\n")
for k, v := range distributedAccount.Participants() {
fmt.Printf(" %d: %s\n", k, v)
@@ -67,7 +67,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
withdrawalPubKey = distributedAccount.CompositePublicKey()
}
if verbose {
if viper.GetBool("verbose") {
withdrawalCredentials := util.SHA256(withdrawalPubKey.Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
fmt.Printf("Withdrawal credentials: %#x\n", withdrawalCredentials)

View File

@@ -49,17 +49,13 @@ func init() {
attesterFlags(attesterDutiesCmd)
attesterDutiesCmd.Flags().String("epoch", "head", "the epoch for which to obtain the duties")
attesterDutiesCmd.Flags().String("validator", "", "the index, public key, or acount of the validator")
attesterDutiesCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
}
func attesterDutiesBindings() {
if err := viper.BindPFlag("epoch", attesterDutiesCmd.Flags().Lookup("epoch")); err != nil {
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 {
panic(err)
}
if err := viper.BindPFlag("json", attesterDutiesCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
panic(err)
}
}

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -439,6 +439,13 @@ func (c *command) analyzeSyncCommittees(_ context.Context, block *spec.Versioned
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
case spec.DataVersionDeneb:
c.analysis.SyncCommitee.Contributions = int(block.Deneb.Message.Body.SyncAggregate.SyncCommitteeBits.Count())
c.analysis.SyncCommitee.PossibleContributions = int(block.Deneb.Message.Body.SyncAggregate.SyncCommitteeBits.Len())
c.analysis.SyncCommitee.Score = float64(c.syncRewardWeight) / float64(c.weightDenominator)
c.analysis.SyncCommitee.Value = c.analysis.SyncCommitee.Score * float64(c.analysis.SyncCommitee.Contributions)
c.analysis.Value += c.analysis.SyncCommitee.Value
return nil
default:
return fmt.Errorf("unsupported block version %d", block.Version)
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020, 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
@@ -28,6 +28,7 @@ import (
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/deneb"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-bitfield"
@@ -246,7 +247,7 @@ func outputBlockBLSToExecutionChanges(ctx context.Context, eth2Client eth2client
} else {
res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[op.Message.ValidatorIndex].Validator.PublicKey, op.Message.ValidatorIndex))
res.WriteString(fmt.Sprintf(" BLS public key: %#x\n", op.Message.FromBLSPubkey))
res.WriteString(fmt.Sprintf(" Execution address: %#x\n", op.Message.ToExecutionAddress))
res.WriteString(fmt.Sprintf(" Execution address: %s\n", op.Message.ToExecutionAddress.String()))
}
}
}
@@ -388,6 +389,108 @@ func outputCapellaBlockText(ctx context.Context, data *dataOut, signedBlock *cap
return res.String(), nil
}
func outputDenebBlockText(ctx context.Context, data *dataOut, signedBlock *deneb.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
}
body := signedBlock.Message.Body
res := strings.Builder{}
// General info.
blockRoot, err := signedBlock.Message.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to obtain block root")
}
bodyRoot, err := signedBlock.Message.Body.HashTreeRoot()
if err != nil {
return "", errors.Wrap(err, "failed to generate body root")
}
tmp, err := outputBlockGeneral(ctx,
data.verbose,
signedBlock.Message.Slot,
blockRoot,
bodyRoot,
signedBlock.Message.ParentRoot,
signedBlock.Message.StateRoot,
signedBlock.Message.Body.Graffiti[:],
data.genesisTime,
data.slotDuration,
data.slotsPerEpoch)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Eth1 data.
if data.verbose {
tmp, err := outputBlockETH1Data(ctx, body.ETH1Data)
if err != nil {
return "", err
}
res.WriteString(tmp)
}
// Sync aggregate.
tmp, err = outputBlockSyncAggregate(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.SyncAggregate, phase0.Epoch(uint64(signedBlock.Message.Slot)/data.slotsPerEpoch))
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attestations.
tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Attester slashings.
tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings)
if err != nil {
return "", err
}
res.WriteString(tmp)
res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings)))
// Add verbose proposer slashings.
tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits)
if err != nil {
return "", err
}
res.WriteString(tmp)
// Voluntary exits.
tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputBlockBLSToExecutionChanges(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.BLSToExecutionChanges)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputDenebBlockExecutionPayload(ctx, data.verbose, signedBlock.Message.Body.ExecutionPayload)
if err != nil {
return "", err
}
res.WriteString(tmp)
tmp, err = outputDenebBlobInfo(ctx, data.verbose, signedBlock.Message.Body)
if err != nil {
return "", err
}
res.WriteString(tmp)
return res.String(), nil
}
func outputBellatrixBlockText(ctx context.Context, data *dataOut, signedBlock *bellatrix.SignedBeaconBlock) (string, error) {
if signedBlock == nil {
return "", errors.New("no block supplied")
@@ -676,7 +779,7 @@ func outputCapellaBlockExecutionPayload(_ context.Context,
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
@@ -706,6 +809,97 @@ func outputCapellaBlockExecutionPayload(_ context.Context,
return res.String(), nil
}
func outputDenebBlockExecutionPayload(_ context.Context,
verbose bool,
payload *deneb.ExecutionPayload,
) (
string,
error,
) {
if payload == nil {
return "", nil
}
// If the block number is 0 then we're before the merge.
if payload.BlockNumber == 0 {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString("Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString("Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
} else {
res.WriteString("Execution payload:\n")
res.WriteString(" Execution block number: ")
res.WriteString(fmt.Sprintf("%d\n", payload.BlockNumber))
res.WriteString(" Base fee per gas: ")
res.WriteString(string2eth.WeiToString(payload.BaseFeePerGas.ToBig(), true))
res.WriteString("\n Block hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.BlockHash))
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasUsed))
res.WriteString(" Timestamp: ")
res.WriteString(fmt.Sprintf("%s (%d)\n", time.Unix(int64(payload.Timestamp), 0).String(), payload.Timestamp))
res.WriteString(" Prev RANDAO: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.PrevRandao))
res.WriteString(" Receipts root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ReceiptsRoot))
res.WriteString(" State root: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.StateRoot))
res.WriteString(" Extra data: ")
if utf8.Valid(payload.ExtraData) {
res.WriteString(fmt.Sprintf("%s\n", string(payload.ExtraData)))
} else {
res.WriteString(fmt.Sprintf("%#x\n", payload.ExtraData))
}
res.WriteString(" Logs bloom: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.LogsBloom))
res.WriteString(" Transactions: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Transactions)))
res.WriteString(" Withdrawals: ")
res.WriteString(fmt.Sprintf("%d\n", len(payload.Withdrawals)))
res.WriteString(" Excess data gas: ")
res.WriteString(payload.ExcessDataGas.Dec())
res.WriteString("\n")
}
return res.String(), nil
}
func outputDenebBlobInfo(_ context.Context,
verbose bool,
body *deneb.BeaconBlockBody,
) (
string,
error,
) {
if body == nil {
return "", nil
}
res := strings.Builder{}
if !verbose {
res.WriteString(fmt.Sprintf("Blob KZG commitments: %d\n", len(body.BlobKzgCommitments)))
} else if len(body.BlobKzgCommitments) > 0 {
res.WriteString("Blob KZG commitments:\n")
for i := range body.BlobKzgCommitments {
res.WriteString(fmt.Sprintf(" %s\n", body.BlobKzgCommitments[i].String()))
}
}
return res.String(), nil
}
func outputBellatrixBlockExecutionPayload(_ context.Context,
verbose bool,
payload *bellatrix.ExecutionPayload,
@@ -744,7 +938,7 @@ func outputBellatrixBlockExecutionPayload(_ context.Context,
res.WriteString(" Parent hash: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.ParentHash))
res.WriteString(" Fee recipient: ")
res.WriteString(fmt.Sprintf("%#x\n", payload.FeeRecipient))
res.WriteString(payload.FeeRecipient.String())
res.WriteString(" Gas limit: ")
res.WriteString(fmt.Sprintf("%d\n", payload.GasLimit))
res.WriteString(" Gas used: ")

View File

@@ -25,6 +25,7 @@ import (
"github.com/attestantio/go-eth2-client/spec/altair"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
"github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/deneb"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
@@ -85,6 +86,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if err := outputCapellaBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Capella); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
case spec.DataVersionDeneb:
if err := outputDenebBlock(ctx, data.jsonOutput, data.sszOutput, signedBlock.Deneb); err != nil {
return nil, errors.Wrap(err, "failed to output block")
}
default:
return nil, errors.New("unknown block version")
}
@@ -125,41 +130,26 @@ func headEventHandler(event *api.Event) {
}
return
}
switch signedBlock.Version {
case spec.DataVersionPhase0:
if err := outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputPhase0Block(context.Background(), jsonOutput, signedBlock.Phase0)
case spec.DataVersionAltair:
if err := outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputAltairBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Altair)
case spec.DataVersionBellatrix:
if err := outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputBellatrixBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Bellatrix)
case spec.DataVersionCapella:
if err := outputCapellaBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Capella); err != nil {
if !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
}
return
}
err = outputCapellaBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Capella)
case spec.DataVersionDeneb:
err = outputDenebBlock(context.Background(), jsonOutput, sszOutput, signedBlock.Deneb)
default:
if !jsonOutput && !sszOutput {
fmt.Printf("Unknown block version: %v\n", signedBlock.Version)
}
err = errors.New("unknown block version")
}
if err != nil && !jsonOutput && !sszOutput {
fmt.Printf("Failed to output block: %v\n", err)
return
}
if !jsonOutput && !sszOutput {
fmt.Println("")
}
@@ -254,3 +244,27 @@ func outputCapellaBlock(ctx context.Context, jsonOutput bool, sszOutput bool, si
}
return nil
}
func outputDenebBlock(ctx context.Context, jsonOutput bool, sszOutput bool, signedBlock *deneb.SignedBeaconBlock) error {
switch {
case jsonOutput:
data, err := json.Marshal(signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate JSON")
}
fmt.Printf("%s\n", string(data))
case sszOutput:
data, err := signedBlock.MarshalSSZ()
if err != nil {
return errors.Wrap(err, "failed to generate SSZ")
}
fmt.Printf("%x\n", data)
default:
data, err := outputDenebBlockText(ctx, results, signedBlock)
if err != nil {
return errors.Wrap(err, "failed to generate text")
}
fmt.Print(data)
}
return nil
}

View File

@@ -49,17 +49,13 @@ func init() {
blockFlags(blockAnalyzeCmd)
blockAnalyzeCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockAnalyzeCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockAnalyzeCmd.Flags().Bool("json", false, "output data in JSON format")
}
func blockAnalyzeBindings() {
if err := viper.BindPFlag("blockid", blockAnalyzeCmd.Flags().Lookup("blockid")); err != nil {
func blockAnalyzeBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("blockid", cmd.Flags().Lookup("blockid")); err != nil {
panic(err)
}
if err := viper.BindPFlag("stream", blockAnalyzeCmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", blockAnalyzeCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("stream", cmd.Flags().Lookup("stream")); err != nil {
panic(err)
}
}

View File

@@ -49,21 +49,17 @@ func init() {
blockFlags(blockInfoCmd)
blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch")
blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive")
blockInfoCmd.Flags().Bool("json", false, "output data in JSON format")
blockInfoCmd.Flags().Bool("ssz", false, "output data in SSZ format")
}
func blockInfoBindings() {
if err := viper.BindPFlag("blockid", blockInfoCmd.Flags().Lookup("blockid")); err != nil {
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("json", blockInfoCmd.Flags().Lookup("json")); 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)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -90,6 +90,10 @@ func (c *command) process(ctx context.Context) error {
c.slot = state.Capella.Slot
c.incumbent = state.Capella.ETH1Data
c.eth1DataVotes = state.Capella.ETH1DataVotes
case spec.DataVersionDeneb:
c.slot = state.Deneb.Slot
c.incumbent = state.Deneb.ETH1Data
c.eth1DataVotes = state.Deneb.ETH1DataVotes
default:
return fmt.Errorf("unhandled beacon state version %v", state.Version)
}

View File

@@ -51,17 +51,13 @@ func init() {
chainFlags(chainEth1VotesCmd)
chainEth1VotesCmd.Flags().String("epoch", "", "epoch for which to fetch the votes")
chainEth1VotesCmd.Flags().String("period", "", "period for which to fetch the votes")
chainEth1VotesCmd.Flags().Bool("json", false, "output data in JSON format")
}
func chainEth1VotesBindings() {
if err := viper.BindPFlag("epoch", chainEth1VotesCmd.Flags().Lookup("epoch")); err != nil {
func chainEth1VotesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("period", chainEth1VotesCmd.Flags().Lookup("period")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", chainEth1VotesCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("period", cmd.Flags().Lookup("period")); err != nil {
panic(err)
}
}

View File

@@ -54,12 +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 {
os.Exit(_exitSuccess)
}
if viper.GetBool("prepare-offline") {
fmt.Printf("Add the following to your command to run it offline:\n --offline --genesis-validators=root=%#x --fork-version=%#x\n", genesis.GenesisValidatorsRoot, fork.CurrentVersion)
if viper.GetBool("quiet") {
os.Exit(_exitSuccess)
}
@@ -67,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,
@@ -94,11 +89,7 @@ In quiet mode this will return 0 if the chain information can be obtained, other
func init() {
chainCmd.AddCommand(chainInfoCmd)
chainFlags(chainInfoCmd)
chainInfoCmd.Flags().Bool("prepare-offline", false, "Provide information useful for offline commands")
}
func chainInfoBindings() {
if err := viper.BindPFlag("prepare-offline", chainInfoCmd.Flags().Lookup("prepare-offline")); err != nil {
panic(err)
}
func chainInfoBindings(_ *cobra.Command) {
}

View File

@@ -48,14 +48,10 @@ func init() {
chainCmd.AddCommand(chainQueuesCmd)
chainFlags(chainQueuesCmd)
chainQueuesCmd.Flags().String("epoch", "", "epoch for which to fetch the queues")
chainQueuesCmd.Flags().Bool("json", false, "output data in JSON format")
}
func chainQueuesBindings() {
if err := viper.BindPFlag("epoch", chainQueuesCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", chainQueuesCmd.Flags().Lookup("json")); err != nil {
func chainQueuesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
}

97
cmd/chainspec.go Normal file
View File

@@ -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) {
}

View File

@@ -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("-")

View File

@@ -47,14 +47,14 @@ func init() {
chainTimeCmd.Flags().String("timestamp", "", "The timestamp for which to obtain information (format YYYY-MM-DDTHH:MM:SS+ZZZZ)")
}
func chainTimeBindings() {
if err := viper.BindPFlag("slot", chainTimeCmd.Flags().Lookup("slot")); err != nil {
func chainTimeBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("slot", cmd.Flags().Lookup("slot")); err != nil {
panic(err)
}
if err := viper.BindPFlag("epoch", chainTimeCmd.Flags().Lookup("epoch")); err != nil {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("timestamp", chainTimeCmd.Flags().Lookup("timestamp")); err != nil {
if err := viper.BindPFlag("timestamp", cmd.Flags().Lookup("timestamp")); err != nil {
panic(err)
}
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -287,6 +287,8 @@ func (c *command) processSyncCommitteeDuties(ctx context.Context) error {
aggregate = block.Bellatrix.Message.Body.SyncAggregate
case spec.DataVersionCapella:
aggregate = block.Capella.Message.Body.SyncAggregate
case spec.DataVersionDeneb:
aggregate = block.Deneb.Message.Body.SyncAggregate
default:
return fmt.Errorf("unhandled block version %v", block.Version)
}

View File

@@ -47,12 +47,8 @@ In quiet mode this will return 0 if information for the epoch is found, otherwis
func init() {
epochCmd.AddCommand(epochSummaryCmd)
epochFlags(epochSummaryCmd)
epochSummaryCmd.Flags().Bool("json", false, "output data in JSON format")
}
func epochSummaryBindings() {
epochBindings()
if err := viper.BindPFlag("json", epochSummaryCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
func epochSummaryBindings(cmd *cobra.Command) {
epochBindings(cmd)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,14 +48,10 @@ func init() {
proposerCmd.AddCommand(proposerDutiesCmd)
proposerFlags(proposerDutiesCmd)
proposerDutiesCmd.Flags().String("epoch", "", "the epoch for which to fetch duties")
proposerDutiesCmd.Flags().Bool("json", false, "output data in JSON format")
}
func proposerDutiesBindings() {
if err := viper.BindPFlag("epoch", proposerDutiesCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", proposerDutiesCmd.Flags().Lookup("json")); err != nil {
func proposerDutiesBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("epoch", cmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
}

View File

@@ -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/eth1votes":
chainEth1VotesBindings()
case "chain/info":
chainInfoBindings()
case "chain/queues":
chainQueuesBindings()
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)
@@ -189,7 +158,7 @@ func init() {
if err := viper.BindPFlag("path", RootCmd.PersistentFlags().Lookup("path")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().String("private-key", "", "Private key to provide access to an account")
RootCmd.PersistentFlags().String("private-key", "", "Private key to provide access to an account or validaotr")
if err := viper.BindPFlag("private-key", RootCmd.PersistentFlags().Lookup("private-key")); err != nil {
panic(err)
}
@@ -242,6 +211,10 @@ func init() {
if err := viper.BindPFlag("verbose", RootCmd.PersistentFlags().Lookup("verbose")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Bool("json", false, "generate JSON output where available")
if err := viper.BindPFlag("json", RootCmd.PersistentFlags().Lookup("json")); err != nil {
panic(err)
}
RootCmd.PersistentFlags().Bool("debug", false, "generate debug output")
if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil {
panic(err)

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -98,6 +98,13 @@ func (c *command) process(ctx context.Context) error {
} else {
c.inclusions = append(c.inclusions, 2)
}
case spec.DataVersionDeneb:
aggregate = block.Deneb.Message.Body.SyncAggregate
if aggregate.SyncCommitteeBits.BitAt(c.committeeIndex) {
c.inclusions = append(c.inclusions, 1)
} else {
c.inclusions = append(c.inclusions, 2)
}
default:
return fmt.Errorf("unhandled block version %v", block.Version)
}

View File

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

View File

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

View File

@@ -42,7 +42,8 @@ type command struct {
forkVersion string
genesisValidatorsRoot string
prepareOffline bool
signedOperationInput string
signedOperationsInput string
epoch string
// Beacon node connection.
timeout time.Duration
@@ -58,7 +59,7 @@ type command struct {
chainTime chaintime.Service
// Output.
signedOperation *phase0.SignedVoluntaryExit
signedOperations []*phase0.SignedVoluntaryExit
}
func newCommand(_ context.Context) (*command, error) {
@@ -76,10 +77,12 @@ func newCommand(_ context.Context) (*command, error) {
mnemonic: viper.GetString("mnemonic"),
path: viper.GetString("path"),
privateKey: viper.GetString("private-key"),
signedOperationInput: viper.GetString("signed-operation"),
signedOperationsInput: viper.GetString("signed-operations"),
validator: viper.GetString("validator"),
forkVersion: viper.GetString("fork-version"),
genesisValidatorsRoot: viper.GetString("genesis-validators-root"),
epoch: viper.GetString("epoch"),
signedOperations: make([]*phase0.SignedVoluntaryExit, 0),
}
// Account and validator are synonymous.

View File

@@ -33,15 +33,21 @@ func (c *command) output(_ context.Context) (string, error) {
}
if c.json || c.offline {
data, err := json.Marshal(c.signedOperation)
var data []byte
var err error
if len(c.signedOperations) == 1 {
data, err = json.Marshal(c.signedOperations[0])
} else {
data, err = json.Marshal(c.signedOperations)
}
if err != nil {
return "", errors.Wrap(err, "failed to marshal signed operation")
return "", errors.Wrap(err, "failed to marshal signed operations")
}
if c.json {
return string(data), nil
}
if err := os.WriteFile(exitOperationFilename, data, 0o600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", exitOperationFilename))
if err := os.WriteFile(exitOperationsFilename, data, 0o600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", exitOperationsFilename))
}
return "", nil
}

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
@@ -48,7 +49,7 @@ var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$")
var (
offlinePreparationFilename = "offline-preparation.json"
exitOperationFilename = "exit-operation.json"
exitOperationsFilename = "exit-operations.json"
)
func (c *command) process(ctx context.Context) error {
@@ -68,37 +69,41 @@ func (c *command) process(ctx context.Context) error {
return err
}
if err := c.obtainOperation(ctx); err != nil {
if err := c.obtainOperations(ctx); err != nil {
return err
}
if validated, reason := c.validateOperation(ctx); !validated {
return fmt.Errorf("operation failed validation: %s", reason)
if len(c.signedOperations) == 0 {
return errors.New("no suitable validators found; no operations generated")
}
if validated, reason := c.validateOperations(ctx); !validated {
return fmt.Errorf("operations failed validation: %s", reason)
}
if c.json || c.offline {
if c.debug {
fmt.Fprintf(os.Stderr, "Not broadcasting exit operation\n")
fmt.Fprintf(os.Stderr, "Not broadcasting exit operations\n")
}
// Want JSON output, or cannot broadcast.
return nil
}
return c.broadcastOperation(ctx)
return c.broadcastOperations(ctx)
}
func (c *command) obtainOperation(ctx context.Context) error {
if (c.mnemonic == "" || c.path == "") && c.privateKey == "" && c.validator == "" {
func (c *command) obtainOperations(ctx context.Context) error {
if c.mnemonic == "" && c.privateKey == "" && c.validator == "" {
// No input information; fetch the operation from a file.
err := c.obtainOperationFromFileOrInput(ctx)
err := c.obtainOperationsFromFileOrInput(ctx)
if err == nil {
// Success.
return nil
}
if c.signedOperationInput != "" {
if c.signedOperationsInput != "" {
return errors.Wrap(err, "failed to obtain supplied signed operation")
}
return errors.Wrap(err, fmt.Sprintf("no account, mnemonic or private key specified, and no %s file loaded", exitOperationFilename))
return errors.Wrap(err, fmt.Sprintf("no account, mnemonic or private key specified, and no %s file loaded", exitOperationsFilename))
}
if c.mnemonic != "" {
@@ -110,7 +115,8 @@ func (c *command) obtainOperation(ctx context.Context) error {
// Have a mnemonic and validator.
return c.generateOperationFromMnemonicAndValidator(ctx)
default:
return errors.New("mnemonic must be supplied with either a path or validator")
// Have a mnemonic only.
return c.generateOperationsFromMnemonic(ctx)
}
}
@@ -126,6 +132,12 @@ func (c *command) obtainOperation(ctx context.Context) error {
}
func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) error {
// Turn the validators in to a map for easy lookup.
validators := make(map[string]*beacon.ValidatorInfo, 0)
for _, validator := range c.chainInfo.Validators {
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
}
seed, err := util.SeedFromMnemonic(c.mnemonic)
if err != nil {
return err
@@ -137,9 +149,13 @@ func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) erro
return fmt.Errorf("path %s does not match EIP-2334 format for a validator", c.path)
}
if err := c.generateOperationFromSeedAndPath(ctx, seed, validatorKeyPath); err != nil {
found, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath)
if err != nil {
return errors.Wrap(err, "failed to generate operation from seed and path")
}
if !found {
return errors.New("no validator found with the provided path and mnemonic, please run with --debug to see more information")
}
return nil
}
@@ -187,6 +203,49 @@ func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context)
return nil
}
func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
seed, err := util.SeedFromMnemonic(c.mnemonic)
if err != nil {
return err
}
// Turn the validators in to a map for easy lookup.
validators := make(map[string]*beacon.ValidatorInfo, 0)
for _, validator := range c.chainInfo.Validators {
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
}
maxDistance := 1024
// Start scanning the validator keys.
lastFoundIndex := 0
foundValidatorCount := 0
for i := 0; ; i++ {
// If no validators have been found in the last maxDistance indices, stop scanning.
if i-lastFoundIndex > maxDistance {
// If no validators were found at all, return an error.
if foundValidatorCount == 0 {
return fmt.Errorf("failed to find validators using the provided mnemonic: searched %d indices without finding a validator", maxDistance)
}
break
}
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
found, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath)
if err != nil {
// We log errors but keep going.
if c.debug {
fmt.Fprintf(os.Stderr, "Failed to generate for path %s: %v\n", validatorKeyPath, err.Error())
}
}
if found {
lastFoundIndex = i
foundValidatorCount++
}
}
return nil
}
func (c *command) generateOperationFromPrivateKey(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
@@ -205,62 +264,104 @@ func (c *command) generateOperationFromValidator(ctx context.Context) error {
return c.generateOperationFromAccount(ctx, validatorAccount)
}
func (c *command) obtainOperationFromFileOrInput(ctx context.Context) error {
// Start off by attempting to use the provided signed operation.
if c.signedOperationInput != "" {
return c.obtainOperationFromInput(ctx)
func (c *command) obtainOperationsFromFileOrInput(ctx context.Context) error {
// Start off by attempting to use the provided signed operations.
if c.signedOperationsInput != "" {
return c.obtainOperationsFromInput(ctx)
}
// If not, read it from the file with the standard name.
return c.obtainOperationFromFile(ctx)
return c.obtainOperationsFromFile(ctx)
}
func (c *command) obtainOperationFromFile(ctx context.Context) error {
_, err := os.Stat(exitOperationFilename)
func (c *command) obtainOperationsFromFile(ctx context.Context) error {
_, err := os.Stat(exitOperationsFilename)
if err != nil {
return errors.Wrap(err, "failed to read exit operation file")
return errors.Wrap(err, "failed to read exit operations file")
}
if c.debug {
fmt.Fprintf(os.Stderr, "%s found; loading operation\n", exitOperationFilename)
fmt.Fprintf(os.Stderr, "%s found; loading operations\n", exitOperationsFilename)
}
data, err := os.ReadFile(exitOperationFilename)
data, err := os.ReadFile(exitOperationsFilename)
if err != nil {
return errors.Wrap(err, "failed to read exit operation file")
return errors.Wrap(err, "failed to read exit operations file")
}
if err := json.Unmarshal(data, &c.signedOperation); err != nil {
return errors.Wrap(err, "failed to parse exit operation file")
if err := json.Unmarshal(data, &c.signedOperations); err != nil {
return errors.Wrap(err, "failed to parse exit operations file")
}
return c.verifySignedOperation(ctx, c.signedOperation)
return c.verifySignedOperations(ctx)
}
func (c *command) obtainOperationFromInput(ctx context.Context) error {
if !strings.HasPrefix(c.signedOperationInput, "{") {
func (c *command) obtainOperationsFromInput(ctx context.Context) error {
if !strings.HasPrefix(c.signedOperationsInput, "{") &&
!strings.HasPrefix(c.signedOperationsInput, "[") {
// This looks like a file; read it in.
data, err := os.ReadFile(c.signedOperationInput)
data, err := os.ReadFile(c.signedOperationsInput)
if err != nil {
return errors.Wrap(err, "failed to read input file")
}
c.signedOperationInput = string(data)
c.signedOperationsInput = string(data)
}
if err := json.Unmarshal([]byte(c.signedOperationInput), &c.signedOperation); err != nil {
if strings.HasPrefix(c.signedOperationsInput, "{") {
// Single operation; put it in an array.
c.signedOperationsInput = fmt.Sprintf("[%s]", c.signedOperationsInput)
}
if err := json.Unmarshal([]byte(c.signedOperationsInput), &c.signedOperations); err != nil {
return errors.Wrap(err, "failed to parse exit operation input")
}
return c.verifySignedOperation(ctx, c.signedOperation)
return c.verifySignedOperations(ctx)
}
func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
validators map[string]*beacon.ValidatorInfo,
seed []byte,
path string,
) error {
) (
bool,
error,
) {
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, path)
if err != nil {
return errors.Wrap(err, "failed to generate validator private key")
return false, errors.Wrap(err, "failed to generate validator private key")
}
c.privateKey = fmt.Sprintf("%#x", validatorPrivkey.Marshal())
return c.generateOperationFromPrivateKey(ctx)
privateKey := fmt.Sprintf("%#x", validatorPrivkey.Marshal())
validatorInfo, exists := validators[fmt.Sprintf("%#x", validatorPrivkey.PublicKey().Marshal())]
if !exists {
return false, errors.New("unknown validator")
}
if validatorInfo.State == apiv1.ValidatorStateActiveExiting ||
validatorInfo.State == apiv1.ValidatorStateActiveSlashed ||
validatorInfo.State == apiv1.ValidatorStateExitedUnslashed ||
validatorInfo.State == apiv1.ValidatorStateExitedSlashed ||
validatorInfo.State == apiv1.ValidatorStateWithdrawalPossible ||
validatorInfo.State == apiv1.ValidatorStateWithdrawalDone {
return false, fmt.Errorf("validator is in state %v, not suitable to generate an exit", validatorInfo.State)
}
validatorAccount, err := util.ParseAccount(ctx, privateKey, nil, true)
if err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "no validator found at path %s: %v\n", path, err)
}
return false, nil
}
if validatorAccount == nil {
return false, nil
}
if err := c.generateOperationFromAccount(ctx, validatorAccount); err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "failed to generate operation at path %s: %v\n", path, err)
}
return false, nil
}
return true, nil
}
func (c *command) generateOperationFromAccount(ctx context.Context,
@@ -276,8 +377,40 @@ func (c *command) generateOperationFromAccount(ctx context.Context,
return err
}
c.signedOperation, err = c.createSignedOperation(ctx, info, account, c.chainInfo.Epoch)
return err
epoch, err := c.selectEpoch()
if err != nil {
return err
}
if c.debug {
fmt.Fprintf(os.Stderr, "Using %d for epoch\n", epoch)
}
signedOperation, err := c.createSignedOperation(ctx, info, account, epoch)
if err != nil {
return err
}
c.signedOperations = append(c.signedOperations, signedOperation)
return nil
}
func (c *command) selectEpoch() (phase0.Epoch, error) {
if c.epoch == "" {
// No user-supplied epoch; use the one from chain info.
return c.chainInfo.Epoch, nil
}
epoch, err := strconv.ParseInt(c.epoch, 10, 64)
if err != nil {
return 0, errors.New("epoch invalid")
}
if epoch < 0 {
// Relative epoch.
return c.chainInfo.Epoch - phase0.Epoch(-epoch), nil
}
return phase0.Epoch(epoch), nil
}
func (c *command) createSignedOperation(ctx context.Context,
@@ -322,6 +455,15 @@ func (c *command) createSignedOperation(ctx context.Context,
}, nil
}
func (c *command) verifySignedOperations(ctx context.Context) error {
for _, op := range c.signedOperations {
if err := c.verifySignedOperation(ctx, op); err != nil {
return err
}
}
return nil
}
func (c *command) verifySignedOperation(ctx context.Context, op *phase0.SignedVoluntaryExit) error {
root, err := op.Message.HashTreeRoot()
if err != nil {
@@ -366,14 +508,26 @@ func (c *command) verifySignedOperation(ctx context.Context, op *phase0.SignedVo
return nil
}
func (c *command) validateOperations(ctx context.Context) (bool, string) {
for _, op := range c.signedOperations {
valid, issue := c.validateOperation(ctx, op)
if !valid {
return valid, issue
}
}
return true, ""
}
func (c *command) validateOperation(_ context.Context,
op *phase0.SignedVoluntaryExit,
) (
bool,
string,
) {
var validatorInfo *beacon.ValidatorInfo
for _, chainValidatorInfo := range c.chainInfo.Validators {
if chainValidatorInfo.Index == c.signedOperation.Message.ValidatorIndex {
if chainValidatorInfo.Index == op.Message.ValidatorIndex {
validatorInfo = chainValidatorInfo
break
}
@@ -382,7 +536,7 @@ func (c *command) validateOperation(_ context.Context,
return false, "validator not known on chain"
}
if c.debug {
fmt.Fprintf(os.Stderr, "Validator exit operation: %v", c.signedOperation)
fmt.Fprintf(os.Stderr, "Validator exit operation: %v", op)
fmt.Fprintf(os.Stderr, "On-chain validator info: %v\n", validatorInfo)
}
@@ -398,8 +552,20 @@ func (c *command) validateOperation(_ context.Context,
return true, ""
}
func (c *command) broadcastOperation(ctx context.Context) error {
return c.consensusClient.(consensusclient.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, c.signedOperation)
func (c *command) broadcastOperations(ctx context.Context) error {
for _, op := range c.signedOperations {
if c.debug {
data, err := json.Marshal(op)
if err != nil {
fmt.Fprintf(os.Stderr, "Broadcasting %s\n", string(data))
}
}
if err := c.consensusClient.(consensusclient.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, op); err != nil {
return err
}
}
return nil
}
func (c *command) setup(ctx context.Context) error {

View File

@@ -17,6 +17,7 @@ import (
"context"
"testing"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/beacon"
@@ -95,7 +96,7 @@ func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, test.command.signedOperation)
require.Equal(t, test.expected, test.command.signedOperations[0])
}
})
}
@@ -187,7 +188,7 @@ func TestGenerateOperationFromMnemonicAndValidator(t *testing.T) {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, test.command.signedOperation)
require.Equal(t, test.expected, test.command.signedOperations[0])
}
})
}
@@ -198,29 +199,44 @@ func TestGenerateOperationFromSeedAndPath(t *testing.T) {
require.NoError(t, e2types.InitBLS())
validator0 := &beacon.ValidatorInfo{
Index: 0,
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
State: apiv1.ValidatorStateActiveOngoing,
}
validator1 := &beacon.ValidatorInfo{
Index: 1,
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
State: apiv1.ValidatorStateActiveOngoing,
}
validator2 := &beacon.ValidatorInfo{
Index: 2,
Pubkey: phase0.BLSPubKey{0xaf, 0x9c, 0xe4, 0x4f, 0x50, 0x14, 0x8d, 0xb4, 0x12, 0x19, 0x4a, 0xf0, 0xba, 0xf0, 0xba, 0xb3, 0x6b, 0xd5, 0xc3, 0xe0, 0xc4, 0x93, 0x89, 0x11, 0xa4, 0xe5, 0x02, 0xe3, 0x98, 0xb5, 0x9e, 0x5c, 0xca, 0x7c, 0x78, 0xe3, 0xfe, 0x03, 0x41, 0x95, 0x47, 0x88, 0x79, 0xee, 0xb2, 0x3d, 0xb0, 0xa6},
WithdrawalCredentials: []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
State: apiv1.ValidatorStateActiveOngoing,
}
validator3 := &beacon.ValidatorInfo{
Index: 3,
Pubkey: phase0.BLSPubKey{0x86, 0xd3, 0x30, 0xaf, 0x51, 0xfa, 0x59, 0x3f, 0xa9, 0xf9, 0x3e, 0xdb, 0x9d, 0x16, 0x64, 0x01, 0x86, 0xbe, 0x2e, 0x93, 0xea, 0x94, 0xd2, 0x59, 0x78, 0x1e, 0x1e, 0xb3, 0x4d, 0xeb, 0x84, 0x4c, 0x39, 0x68, 0xd7, 0x5e, 0xa9, 0x1d, 0x19, 0xf1, 0x59, 0xdb, 0xd0, 0x52, 0x3c, 0x6c, 0x5b, 0xa5},
WithdrawalCredentials: []byte{0x00, 0x81, 0x68, 0x45, 0x6b, 0x6d, 0x9a, 0x32, 0x83, 0x93, 0x1f, 0xea, 0x52, 0x10, 0xda, 0x12, 0x2d, 0x1e, 0x65, 0xe8, 0xed, 0x50, 0xb8, 0xe8, 0xf5, 0x91, 0x11, 0x83, 0xb0, 0x2f, 0xd1, 0x25},
State: apiv1.ValidatorStateActiveOngoing,
}
validators := map[string]*beacon.ValidatorInfo{
"0xb384f767d964e100c8a9b21018d08c25ffebae268b3ab6d610353897541971726dbfc3c7463884c68a531515aab94c87": validator0,
"0xb3d89e2f29c712c6a9f8e5a269b97617c4a94dd6f6662ab3b07ce9e5434573f15b5c988cd14bbd5804f77156a8af1cfa": validator1,
"0xaf9ce44f50148db412194af0baf0bab36bd5c3e0c4938911a4e502e398b59e5cca7c78e3fe034195478879eeb23db0a6": validator2,
"0x86d330af51fa593fa9f93edb9d16640186be2e93ea94d259781e1eb34deb844c3968d75ea91d19f159dbd0523c6c5ba5": validator3,
}
chainInfo := &beacon.ChainInfo{
Version: 1,
Validators: []*beacon.ValidatorInfo{
{
Index: 0,
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
},
{
Index: 1,
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
},
{
Index: 2,
Pubkey: phase0.BLSPubKey{0xaf, 0x9c, 0xe4, 0x4f, 0x50, 0x14, 0x8d, 0xb4, 0x12, 0x19, 0x4a, 0xf0, 0xba, 0xf0, 0xba, 0xb3, 0x6b, 0xd5, 0xc3, 0xe0, 0xc4, 0x93, 0x89, 0x11, 0xa4, 0xe5, 0x02, 0xe3, 0x98, 0xb5, 0x9e, 0x5c, 0xca, 0x7c, 0x78, 0xe3, 0xfe, 0x03, 0x41, 0x95, 0x47, 0x88, 0x79, 0xee, 0xb2, 0x3d, 0xb0, 0xa6},
WithdrawalCredentials: []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
},
{
Index: 3,
Pubkey: phase0.BLSPubKey{0x86, 0xd3, 0x30, 0xaf, 0x51, 0xfa, 0x59, 0x3f, 0xa9, 0xf9, 0x3e, 0xdb, 0x9d, 0x16, 0x64, 0x01, 0x86, 0xbe, 0x2e, 0x93, 0xea, 0x94, 0xd2, 0x59, 0x78, 0x1e, 0x1e, 0xb3, 0x4d, 0xeb, 0x84, 0x4c, 0x39, 0x68, 0xd7, 0x5e, 0xa9, 0x1d, 0x19, 0xf1, 0x59, 0xdb, 0xd0, 0x52, 0x3c, 0x6c, 0x5b, 0xa5},
WithdrawalCredentials: []byte{0x00, 0x81, 0x68, 0x45, 0x6b, 0x6d, 0x9a, 0x32, 0x83, 0x93, 0x1f, 0xea, 0x52, 0x10, 0xda, 0x12, 0x2d, 0x1e, 0x65, 0xe8, 0xed, 0x50, 0xb8, 0xe8, 0xf5, 0x91, 0x11, 0x83, 0xb0, 0x2f, 0xd1, 0x25},
},
validator0,
validator1,
validator2,
validator3,
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
@@ -296,12 +312,14 @@ func TestGenerateOperationFromSeedAndPath(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.generateOperationFromSeedAndPath(ctx, test.seed, test.path)
found, err := test.command.generateOperationFromSeedAndPath(ctx, validators, test.seed, test.path)
if test.err != "" {
require.False(t, found)
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, test.command.signedOperation)
require.True(t, found)
require.Equal(t, test.expected, test.command.signedOperations[0])
}
})
}
@@ -340,10 +358,12 @@ func TestVerifyOperation(t *testing.T) {
name: "SignatureMissing",
command: &command{
chainInfo: chainInfo,
signedOperation: &phase0.SignedVoluntaryExit{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 0,
signedOperations: []*phase0.SignedVoluntaryExit{
{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 0,
},
},
},
},
@@ -353,12 +373,14 @@ func TestVerifyOperation(t *testing.T) {
name: "SignatureShort",
command: &command{
chainInfo: chainInfo,
signedOperation: &phase0.SignedVoluntaryExit{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 0,
signedOperations: []*phase0.SignedVoluntaryExit{
{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 0,
},
Signature: phase0.BLSSignature{0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
},
Signature: phase0.BLSSignature{0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
},
},
err: "invalid signature",
@@ -367,12 +389,14 @@ func TestVerifyOperation(t *testing.T) {
name: "SignatureIncorrect",
command: &command{
chainInfo: chainInfo,
signedOperation: &phase0.SignedVoluntaryExit{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 0,
signedOperations: []*phase0.SignedVoluntaryExit{
{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 0,
},
Signature: phase0.BLSSignature{0x99, 0x78, 0xb4, 0x9c, 0x21, 0x60, 0x3f, 0x04, 0xa3, 0x04, 0x4e, 0x4c, 0x49, 0x0c, 0xb4, 0x68, 0x7c, 0x6e, 0x14, 0xc2, 0xda, 0xed, 0x25, 0x92, 0xe0, 0x02, 0x2d, 0xcd, 0x63, 0xeb, 0xe7, 0x4a, 0xf1, 0x1a, 0xca, 0xba, 0xae, 0x50, 0xe1, 0x8a, 0x1d, 0xae, 0x96, 0xd9, 0xd2, 0x56, 0xbf, 0x9f, 0x02, 0x48, 0x85, 0x05, 0xc1, 0xfb, 0xb3, 0x4a, 0x0b, 0x68, 0xec, 0xc5, 0xb5, 0xf5, 0xea, 0x53, 0xdb, 0xd0, 0x09, 0x08, 0xe3, 0x1e, 0xa8, 0xca, 0x9d, 0x02, 0x08, 0x3b, 0x9e, 0xf1, 0xc7, 0xd2, 0x32, 0xf4, 0xba, 0xd9, 0xea, 0x56, 0x4b, 0xc5, 0x87, 0xd5, 0x27, 0xb7, 0x74, 0x97, 0x8a, 0xee},
},
Signature: phase0.BLSSignature{0x99, 0x78, 0xb4, 0x9c, 0x21, 0x60, 0x3f, 0x04, 0xa3, 0x04, 0x4e, 0x4c, 0x49, 0x0c, 0xb4, 0x68, 0x7c, 0x6e, 0x14, 0xc2, 0xda, 0xed, 0x25, 0x92, 0xe0, 0x02, 0x2d, 0xcd, 0x63, 0xeb, 0xe7, 0x4a, 0xf1, 0x1a, 0xca, 0xba, 0xae, 0x50, 0xe1, 0x8a, 0x1d, 0xae, 0x96, 0xd9, 0xd2, 0x56, 0xbf, 0x9f, 0x02, 0x48, 0x85, 0x05, 0xc1, 0xfb, 0xb3, 0x4a, 0x0b, 0x68, 0xec, 0xc5, 0xb5, 0xf5, 0xea, 0x53, 0xdb, 0xd0, 0x09, 0x08, 0xe3, 0x1e, 0xa8, 0xca, 0x9d, 0x02, 0x08, 0x3b, 0x9e, 0xf1, 0xc7, 0xd2, 0x32, 0xf4, 0xba, 0xd9, 0xea, 0x56, 0x4b, 0xc5, 0x87, 0xd5, 0x27, 0xb7, 0x74, 0x97, 0x8a, 0xee},
},
},
err: "signature does not verify",
@@ -383,12 +407,14 @@ func TestVerifyOperation(t *testing.T) {
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
validator: "0",
chainInfo: chainInfo,
signedOperation: &phase0.SignedVoluntaryExit{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 0,
signedOperations: []*phase0.SignedVoluntaryExit{
{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 0,
},
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
},
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
},
},
},
@@ -396,7 +422,7 @@ func TestVerifyOperation(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.verifySignedOperation(ctx, test.command.signedOperation)
err := test.command.verifySignedOperation(ctx, test.command.signedOperations[0])
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
@@ -438,39 +464,39 @@ func TestObtainOperationFromInput(t *testing.T) {
{
name: "InvalidFilename",
command: &command{
signedOperationInput: `[]`,
chainInfo: chainInfo,
signedOperationsInput: `missing.json`,
chainInfo: chainInfo,
},
err: "failed to read input file: open []: no such file or directory",
err: "failed to read input file: open missing.json: no such file or directory",
},
{
name: "InvalidJSON",
command: &command{
signedOperationInput: `{invalid}`,
chainInfo: chainInfo,
signedOperationsInput: `{invalid}`,
chainInfo: chainInfo,
},
err: "failed to parse exit operation input: invalid character 'i' looking for beginning of object key string",
},
{
name: "Unverifable",
command: &command{
signedOperationInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x9978b49c21603f04a3044e4c490cb4687c6e14c2daed2592e0022dcd63ebe74af11acabaae50e18a1dae96d9d256bf9f02488505c1fbb34a0b68ecc5b5f5ea53dbd00908e31ea8ca9d02083b9ef1c7d232f4bad9ea564bc587d527b774978aee"}`,
chainInfo: chainInfo,
signedOperationsInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x9978b49c21603f04a3044e4c490cb4687c6e14c2daed2592e0022dcd63ebe74af11acabaae50e18a1dae96d9d256bf9f02488505c1fbb34a0b68ecc5b5f5ea53dbd00908e31ea8ca9d02083b9ef1c7d232f4bad9ea564bc587d527b774978aee"}`,
chainInfo: chainInfo,
},
err: "signature does not verify",
},
{
name: "Good",
command: &command{
signedOperationInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x89f5c44288f95e19b6c139f2623005665b983462a228120977d81f2ef547560be22446de21a8a937d9dda4e2d2ec4175196496cdd1306dec4a125f8c861f806171504a9d6a610ec4e135047e4fb67052ecc4561360d0c3de04b6fbc4474223ff"}`,
chainInfo: chainInfo,
signedOperationsInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x89f5c44288f95e19b6c139f2623005665b983462a228120977d81f2ef547560be22446de21a8a937d9dda4e2d2ec4175196496cdd1306dec4a125f8c861f806171504a9d6a610ec4e135047e4fb67052ecc4561360d0c3de04b6fbc4474223ff"}`,
chainInfo: chainInfo,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.obtainOperationFromInput(ctx)
err := test.command.obtainOperationsFromFileOrInput(ctx)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {

View File

@@ -46,9 +46,9 @@ func input(_ context.Context) (*dataIn, error) {
}
data.mnemonic = viper.GetString("mnemonic")
data.privKey = viper.GetString("privkey")
data.privKey = viper.GetString("private-key")
if data.mnemonic == "" && data.privKey == "" {
return nil, errors.New("mnemonic or privkey is required")
return nil, errors.New("mnemonic or private key is required")
}
return data, nil

View File

@@ -38,7 +38,7 @@ func TestInput(t *testing.T) {
vars: map[string]interface{}{
"withdrawal-credentials": "0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02",
},
err: "mnemonic or privkey is required",
err: "mnemonic or private key is required",
},
{
name: "GoodWithMnemonic",

View File

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

View File

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

View File

@@ -0,0 +1,161 @@
// 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))
case spec.DataVersionDeneb:
withdrawals := block.Deneb.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
}

View File

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

View File

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

View File

@@ -61,38 +61,34 @@ func init() {
validatorCredentialsSetCmd.Flags().String("withdrawal-account", "", "Account with which the validator's withdrawal credentials were set")
validatorCredentialsSetCmd.Flags().String("withdrawal-address", "", "Execution address to which to direct withdrawals")
validatorCredentialsSetCmd.Flags().String("signed-operations", "", "Use pre-defined JSON signed operation as created by --json to transmit the credentials change operation (reads from change-operations.json if not present)")
validatorCredentialsSetCmd.Flags().Bool("json", false, "Generate JSON data containing a signed operation rather than broadcast it to the network (implied when offline)")
validatorCredentialsSetCmd.Flags().Bool("offline", false, "Do not attempt to connect to a beacon node to obtain information for the operation")
validatorCredentialsSetCmd.Flags().String("fork-version", "", "Fork version to use for signing (overrides fetching from beacon node)")
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("json", validatorCredentialsSetCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("offline", cmd.Flags().Lookup("offline")); err != nil {
panic(err)
}
if err := viper.BindPFlag("offline", validatorCredentialsSetCmd.Flags().Lookup("offline")); err != nil {
if err := viper.BindPFlag("fork-version", cmd.Flags().Lookup("fork-version")); err != nil {
panic(err)
}
if err := viper.BindPFlag("fork-version", validatorCredentialsSetCmd.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)
}
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
// Copyright © 2020 Weald Technology Trading
// Copyright © 2020, 2023 Weald Technology Trading
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -23,13 +23,16 @@ import (
var validatorExitCmd = &cobra.Command{
Use: "exit",
Short: "Send an exit request for a validator",
Long: `Send an exit request for a validator. For example:
Short: "Send an exit request for one or more validators",
Long: `Send an exit request for one or more validators. For example:
ethdo validator exit --validator=12345
The validator and key can be specified in one of a number of ways:
- mnemonic using --mnemonic; this will scan the mnemonic and generate all applicable operations
- mnemonic and path to the validator key using --mnemonic and --path; this will generate a single operation
- mnemonic and validator index or public key --mnemonic and --validator; this will generate a single operation
- mnemonic and path to the validator using --mnemonic and --path
- mnemonic and validator index or public key using --mnemonic and --validator
- validator private key using --private-key
@@ -54,39 +57,35 @@ In quiet mode this will return 0 if the exit operation has been generated (and s
func init() {
validatorCmd.AddCommand(validatorExitCmd)
validatorFlags(validatorExitCmd)
validatorExitCmd.Flags().Int64("epoch", -1, "Epoch at which to exit (defaults to current epoch)")
validatorExitCmd.Flags().String("epoch", "", "Epoch at which to exit (defaults to current epoch)")
validatorExitCmd.Flags().Bool("prepare-offline", false, "Create files for offline use")
validatorExitCmd.Flags().String("validator", "", "Validator to exit")
validatorExitCmd.Flags().String("signed-operation", "", "Use pre-defined JSON signed operation as created by --json to transmit the exit operation (reads from exit-operation.json if not present)")
validatorExitCmd.Flags().Bool("json", false, "Generate JSON data containing a signed operation rather than broadcast it to the network (implied when offline)")
validatorExitCmd.Flags().String("signed-operations", "", "Use pre-defined JSON signed operation as created by --json to transmit the exit operations (reads from exit-operations.json if not present)")
validatorExitCmd.Flags().Bool("offline", false, "Do not attempt to connect to a beacon node to obtain information for the operation")
validatorExitCmd.Flags().String("fork-version", "", "Fork version to use for signing (overrides fetching from beacon node)")
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-operations", cmd.Flags().Lookup("signed-operations")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", validatorExitCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("offline", cmd.Flags().Lookup("offline")); err != nil {
panic(err)
}
if err := viper.BindPFlag("offline", validatorExitCmd.Flags().Lookup("offline")); err != nil {
if err := viper.BindPFlag("fork-version", cmd.Flags().Lookup("fork-version")); err != nil {
panic(err)
}
if err := viper.BindPFlag("fork-version", validatorExitCmd.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)
}
}

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ var validatorKeycheckCmd = &cobra.Command{
Short: "Check that the withdrawal credentials for a validator matches the given key.",
Long: `Check that the withdrawal credentials for a validator matches the given key. For example:
ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --privkey=0x1b46e61babc7a6a0fbfe8e416de3c71f85e367f24e0bfcb12e57adb11117662c
ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --private-key=0x1b46e61babc7a6a0fbfe8e416de3c71f85e367f24e0bfcb12e57adb11117662c
A mnemonic can be used in place of a private key, in which case the first 1,024 indices of the standard withdrawal key path will be scanned for a matching key.
@@ -50,14 +50,10 @@ func init() {
validatorCmd.AddCommand(validatorKeycheckCmd)
validatorFlags(validatorKeycheckCmd)
validatorKeycheckCmd.Flags().String("withdrawal-credentials", "", "Withdrawal credentials to check (can run offline)")
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 {
panic(err)
}
if err := viper.BindPFlag("privkey", validatorKeycheckCmd.Flags().Lookup("privkey")); err != nil {
func validatorKeycheckBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("withdrawal-credentials", cmd.Flags().Lookup("withdrawal-credentials")); err != nil {
panic(err)
}
}

View File

@@ -49,18 +49,14 @@ func init() {
validatorFlags(validatorSummaryCmd)
validatorSummaryCmd.Flags().String("epoch", "", "the epoch for which to obtain information ()")
validatorSummaryCmd.Flags().StringSlice("validators", nil, "the list of validators for which to obtain information")
validatorSummaryCmd.Flags().Bool("json", false, "output data in JSON format")
}
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 {
panic(err)
}
if err := viper.BindPFlag("json", validatorSummaryCmd.Flags().Lookup("json")); err != nil {
if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil {
panic(err)
}
}

View File

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

View File

@@ -48,14 +48,10 @@ func init() {
validatorCmd.AddCommand(validatorYieldCmd)
validatorFlags(validatorYieldCmd)
validatorYieldCmd.Flags().String("validators", "", "Number of active validators (default fetches from chain)")
validatorYieldCmd.Flags().Bool("json", false, "JSON output")
}
func validatorYieldBindings() {
if err := viper.BindPFlag("validators", validatorYieldCmd.Flags().Lookup("validators")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", validatorYieldCmd.Flags().Lookup("json")); err != nil {
func validatorYieldBindings(cmd *cobra.Command) {
if err := viper.BindPFlag("validators", cmd.Flags().Lookup("validators")); err != nil {
panic(err)
}
}

View File

@@ -24,7 +24,7 @@ import (
// ReleaseVersion is the release version of the codebase.
// Usually overridden by tag names when building binaries.
var ReleaseVersion = "local build (latest release 1.29.2)"
var ReleaseVersion = "local build (latest release 1.31.0)"
// versionCmd represents the version command.
var versionCmd = &cobra.Command{

View File

@@ -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() != "" {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

204
docs/exitingvalidators.md Normal file
View File

@@ -0,0 +1,204 @@
# Exiting validators
Exiting a validator relieves the validator of its duties and makes the initial deposit eligible for withdrawal. This document provides information on how to exit one or more validators given account information.
## Concepts
The following concepts are useful when understanding the rest of this guide.
### Validator
A validator is a logical entity that secures the Ethereum beacon chain (and hence the execution chain) by proposing blocks and attesting to blocks proposed by other validators.
### Private key
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_. 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".
### Online and Offline
An _online_ computer is one that is is connected to the internet. It should be running a consensus node connected to the larger Ethereum network. An online computer is required to carry out the process, to obtain information from the consensus node and to broadcast your actions to the rest of the Ethereum network.
An _offline_ computer is one that is not connected to the internet. As such, it will not be running a consensus node. It can optionally be used in conjunction with an online computer to provide higher levels of security for your mnemonic or private key, but is less convenient because it requires manual transfer of files from the online computer to the offline computer, and back.
If you use your mnemonic when generating exit operations you should use the offline process. If you use a private key or keystore then the online process should be safe.
With only an online computer the flow of information is roughly as follows:
![Online process](images/exit-online.png)
Here it can be seen that a copy of `ethdo` with access to private keys connects to a consensus node with access to the internet. Due to its connection to the internet it is possible that the computer on which `ethdo` and the consensus node runs has been compromised, and as such would expose the private keys to an attacker.
With both an offline and an online computer the flow of information is roughly as follows:
![Offline process](images/exit-offline.png)
Here the copy of `ethdo` with access to private keys is on an offline computer, which protects it from being compromised via the internet. Data is physically moved from the offline to the online computer via a USB storage key or similar, and none of the information on the online computer is sensitive.
## Preparation
Regardless of the method selected, preparation must take place on the online computer to ensure that `ethdo` can access your consensus node. `ethdo` will attempt to find a local consensus node automatically, but if not then an explicit connection value will be required. To find out if `ethdo` has access to the consensus node run:
```sh
ethdo node info
```
The result should be something similar to the following:
```
Syncing: false
```
Alternatively, the result may look like this:
```
No connection supplied; using mainnet public access endpoint
Syncing: false
```
which means that a local consensus node was not accessed and instead a public endpoint specifically assigned to handle these operations was used instead. If you do have a local consensus node but see this message it means that the local node could not be accessed, usually because it is running on a non-standard port. If this is the case for your configuration, you need to let `ethdo` know where the consensus node's REST API is. For example, if your consensus node is serving its REST API on port 12345 then you should add `--connection=http://localhost:12345` to all `ethdo` commands in this process, for example:
```sh
ethdo --connection=http://localhost:12345 node info
```
Note that some consensus nodes may require configuration to serve their REST API. Please refer to the documentation of your specific consensus node to enable this.
Regardless of your method used above, it is important to confirm that the "Syncing" value is "false". If this is "true" it means that the node is currently syncing, and you will need to wait for the process to finish before proceeding.
Once the preparation is complete you should select either basic or advanced operation, depending on your requirements.
## Basic operation
Given the above concepts, the purpose of this guide is to exit one or more active validators, allowing the initial deposit to be returned.
Basic operation is suitable in the majority of cases. If you:
- generated your validators using a mnemonic (_e.g._ using the deposit CLI or launchpad)
- want to exit all of your validators at the same time
then this method is for you. If any of the above does not apply then please go to the "Advanced operation" section.
### Online process
The online process generates and broadcasts the operations to exit all of your validators tied to a mnemonic in a single action.
One piece of information are required for carrying out this process online: the mnemonic.
On your _online_ computer run the following:
```
ethdo validator exit --mnemonic="abandon abandon abandon … art"
```
Replacing the `mnemonic` value with your own values. This command will:
1. obtain information from your consensus node about all currently-running validators and various additional information required to generate the operations
2. scan your mnemonic to find any validators that were generated by it, and create the operations to exit
3. broadcast the exit operations to the Ethereum network
### Online and Offline process
The online and offline process contains three steps. In the first, data is gathered on the online computer. In the second, the exit operations are generated on the offline computer. In the third, the operations are broadcast on the online computer.
One piece of information are required for carrying out this process online: the mnemonic from which the validators were derived.
On your _online_ computer run the following:
```
ethdo validator exit --prepare-offline
```
This command will:
1. obtain information from your consensus node about all currently-running validators and various additional information required to generate the operations
2. write this information to a file called `offline-preparation.json`
The `offline-preparation.json` file must be copied to your _offline_ computer. Once this has been done, on your _offline_ computer run the following:
```
ethdo validator exit --offline --mnemonic="abandon abandon abandon … art"
```
Replacing the `mnemonic` value with your own value. This command will:
1. read the `offline-preparation.json` file to obtain information about all currently-running validators and various additional information required to generate the operations
2. scan your mnemonic to find any validators that were generated by it, and create the operations to exit
3. write this information to a file called `exit-operations.json`
The `exit-operations.json` file must be copied to your _online_ computer. Once this has been done, on your _online_ computer run the following:
```
ethdo validator exit
```
This command will:
1. read the `exit-operations.json` file to obtain the operations to exit the validators
2. broadcast the exit operations to the Ethereum network
## Advanced operation
Advanced operation is required when any of the following conditions are met:
- your validators were created using something other than the deposit CLI or launchpad (_e.g._ `ethdo`)
- you want to exit your validators individually
### Validator reference
There are three options to reference a validator:
- the `ethdo` account of the validator (in format wallet/account)
- the validator's public key (in format 0x…)
- the validator's on-chain index (in format 123…)
- the validator's keystore, either provided directly or as a path to the keystore on the local filesystem
Any of these can be passed to the following commands with the `--validator` parameter. You need to ensure that you have this information before starting the process.
**In the following examples we will use the validator with index 123. Please replace this with the reference to your validator in all commands.**
### Generating exit operations
Note that if you are carrying out this process offline then you still need to carry out the first and third steps outlined in the "Basic operation" section above. This is to ensure that the offline computer has the correct information to generate the operations, and that the operations are made available to the online computer for broadcasting to the network.
If using the online and offline process run the commands below on the offline computer, and add the `--offline` flag to the commands below. You will need to copy the resultant `exit-operations.json` file to the online computer to broadcast to the network.
If using the online process run the commands below on the online computer. The operation will be broadcast to the network automatically.
#### Using a mnemonic and path.
A mnemonic is a 24-word phrase from which withdrawal and validator keys are derived using a _path_. Commonly, keys will have been generated using the path m/12381/3600/_i_/0/0, where _i_ starts at 0 for the first validator, 1 for the second validator, _etc._
however this is only a standard and not a restriction, and it is possible for users to have created validators using paths of their own choice.
```
ethdo validator exit --mnemonic="abandon abandon abandon … art" --path='m/12381/3600/0/0/0'
```
replacing the path with the path to your validator key, and all other parameters with your own values.
#### Using a mnemonic and validator.
Similar to the previous section, however instead of specifying a path instead the index, public key or account of the validator is provided.
```
ethdo validator exit --mnemonic="abandon abandon abandon … art" --validator=123
```
#### Using an account
If you used `ethdo` to create your validator you can specify the accout of the validator to generate and broadcast the exit operation with the following command:
```
ethdo validator exit --account=Wallet/Account --passphrase=secret
```
replacing the parameters with your own values. Note that the passphrase here is the passphrsae of the validator account.
## Confirming the process has succeeded
The final step is confirming the operation has taken place. To do so, run the following command on an online server:
```sh
ethdo validator info --validator=123
```
The result should show the state of the validator as exiting or exited.

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
docs/images/exit-online.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -1,8 +1,12 @@
# 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
Note that the below provides a list of commands rather than a guide for specific situations. Other guides available are:
- [How to change withdrawal credentials for a validator](./changingwithdrawalcredentials.md)
- [How to convert from mnemonics to keys and accounts](./conversions.md)
- [How to achieve common tasks with ethdo](./howto.md)
### `wallet` commands
@@ -28,10 +32,11 @@ Spending: 0x85dfc6dcee4c9da36f6473ec02fda283d6c920c641fc8e3a76113c5c227d4aeeb100
#### `create`
`ethdo wallet create` creates a new wallet with the given parameters. Options for creating a wallet include:
- `wallet`: the name of the wallet to create
- `type`: the type of wallet to create. This can be either "nd" for a non-deterministic wallet, where private keys are generated randomly, or "hd" for a hierarchical deterministic wallet, where private keys are generated from a seed and path as per [ERC-2333](https://github.com/CarlBeek/EIPs/blob/bls_path/EIPS/eip-2334.md) (defaults to "nd")
- `wallet-passphrase`: the passphrase for of the wallet. This is required for hierarchical deterministic wallets, to protect the seed
- `mnemonic`: for hierarchical deterministic wallets only, use a pre-defined 24-word [BIP-39 seed phrase](https://en.bitcoin.it/wiki/Seed_phrase) to create the wallet, along with an additional "seed extension" phrase if required. **Warning** The same mnemonic can be used to create multiple wallets, in which case they will generate the same keys.
- `wallet`: the name of the wallet to create
- `type`: the type of wallet to create. This can be either "nd" for a non-deterministic wallet, where private keys are generated randomly, or "hd" for a hierarchical deterministic wallet, where private keys are generated from a seed and path as per [ERC-2333](https://github.com/CarlBeek/EIPs/blob/bls_path/EIPS/eip-2334.md) (defaults to "nd")
- `wallet-passphrase`: the passphrase for of the wallet. This is required for hierarchical deterministic wallets, to protect the seed
- `mnemonic`: for hierarchical deterministic wallets only, use a pre-defined 24-word [BIP-39 seed phrase](https://en.bitcoin.it/wiki/Seed_phrase) to create the wallet, along with an additional "seed extension" phrase if required. **Warning** The same mnemonic can be used to create multiple wallets, in which case they will generate the same keys.
```sh
$ ethdo wallet create --wallet="Personal wallet" --type="hd" --wallet-passphrase="my wallet secret"
@@ -39,7 +44,8 @@ $ ethdo wallet create --wallet="Personal wallet" --type="hd" --wallet-passphrase
#### `delete`
`ethdo wallet delete` deletes a wallet. Options for deleting a wallet include:
- `wallet`: the name of the wallet to delete
- `wallet`: the name of the wallet to delete
```sh
$ ethdo wallet delete --wallet="Old wallet"
@@ -50,8 +56,9 @@ $ ethdo wallet delete --wallet="Old wallet"
#### `export`
`ethdo wallet export` exports the wallet and all of its accounts. Options for exporting a wallet include:
- `wallet`: the name of the wallet to export (defaults to "primary")
- `passphrase`: the passphrase with which to encrypt the wallet backup
- `wallet`: the name of the wallet to export (defaults to "primary")
- `passphrase`: the passphrase with which to encrypt the wallet backup
```sh
$ ethdo wallet export --wallet="Personal wallet" --passphrase="my export secret"
@@ -67,9 +74,10 @@ $ ethdo wallet export --wallet="Personal wallet" --passphrase="my export secret"
#### `import`
`ethdo wallet import` imports a wallet and all of its accounts exported by `ethdo wallet export`. Options for importing a wallet include:
- `data`: the data exported by `ethdo wallet export`
- `passphrase`: the passphrase that was provided to `ethdo wallet export` to encrypt the data
- `verify`: confirm information about the wallet import without importing it
- `data`: the data exported by `ethdo wallet export`
- `passphrase`: the passphrase that was provided to `ethdo wallet export` to encrypt the data
- `verify`: confirm information about the wallet import without importing it
```sh
$ ethdo wallet import --data="0x01c7a27ad40d45b4ae5be5f..." --passphrase="my export secret"
@@ -84,7 +92,8 @@ $ ethdo wallet import --data=`cat export.dat` --passphrase="my export secret"
#### `info`
`ethdo wallet info` provides information about a given wallet. Options include:
- `wallet`: the name of the wallet
- `wallet`: the name of the wallet
```sh
$ ethdo wallet info --wallet="Personal wallet"
@@ -106,10 +115,11 @@ Personal wallet
#### `sharedexport`
`ethdo wallet sharedexport` exports the wallet and all of its accounts with shared keys. Options for exporting a wallet include:
- `wallet`: the name of the wallet to export (defaults to "primary")
- `participants`: the total number of participants that each hold a share
- `threshold`: the number of participants necessary to provide their share to restore the wallet
- `file`: the name of the file that stores the backup
- `wallet`: the name of the wallet to export (defaults to "primary")
- `participants`: the total number of participants that each hold a share
- `threshold`: the number of participants necessary to provide their share to restore the wallet
- `file`: the name of the file that stores the backup
```sh
$ ethdo wallet sharedexport --wallet="Personal wallet" --participants=3 --threshold=2 --file=backup.dat
@@ -123,8 +133,9 @@ Each line of the output is a share and should be provided to one of the particip
#### `sharedimport`
`ethdo wallet sharedimport` imports a wallet and all of its accounts exported by `ethdo wallet sharedexport`. Options for importing a wallet include:
- `file`: the name of the file that stores the backup
- `shares`: a number of shares, defined by _threshold_ during the export, separated by spaces
- `file`: the name of the file that stores the backup
- `shares`: a number of shares, defined by _threshold_ during the export, separated by spaces
```sh
$ ethdo wallet sharedimport --file=backup.dat --shares="298a…9189 10ea…5063"
@@ -137,9 +148,10 @@ Account commands focus on information about local accounts, generally those used
#### `create`
`ethdo account create` creates a new account with the given parameters. Options for creating an account include:
- `account`: the name of the account to create (in format "wallet/account")
- `passphrase`: the passphrase for the account
- `path`: the HD path for the account (only for hierarchical deterministic accounts)
- `account`: the name of the account to create (in format "wallet/account")
- `passphrase`: the passphrase for the account
- `path`: the HD path for the account (only for hierarchical deterministic accounts)
Note that for hierarchical deterministic wallets you will also need to supply `--wallet-passphrase` to unlock the wallet seed.
@@ -153,10 +165,11 @@ $ ethdo account create --account="Personal wallet/Operations" --wallet-passphras
`ethdo account derive` provides the ability to derive an account's keys without creating either the wallet or the account. This allows users to quickly obtain or confirm keys without going through a relatively long process, and has the added security benefit of not writing any information to disk. Options for deriving the account include:
- `mnemonic`: a pre-defined 24-word [BIP-39 seed phrase](https://en.bitcoin.it/wiki/Seed_phrase) to derive the account, along with an additional "seed extension" phrase if required supplied as the 25th word
- `path`: the HD path used to derive the account
- `show-private-key`: show the private of the derived account. **Warning** displaying private keys, especially those derived from seeds held on hardware wallets, can expose your Ether to risk of being stolen. Only use this option if you are sure you understand the risks involved
- `show-withdrawal-credentials`: show the withdrawal credentials of the derived account
- `mnemonic`: a pre-defined 24-word [BIP-39 seed phrase](https://en.bitcoin.it/wiki/Seed_phrase) to derive the account, along with an additional "seed extension" phrase if required supplied as the 25th word
- `path`: the HD path used to derive the account
- `show-private-key`: show the private of the derived account. **Warning** displaying private keys, especially those derived from seeds held on hardware wallets, can expose your Ether to risk of being stolen. Only use this option if you are sure you understand the risks involved
- `show-withdrawal-credentials`: show the withdrawal credentials of the derived account
- `generate-keystore`: generate a keystore for the account
```sh
$ ethdo account derive --mnemonic="abandon ... abandon art" --path="m/12381/3600/0/0"
@@ -166,9 +179,10 @@ Public key: 0x99b1f1d84d76185466d86c34bde1101316afddae76217aa86cd066979b19858c2c
#### `import`
`ethdo account import` creates a new account by importing its private key. Options for creating the account include:
- `account`: the name of the account to create (in format "wallet/account")
- `passphrase`: the passphrase for the account
- `key`: the private key to import
- `account`: the name of the account to create (in format "wallet/account")
- `passphrase`: the passphrase for the account
- `key`: the private key to import
```sh
$ ethdo account import --account=Validators/123 --key=6dd12d588d1c05ba40e80880ac7e894aa20babdbf16da52eae26b3f267d68032 --passphrase="my account secret"
@@ -179,12 +193,14 @@ You can also import from an existing keystore such as those generated by the dep
```sh
$ ethdo account import --account=Validators/123 --keystore=/path/to/keystore.json --keystore-passphrase="the keystore secret" --passphrase="my account secret"
```
`--keystore` can either be the path to the keystore file, or the contents of the keystore file.
#### `info`
`ethdo account info` provides information about the given account. Options include:
- `account`: the name of the account on which to obtain information (in format "wallet/account")
- `account`: the name of the account on which to obtain information (in format "wallet/account")
```sh
$ ethdo account info --account="Personal wallet/Operations"
@@ -194,8 +210,9 @@ Public key: 0x8e2f9e8cc29658ff37ecc30e95a0807579b224586c185d128cb7a7490784c1ad9b
#### `key`
`ethdo account key` provides the private key for an account. Options include:
- `account`: the name of the account on which to obtain information (in format "wallet/account")
- `passphrase`: the passphrase for the account
- `account`: the name of the account on which to obtain information (in format "wallet/account")
- `passphrase`: the passphrase for the account
```sh
$ ethdo account key --account=interop/00001 --passphrase=secret
@@ -205,7 +222,8 @@ $ ethdo account key --account=interop/00001 --passphrase=secret
#### `lock`
`ethdo account lock` manually locks an account on a remote signer. Locked accounts cannot carry out signing requests. Options include:
- `account`: the name of the account to lock (in format "wallet/account")
- `account`: the name of the account to lock (in format "wallet/account")
Note that this command only works with remote signers; it has no effect on local accounts.
@@ -216,8 +234,9 @@ $ ethdo account lock --account=Validators/123
#### `unlock`
`ethdo account unlock` manually unlocks an account on a remote signer. Unlocked accounts cannot carry out signing requests. Options include:
- `account`: the name of the account to unlock (in format "wallet/account")
- `passphrase`: the passphrase for the account
- `account`: the name of the account to unlock (in format "wallet/account")
- `passphrase`: the passphrase for the account
Note that this command only works with remote signers; it has no effect on local accounts.
@@ -232,10 +251,11 @@ Signature commands focus on generation and verification of data signatures.
#### `signature sign`
`ethdo signature sign` signs provided data. Options include:
- `data`: the data to sign, as a hex string
- `domain`: the domain in which to sign the data. This is a 32-byte hex string
- `account`: the account to sign the data (in format "wallet/account")
- `passphrase`: the passphrase for the account
- `data`: the data to sign, as a hex string
- `domain`: the domain in which to sign the data. This is a 32-byte hex string
- `account`: the account to sign the data (in format "wallet/account")
- `passphrase`: the passphrase for the account
```sh
$ ethdo signature sign --data="0x08140077a94642919041503caf5cc1c89c7744a2a08d43cec91df1795b23ecf2" --account="Personal wallet/Operations" --passphrase="my account secret"
@@ -245,10 +265,11 @@ $ ethdo signature sign --data="0x08140077a94642919041503caf5cc1c89c7744a2a08d43c
#### `signature verify`
`ethdo signature verify` verifies signed data. Options include:
- `data`: the data whose signature to verify, as a hex string
- `signature`: the signature to verify, as a hex string
- `account`: the account which signed the data (if available as an account, in format "wallet/account")
- `signer`: the public key of the account which signed the data (if not available as an account)
- `data`: the data whose signature to verify, as a hex string
- `signature`: the signature to verify, as a hex string
- `account`: the account which signed the data (if available as an account, in format "wallet/account")
- `signer`: the public key of the account which signed the data (if not available as an account)
```sh
$ ethdo signature verify --data="0x08140077a94642919041503caf5cc1c89c7744a2a08d43cec91df1795b23ecf2" --signature="0x87c83b31081744667406a11170c5585a11195621d0d3f796bd9006ac4cb5f61c10bf8c5b3014cd4f792b143a644cae100cb3155e8b00a961287bd9e7a5e18cb3b80930708bc9074d11ff47f1e8b9dd0b633e71bcea725fc3e550fdc259c3d130" --account="Personal wallet/Operations"
@@ -272,10 +293,11 @@ $ 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:
- `blockid`: the ID (slot, root, 'head') of the block to obtain
`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
$ ethdo block analyze --blockid=80
@@ -297,8 +319,9 @@ Value for block 80: 488.531
#### `info`
`ethdo block info` obtains information about a block in Ethereum 2. Options include:
- `blockid`: the ID (slot, root, 'head') of the block to obtain
`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
$ ethdo block info --blockid=80
@@ -335,13 +358,14 @@ 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`
`ethdo chain eth1votes` obtains information about the votes for the next Ethereum 1 block to be incorporated in to the chain for deposits. Options include:
- `epoch` show the votes at the end of the given epoch
- `json` provide JSON output
- `epoch` show the votes at the end of the given epoch
- `json` provide JSON output
```sh
$ ethdo chain eth1votes
@@ -355,7 +379,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
@@ -375,18 +399,32 @@ Slots per epoch: 32
#### `queues`
`ethdo chain queues` obtains the activation and exit queue lengths of an Ethereum chain from the node's point of view. Options include:
- `epoch` show the queue length at a given epoch
- `json` provide JSON output
- `epoch` show the queue length at a given epoch
- `json` provide JSON output
```sh
$ 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:
- `slot` show output in terms of slots rather than epochs
`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
$ ethdo chain status
@@ -410,10 +448,11 @@ Prior justified epoch distance: 4
#### `time`
`ethdo chain time` calculates the time period of Ethereum 2 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
`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
```sh
$ ethdo chain time --epoch=1234
@@ -432,10 +471,11 @@ Deposit commands focus on information about deposit data information in a JSON f
#### `verify`
`ethdo deposit verify` verifies one or more deposit data information in a JSON file generated by the `ethdo validator depositdata` command. Options include:
- `data`: either a path to the JSON file, the JSON itself, or a hex string representing a deposit transaction
- `withdrawalpubkey`: the public key of the withdrawal for the deposit. If no value is supplied then withdrawal credentials for deposits will not be checked
- `validatorpubkey`: the public key of the validator for the deposit. If no value is supplied then validator public keys will not be checked
- `depositvalue`: the value of the Ether being deposited. If no value is supplied then deposit values will not be checked.
- `data`: either a path to the JSON file, the JSON itself, or a hex string representing a deposit transaction
- `withdrawalpubkey`: the public key of the withdrawal for the deposit. If no value is supplied then withdrawal credentials for deposits will not be checked
- `validatorpubkey`: the public key of the validator for the deposit. If no value is supplied then validator public keys will not be checked
- `depositvalue`: the value of the Ether being deposited. If no value is supplied then deposit values will not be checked.
```sh
$ ethdo deposit verify --data=${HOME}/depositdata.json --withdrawalpubkey=0xad1868210a0cff7aff22633c003c503d4c199c8dcca13bba5b3232fc784d39d3855936e94ce184c3ce27bf15d4347695 --validatorpubkey=0xa951530887ae2494a8cc4f11cf186963b0051ac4f7942375585b9cf98324db1e532a67e521d0fcaab510edad1352394c --depositvalue=32Ether
@@ -448,8 +488,9 @@ Epoch commands focus on information about a beacon chain epoch.
#### `summary`
`ethdo epoch summary` provides a summary of the given epoch. Options include:
- `epoch`: the epoch for which to provide a summary; defaults to last complete epoch
- `json`: provide JSON output
- `epoch`: the epoch for which to provide a summary; defaults to last complete epoch
- `json`: provide JSON output
```sh
$ ethdo epoch summary
@@ -481,7 +522,8 @@ Exit commands focus on information about validator exits generated by the `ethdo
#### `verify`
`ethdo exit verify` verifies the validator exit information in a JSON file generated by the `ethdo validator exit` command. Options include:
- `signed-operation`: either a path to the JSON file or the JSON itself
- `signed-operation`: either a path to the JSON file or the JSON itself
```sh
$ ethdo exit verify --signed-operation=${HOME}/exit.json
@@ -489,11 +531,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 +546,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,12 +568,13 @@ Genesis timestamp: 1587020563
### `slot` commands
Slot commands focus on information about Ethereum 2 slots.
Slot commands focus on information about Ethereum consensus slots.
#### `slottime`
`ethdo slot time` provides information about the time of a slot. options include:
- `slot` the slot for which to provide the time
- `slot` the slot for which to provide the time
```sh
$ ethdo slot time --slot=5
@@ -545,9 +588,9 @@ Sync committee commands focus on information about sync committees.
#### `inclusion`
`ethdo synccommittee inclusion` provides information about the inclusion, or not, of a validator's sync committee messages. Options include:
- `validator`: the index, public key or account of the validator in format "wallet/account"
- `epoch` the specific epoch for which to print sync committee contributions. Defaults to the last complete epoch
- `validator`: a [validator specifier](https://github.com/wealdtech/ethdo#validator-specifier)
- `epoch` the specific epoch for which to print sync committee contributions. Defaults to the last complete epoch
```sh
$ ethdo synccommittee inclusion --index=274946 --epoch=91592
@@ -562,8 +605,9 @@ Per-slot result: ✓✓✓✓✓✓✓✓ ✓✓✕✓✓✓✓✓ ✓✓✓✓-
#### `members`
`ethdo synccommittee members` provides information about the members of a sync committee. Options include:
- `epoch` the specific epoch for which to provide sync committee members.
- `period` the period for which to provide sync committee members. Can be 'current' or 'next'; dfeaults to 'current'
- `epoch` the specific epoch for which to provide sync committee members.
- `period` the period for which to provide sync committee members. Can be 'current' or 'next'; dfeaults to 'current'
```sh
$ ethdo synccommittee members
@@ -572,12 +616,13 @@ $ 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`
`ethdo validator credentials get` provides information about the withdrawal credentials for the provided validator. Options include:
- `validator` the account, public key or index for which to obtain the withdrawal credentials
- `validator`: a [validator specifier](https://github.com/wealdtech/ethdo#validator-specifier)
```sh
$ ethdo validator credentials get --validator=Validators/1
@@ -593,35 +638,35 @@ $ 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:
- `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)
- `validatoraccount` specify the account to be used for the validator
- `depositvalue` specify the amount of the deposit
- `forkversion` specify the fork version for the deposit signature; this defaults to mainnet. Note that supplying an incorrect value could result in the loss of your deposit, so only supply this value if you are sure you know what you are doing. You can find the value for other chains by fetching the value supplied in "Genesis fork version" of the `ethdo chain info` command
- `raw` generate raw hex output that can be supplied as the data to an Ethereum 1 deposit transaction
`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)
- `validatoraccount` specify the account to be used for the validator
- `depositvalue` specify the amount of the deposit
- `forkversion` specify the fork version for the deposit signature; this defaults to mainnet. Note that supplying an incorrect value could result in the loss of your deposit, so only supply this value if you are sure you know what you are doing. You can find the value for other chains by fetching the value supplied in "Genesis fork version" of the `ethdo chain info` command
- `raw` generate raw hex output that can be supplied as the data to an Ethereum 1 deposit transaction
#### `exit`
`ethdo validator exit` sends a transaction to the chain to tell an active validator to exit the validation queue. Options include:
- `epoch` specify an epoch before which this exit is not valid
- `json` generate JSON output rather than sending a transaction immediately
- `exit` use JSON exit input created by the `--json` option rather than generate data from scratch
`ethdo validator exit` sends a transaction to the chain to tell an active validator to exit the validation queue. Full information about using this command can be found in the [specific documentation](./exitingvalidators.md).
```sh
$ ethdo validator exit --account=Validators/1 --passphrase="my validator secret"
$ ethdo validator exit --validator=Validators/1 --passphrase="my validator secret"
```
To send a transaction when the account is not accessible to ethdo accout you can use the validator's private key instead:
```sh
$ ethdo validator exit --key=0x01e748d098d3bcb477d636f19d510399ae18205fadf9814ee67052f88c1f88c0
$ ethdo validator exit --private-key=0x01e748d098d3bcb477d636f19d510399ae18205fadf9814ee67052f88c1f88c0
```
#### `info`
`ethdo validator info` provides information for a given validator.
`ethdo validator info` provides information for a given validator. Options include:
- `validator`: the validator for which to obtain information, as a [validator specifier](https://github.com/wealdtech/ethdo#validator-specifier)
```sh
$ ethdo validator info --validator=Validators/1
@@ -633,7 +678,7 @@ Effective balance: 3.1 Ether
Additional information is supplied when using `--verbose`
```sh
$ ethdo validator info --validator=Validators/1 --verbose
$ ethdo validator info --validator=0xb3bb6b7a8d809e59544472853d219499765bf01d14de1e0549bd6fc2a86627ac9033264c84cd503b6339e3334726562f --verbose
Epoch of data: 3398
Index: 26913
Public key: 0xb3bb6b7a8d809e59544472853d219499765bf01d14de1e0549bd6fc2a86627ac9033264c84cd503b6339e3334726562f
@@ -643,21 +688,13 @@ Effective balance: 3.1 Ether
Withdrawal credentials: 0x0033ef3cb10b36d0771ffe8a02bc5bfc7e64ea2f398ce77e25bb78989edbee36
```
If the validator is not an account then `--validator` option can be supplied with a validator index or public key.
```sh
$ ethdo validator info --validator=0x842dd66cfeaeff4397fc7c94f7350d2131ca0c4ad14ff727963be9a1edb4526604970df6010c3da6474a9820fa81642b
Status: Active
Balance: 3.201850307 Ether
Effective balance: 3.1 Ether
```
#### `keycheck`
`ethdo validator keycheck` checks if a given key matches a validator's withdrawal credentials. Options include:
- `withdrawal-credentials` the withdrawal credentials against which to match
- `privkey` the private key used to generat matching withdrawal credentials
- `mnemonic` the mnemonic used to generate matching withdrawal credentials
- `withdrawal-credentials` the withdrawal credentials against which to match
- `private-key` the private key used to generat matching withdrawal credentials
- `mnemonic` the mnemonic used to generate matching withdrawal credentials
```sh
$ ethdo validator keycheck --withdrawal-credentials=0x007e28dcf9029e8d92ca4b5d01c66c934e7f3110606f34ae3052cbf67bd3fc02 --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'
@@ -666,7 +703,9 @@ Withdrawal credentials confirmed at path m/12381/3600/10/0
#### `expectation`
`ethdo validator expectation` calculates the times between expected actions.
`ethdo validator expectation` calculates the times between expected actions. Options include:
- `validators` the number of active validators to use as the basis for calculations
```sh
$ ethdo validator expectation
@@ -676,13 +715,14 @@ 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`
`ethdo attester duties` provides information on the duties that a given validator has in a given epoch. Options include:
- `epoch` the epoch in which to obtain the duties (defaults to current epoch)
- `validator` the validator for which to fetch the duties, as an index, publi key or account in the format "wallet/account"
- `epoch` the epoch in which to obtain the duties (defaults to current epoch)
- `validator`: the validator for which to fetch the duties, as a [validator specifier](https://github.com/wealdtech/ethdo#validator-specifier)
```sh
$ ethdo attester duties --validator=Validators/0 --epoch=5
@@ -692,19 +732,32 @@ Validator attesting in slot 186 committee 3
#### `inclusion`
`ethdo attester inclusion` finds the block with wihch an attestation is included on the chain. Options include:
- `epoch` the epoch in which to obtain the inclusion information (defaults to previous epoch)
- `validator` the validator for which to fetch the duties, as an index, publi key or account in the format "wallet/account"
- `epoch` the epoch in which to obtain the inclusion information (defaults to previous epoch)
- `validator`: the validator for which to fetch the duties, as a [validator specifier](https://github.com/wealdtech/ethdo#validator-specifier)
```sh
$ 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 validator for which to fetch the withdrawal, as a [validator specifier](https://github.com/wealdtech/ethdo#validator-specifier)
- `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:
- `validators` use a specified number of validators rather than the current number of active validators
- `json` obtain detailed information in JSON format
- `validators` use a specified number of validators rather than the current number of active validators
- `json` obtain detailed information in JSON format
```sh
$ ethdo validator yield
@@ -713,19 +766,21 @@ Yield: 4.64%
#### `summary`
`ethdo validator summary` provides a summary of the given epoch for the given validators. Options include:
- `epoch`: the epoch for which to provide a summary; defaults to last complete epoch
- `validators`: the list of validators for which to provide a summary
- `json`: provide JSON output
- `epoch`: the epoch for which to provide a summary; defaults to last complete epoch
- `validators`: the list of validators for which to provide a summary, as [validator specifiers](https://github.com/wealdtech/ethdo#validator-specifier)
- `json`: provide JSON output
### `proposer` commands
Proposer commands focus on Ethereum 2 validators' actions as proposers.
Proposer commands focus on Ethereum consensus validators' actions as proposers.
#### `duties`
`ethdo proposer duties` provides information on the proposal duties for a given epoch. Options include:
- `epoch` the epoch in which to obtain the duties (defaults to current epoch)
- `json` obtain detailed information in JSON format
- `epoch` the epoch in which to obtain the duties (defaults to current epoch)
- `json` obtain detailed information in JSON format
```sh
$ ethdo proposer duties --epoch=5

3
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/wealdtech/ethdo
go 1.20
require (
github.com/attestantio/go-eth2-client v0.15.8
github.com/attestantio/go-eth2-client v0.16.0
github.com/ferranbt/fastssz v0.1.3
github.com/gofrs/uuid v4.4.0+incompatible
github.com/google/uuid v1.3.0
@@ -54,6 +54,7 @@ require (
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/holiman/uint256 v1.2.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/puddle v1.3.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect

27
go.sum
View File

@@ -48,8 +48,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/attestantio/go-eth2-client v0.15.8 h1:ndeqKacjT3vDD8yJVGe7CGmyrhVbQleBCYIPsULzGMM=
github.com/attestantio/go-eth2-client v0.15.8/go.mod h1:PLRKnILnr63V3yl2VagBqnhVRFBWc0V+JhQSsXQaSwQ=
github.com/attestantio/go-eth2-client v0.16.0 h1:PdO+Z9il00oDSkURsR8miT4VV/11Y/aBC6y3R3jQDus=
github.com/attestantio/go-eth2-client v0.16.0/go.mod h1:ES/aAi5Pog4l8ZCXRvAGnbLdSuoa0I9kZ6aet/aRC8g=
github.com/aws/aws-sdk-go v1.44.213 h1:WahquyWs7cQdz0vpDVWyWETEemgSoORx0PbWL9oz2WA=
github.com/aws/aws-sdk-go v1.44.213/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -70,7 +70,6 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -93,7 +92,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/ferranbt/fastssz v0.1.2/go.mod h1:X5UPrE2u1UJjxHA8X54u04SBwdAQjG2sFtWs39YxyWs=
github.com/ferranbt/fastssz v0.1.3 h1:ZI+z3JH05h4kgmFXdHuR1aWYsgrg7o+Fw7/NCzM16Mo=
github.com/ferranbt/fastssz v0.1.3/go.mod h1:0Y9TEd/9XuFlh7mskMPfXiI2Dkw4Ddg9EyXt1W7MRvE=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
@@ -159,9 +157,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -205,6 +201,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/herumi/bls-eth-go-binary v1.29.1 h1:XcNSHYTyNjEUVfWDCE2gtG5r95biTwd7MJUJF09LtSE=
github.com/herumi/bls-eth-go-binary v1.29.1/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U=
github.com/holiman/uint256 v1.2.2 h1:TXKcSGc2WaxPD2+bmzAsVthL4+pEN0YwXcL5qED83vk=
github.com/holiman/uint256 v1.2.2/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -226,8 +224,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.1.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -235,7 +231,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -261,7 +256,6 @@ github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -315,15 +309,12 @@ github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 h1:0tVE4
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7/go.mod h1:wmuf/mdK4VMD+jA9ThwcUKjg3a2XWM9cVfFYjDyY4j4=
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388 h1:4bD+ujqGfY4zoDUF3q9MhdmpPXzdp03DYUIlXeQ72kk=
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388/go.mod h1:VecIJZrewdAuhVckySLFt2wAAHRME934bSDurP8ftkc=
github.com/prysmaticlabs/gohashtree v0.0.1-alpha.0.20220714111606-acbb2962fb48/go.mod h1:4pWaT30XoEx1j8KNJf3TV+E3mQkaufn7mf+jRNb/Fuk=
github.com/r3labs/sse/v2 v2.7.4 h1:pvCMswPDlXd/ZUFx1dry0LbXJNHXwWPulLcUGYwClc0=
github.com/r3labs/sse/v2 v2.7.4/go.mod h1:hUrYMKfu9WquG9MyI0r6TKiNH+6Sw/QPKm2YbNbU5g8=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -406,7 +397,6 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
@@ -432,7 +422,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -468,7 +457,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -506,7 +494,6 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -536,7 +523,6 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -588,7 +574,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -600,7 +585,6 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -668,7 +652,6 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -800,11 +783,9 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading.
// Copyright © 2021 - 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -60,4 +60,6 @@ type Service interface {
AltairInitialSyncCommitteePeriod() uint64
// CapellaInitialEpoch provides the epoch at which the Capella hard fork takes place.
CapellaInitialEpoch() phase0.Epoch
// DenebInitialEpoch provides the epoch at which the Deneb hard fork takes place.
DenebInitialEpoch() phase0.Epoch
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2021 Weald Technology Trading.
// Copyright © 2021 - 2023 Weald Technology Trading.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -33,6 +33,7 @@ type Service struct {
altairForkEpoch phase0.Epoch
bellatrixForkEpoch phase0.Epoch
capellaForkEpoch phase0.Epoch
denebForkEpoch phase0.Epoch
}
// module-wide log.
@@ -107,6 +108,13 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
}
log.Trace().Uint64("epoch", uint64(capellaForkEpoch)).Msg("Obtained Capella fork epoch")
denebForkEpoch, err := fetchDenebForkEpoch(ctx, parameters.specProvider)
if err != nil {
// Set to far future epoch.
denebForkEpoch = 0xffffffffffffffff
}
log.Trace().Uint64("epoch", uint64(denebForkEpoch)).Msg("Obtained Deneb fork epoch")
s := &Service{
genesisTime: genesisTime,
slotDuration: slotDuration,
@@ -115,6 +123,7 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
altairForkEpoch: altairForkEpoch,
bellatrixForkEpoch: bellatrixForkEpoch,
capellaForkEpoch: capellaForkEpoch,
denebForkEpoch: denebForkEpoch,
}
return s, nil
@@ -302,3 +311,32 @@ func fetchCapellaForkEpoch(ctx context.Context,
return phase0.Epoch(epoch), nil
}
// DenebInitialEpoch provides the epoch at which the Deneb hard fork takes place.
func (s *Service) DenebInitialEpoch() phase0.Epoch {
return s.denebForkEpoch
}
func fetchDenebForkEpoch(ctx context.Context,
specProvider eth2client.SpecProvider,
) (
phase0.Epoch,
error,
) {
// Fetch the fork version.
spec, err := specProvider.Spec(ctx)
if err != nil {
return 0, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["DENEB_FORK_EPOCH"]
if !exists {
return 0, errors.New("deneb fork version not known by chain")
}
epoch, isEpoch := tmp.(uint64)
if !isEpoch {
//nolint:revive
return 0, errors.New("DENEB_FORK_EPOCH is not a uint64!")
}
return phase0.Epoch(epoch), nil
}

View File

@@ -40,13 +40,23 @@ var mnemonicWordLists = [][]string{
// 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.
// Handle situations where there may be a passphrase with the mnemonic.
mnemonicParts := strings.Split(mnemonic, " ")
mnemonicPassphrase := ""
if len(mnemonicParts) > 24 {
switch {
case len(mnemonicParts) == 13:
// Assume that passphrase is a single word here.
mnemonic = strings.Join(mnemonicParts[:12], " ")
mnemonicPassphrase = mnemonicParts[12]
case len(mnemonicParts) == 19:
// Assume that passphrase is a single word here.
mnemonic = strings.Join(mnemonicParts[:18], " ")
mnemonicPassphrase = mnemonicParts[18]
case len(mnemonicParts) > 24:
mnemonic = strings.Join(mnemonicParts[:24], " ")
mnemonicPassphrase = strings.Join(mnemonicParts[24:], " ")
}
// Normalise the input.
mnemonic = string(norm.NFKD.Bytes([]byte(mnemonic)))
mnemonicPassphrase = string(norm.NFKD.Bytes([]byte(mnemonicPassphrase)))

View File

@@ -42,10 +42,35 @@ func TestSeedFromMnemonic(t *testing.T) {
err: "mnemonic is invalid",
},
{
name: "Default",
name: "Twelve",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
seed: bytesStr("0x5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"),
},
{
name: "TwelvePlusPassphrase",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about passphrase",
seed: bytesStr("0x4865438d10636e1453b2d3c06444c669b80fb1ae77111f1f91b64278ed4d493465276d2e00f93be2a8e82c2f72555370a4bf31bcf1f9addaf0a31499a3baeeae"),
},
{
name: "Eighteen",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
seed: bytesStr("0x4975bb3d1faf5308c86a30893ee903a976296609db223fd717e227da5a813a34dc1428b71c84a787fc51f3b9f9dc28e9459f48c08bd9578e9d1b170f2d7ea506"),
},
{
name: "EighteenPlusPassphrase",
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent passphrase",
seed: bytesStr("0xbea1dd48440f3a8a7c02d0f7977fe03ba1dd409dda1ce971e80adc38f750c51d0959bd15c48cca2649cbcba8160d8a6c4026f2ee22dd387aa9b005041a5b8ea2"),
},
{
name: "TwentyFour",
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: "TwentyFourPlusPassphrase",
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 passphrase",
seed: bytesStr("0x3b9096d658962052e9e778a18e7fddb8f530cbf783f38b26cf3e89fff6bf385728028ea0e906d47c24f88b666d61a59bdb88a7fc11b9e302ae75482c9562c282"),
},
{
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",