mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-11 06:58:02 -05:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f1c6f09bd | ||
|
|
6118f9cab8 | ||
|
|
829dbd3bf2 | ||
|
|
f0ad10463e | ||
|
|
3d0dab0b95 | ||
|
|
5abfabc355 | ||
|
|
e84b600d5d | ||
|
|
e64a46f126 |
@@ -1,3 +1,10 @@
|
||||
1.5.9:
|
||||
- fix issue where wallet mnemonics were not normalised to NFKD
|
||||
- "block info" supports fetching the gensis block (--slot=0)
|
||||
- "attester inclusion" command finds the inclusion slot for a validator's attestation
|
||||
- "account info" with verbose option now displays participants for distributed accounts
|
||||
- fix issue where distributed account generation without a passphrase was not allowed
|
||||
|
||||
1.5.8:
|
||||
- allow raw deposit transactions to be supplied to "deposit verify"
|
||||
- move functionality of "account withdrawalcredentials" to be part of "account info"
|
||||
|
||||
@@ -61,7 +61,7 @@ In quiet mode this will return 0 if the account is created successfully, otherwi
|
||||
outputIf(debug, fmt.Sprintf("Distributed account has %d/%d threshold", viper.GetUint32("signing-threshold"), viper.GetUint32("participants")))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
account, err = distributedCreator.CreateDistributedAccount(ctx, accountName, viper.GetUint32("participants"), viper.GetUint32("signing-threshold"), []byte(getPassphrase()))
|
||||
account, err = distributedCreator.CreateDistributedAccount(ctx, accountName, viper.GetUint32("participants"), viper.GetUint32("signing-threshold"), []byte(getOptionalPassphrase()))
|
||||
} else {
|
||||
if viper.GetString("path") != "" {
|
||||
// Want a pathed account
|
||||
@@ -99,14 +99,17 @@ func init() {
|
||||
accountCmd.AddCommand(accountCreateCmd)
|
||||
accountFlags(accountCreateCmd)
|
||||
accountCreateCmd.Flags().Uint32("participants", 0, "Number of participants (for distributed accounts)")
|
||||
accountCreateCmd.Flags().Uint32("signing-threshold", 0, "Signing threshold (for distributed accounts)")
|
||||
accountCreateCmd.Flags().String("path", "", "path of account (for hierarchical deterministic accounts)")
|
||||
}
|
||||
|
||||
func accountCreateBindings() {
|
||||
if err := viper.BindPFlag("participants", accountCreateCmd.Flags().Lookup("participants")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
accountCreateCmd.Flags().Uint32("signing-threshold", 0, "Signing threshold (for distributed accounts)")
|
||||
if err := viper.BindPFlag("signing-threshold", accountCreateCmd.Flags().Lookup("signing-threshold")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
accountCreateCmd.Flags().String("path", "", "path of account (for hierarchical deterministic accounts)")
|
||||
if err := viper.BindPFlag("path", accountCreateCmd.Flags().Lookup("path")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -58,6 +58,13 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
|
||||
if distributedAccount, ok := account.(e2wtypes.DistributedAccount); ok {
|
||||
fmt.Printf("Composite public key: %#x\n", distributedAccount.CompositePublicKey().Marshal())
|
||||
fmt.Printf("Signing threshold: %d/%d\n", distributedAccount.SigningThreshold(), len(distributedAccount.Participants()))
|
||||
if verbose {
|
||||
fmt.Printf("Participants:\n")
|
||||
for k, v := range distributedAccount.Participants() {
|
||||
fmt.Printf(" %d: %s\n", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
withdrawalPubKey = distributedAccount.CompositePublicKey()
|
||||
}
|
||||
if verbose {
|
||||
|
||||
32
cmd/attester.go
Normal file
32
cmd/attester.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// attesterCmd represents the attester command
|
||||
var attesterCmd = &cobra.Command{
|
||||
Use: "attester",
|
||||
Short: "Obtain information about Ethereum 2 attesters",
|
||||
Long: "Obtain information about Ethereum 2 attesters",
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(attesterCmd)
|
||||
}
|
||||
|
||||
func attesterFlags(cmd *cobra.Command) {
|
||||
}
|
||||
156
cmd/attesterinclusion.go
Normal file
156
cmd/attesterinclusion.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/wealdtech/ethdo/grpc"
|
||||
"github.com/wealdtech/ethdo/util"
|
||||
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||
)
|
||||
|
||||
var attesterInclusionCmd = &cobra.Command{
|
||||
Use: "inclusion",
|
||||
Short: "Obtain information about attester inclusion",
|
||||
Long: `Obtain information about attester inclusion. For example:
|
||||
|
||||
ethdo attester inclusion --account=Validators/00001 --epoch=12345
|
||||
|
||||
In quiet mode this will return 0 if an attestation from the attester is found on the block fo the given epoch, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := connect()
|
||||
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain block")
|
||||
|
||||
// Obtain the epoch.
|
||||
epoch := viper.GetInt64("epoch")
|
||||
if epoch == -1 {
|
||||
outputIf(debug, "No epoch supplied; fetching current epoch")
|
||||
config, err := grpc.FetchChainConfig(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain beacon chain configuration")
|
||||
slotsPerEpoch := config["SlotsPerEpoch"].(uint64)
|
||||
secondsPerSlot := config["SecondsPerSlot"].(uint64)
|
||||
genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain beacon chain genesis")
|
||||
epoch = int64(time.Since(genesisTime).Seconds()) / int64(secondsPerSlot*slotsPerEpoch)
|
||||
}
|
||||
outputIf(debug, fmt.Sprintf("Epoch is %d", epoch))
|
||||
|
||||
// Obtain the validator.
|
||||
account, err := attesterInclusionAccount()
|
||||
errCheck(err, "Failed to obtain account")
|
||||
validatorIndex, err := grpc.FetchValidatorIndex(eth2GRPCConn, account)
|
||||
errCheck(err, "Failed to obtain validator")
|
||||
|
||||
// Find the attesting slot for the given epoch.
|
||||
committees, err := grpc.FetchValidatorCommittees(eth2GRPCConn, uint64(epoch))
|
||||
errCheck(err, "Failed to obtain validator committees")
|
||||
|
||||
slot := uint64(0)
|
||||
committeeIndex := uint64(0)
|
||||
validatorPositionInCommittee := uint64(0)
|
||||
found := false
|
||||
for searchSlot, committee := range committees {
|
||||
for searchCommitteeIndex, committeeValidatorIndices := range committee {
|
||||
for position, committeeValidatorIndex := range committeeValidatorIndices {
|
||||
if validatorIndex == committeeValidatorIndex {
|
||||
outputIf(verbose, fmt.Sprintf("Validator %d scheduled to attest at slot %d for epoch %d: entry %d in committee %d", validatorIndex, searchSlot, epoch, position, searchCommitteeIndex))
|
||||
slot = searchSlot
|
||||
committeeIndex = uint64(searchCommitteeIndex)
|
||||
validatorPositionInCommittee = uint64(position)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(found, "Failed to find attester duty for validator in the given epoch")
|
||||
|
||||
startSlot := slot + 1
|
||||
endSlot := startSlot + 32
|
||||
for curSlot := startSlot; curSlot < endSlot; curSlot++ {
|
||||
signedBlock, err := grpc.FetchBlock(eth2GRPCConn, curSlot)
|
||||
errCheck(err, "Failed to obtain block")
|
||||
if signedBlock == nil {
|
||||
outputIf(debug, fmt.Sprintf("No block at slot %d", curSlot))
|
||||
continue
|
||||
}
|
||||
outputIf(debug, fmt.Sprintf("Fetched block %d", curSlot))
|
||||
for i, attestation := range signedBlock.Block.Body.Attestations {
|
||||
outputIf(debug, fmt.Sprintf("Attestation %d is for slot %d and committee %d", i, attestation.Data.Slot, attestation.Data.CommitteeIndex))
|
||||
if attestation.Data.Slot == slot &&
|
||||
attestation.Data.CommitteeIndex == committeeIndex &&
|
||||
attestation.AggregationBits.BitAt(validatorPositionInCommittee) {
|
||||
if verbose {
|
||||
fmt.Printf("Attestation included in block %d, attestation %d (inclusion delay %d)\n", curSlot, i, curSlot-slot)
|
||||
} else if !quiet {
|
||||
fmt.Printf("Attestation included in block %d (inclusion delay %d)\n", curSlot, curSlot-slot)
|
||||
}
|
||||
os.Exit(_exitSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
outputIf(verbose, "Attestation not included on the chain")
|
||||
os.Exit(_exitFailure)
|
||||
},
|
||||
}
|
||||
|
||||
// attesterInclusionAccount obtains the account for the attester inclusion command.
|
||||
func attesterInclusionAccount() (e2wtypes.Account, error) {
|
||||
var account e2wtypes.Account
|
||||
var err error
|
||||
if viper.GetString("account") != "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
_, account, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to obtain account")
|
||||
}
|
||||
} else {
|
||||
pubKey := viper.GetString("pubkey")
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubKey, "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", pubKey))
|
||||
}
|
||||
account, err = util.NewScratchAccount(nil, pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", pubKey))
|
||||
}
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
attesterCmd.AddCommand(attesterInclusionCmd)
|
||||
attesterFlags(attesterInclusionCmd)
|
||||
attesterInclusionCmd.Flags().Int64("epoch", -1, "the current epoch")
|
||||
attesterInclusionCmd.Flags().String("pubkey", "", "the public key of the attester")
|
||||
}
|
||||
|
||||
func attesterInclusionBindings() {
|
||||
if err := viper.BindPFlag("epoch", attesterInclusionCmd.Flags().Lookup("epoch")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("pubkey", attesterInclusionCmd.Flags().Lookup("pubkey")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,6 @@ In quiet mode this will return 0 if the block information is present and not ski
|
||||
genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain beacon chain genesis")
|
||||
|
||||
assert(blockInfoStream || blockInfoSlot != 0, "--slot or --stream is required")
|
||||
assert(!blockInfoStream || blockInfoSlot == -1, "--slot and --stream are not supported together")
|
||||
|
||||
var slot uint64
|
||||
@@ -64,8 +63,6 @@ In quiet mode this will return 0 if the block information is present and not ski
|
||||
} else {
|
||||
slot = uint64(blockInfoSlot)
|
||||
}
|
||||
assert(slot > 0, "slot must be greater than 0")
|
||||
|
||||
signedBlock, err := grpc.FetchBlock(eth2GRPCConn, slot)
|
||||
errCheck(err, "Failed to obtain block")
|
||||
if signedBlock == nil {
|
||||
|
||||
@@ -129,7 +129,10 @@ func init() {
|
||||
exitFlags(exitVerifyCmd)
|
||||
exitVerifyCmd.Flags().String("data", "", "JSON data, or path to JSON data")
|
||||
exitVerifyCmd.Flags().StringVar(&exitVerifyPubKey, "pubkey", "", "Public key for which to verify exit")
|
||||
if err := viper.BindPFlag("exit.data", exitVerifyCmd.Flags().Lookup("data")); err != nil {
|
||||
}
|
||||
|
||||
func exitVerifyBindings() {
|
||||
if err := viper.BindPFlag("data", exitVerifyCmd.Flags().Lookup("data")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,3 +37,13 @@ func getPassphrase() string {
|
||||
assert(len(passphrases) == 1, "multiple passphrases supplied; cannot continue")
|
||||
return passphrases[0]
|
||||
}
|
||||
|
||||
// getOptionalPassphrase fetches the passphrase if supplied by the user.
|
||||
func getOptionalPassphrase() string {
|
||||
passphrases := getPassphrases()
|
||||
if len(passphrases) == 0 {
|
||||
return ""
|
||||
}
|
||||
assert(len(passphrases) == 1, "multiple passphrases supplied; cannot continue")
|
||||
return passphrases[0]
|
||||
}
|
||||
|
||||
13
cmd/root.go
13
cmd/root.go
@@ -72,11 +72,22 @@ func persistentPreRun(cmd *cobra.Command, args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
// We bind viper here so that we bind to the correct command
|
||||
// We bind viper here so that we bind to the correct command.
|
||||
quiet = viper.GetBool("quiet")
|
||||
verbose = viper.GetBool("verbose")
|
||||
debug = viper.GetBool("debug")
|
||||
rootStore = viper.GetString("store")
|
||||
// Command-specific bindings.
|
||||
switch fmt.Sprintf("%s/%s", cmd.Parent().Name(), cmd.Name()) {
|
||||
case "account/create":
|
||||
accountCreateBindings()
|
||||
case "attester/inclusion":
|
||||
attesterInclusionBindings()
|
||||
case "exit/verify":
|
||||
exitVerifyBindings()
|
||||
case "wallet/create":
|
||||
walletCreateBindings()
|
||||
}
|
||||
|
||||
if quiet && verbose {
|
||||
fmt.Println("Cannot supply both quiet and verbose flags")
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var ReleaseVersion = "local build from v1.5.8"
|
||||
var ReleaseVersion = "local build from v1.5.9"
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
|
||||
hd "github.com/wealdtech/go-eth2-wallet-hd/v2"
|
||||
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
var walletCreateCmd = &cobra.Command{
|
||||
@@ -106,6 +107,9 @@ func walletCreateHD(ctx context.Context, name string, passphrase string, mnemoni
|
||||
mnemonicPassphrase = strings.Join(mnemonicParts[24:], " ")
|
||||
}
|
||||
}
|
||||
// Normalise the input.
|
||||
mnemonic = string(norm.NFKD.Bytes([]byte(mnemonic)))
|
||||
mnemonicPassphrase = string(norm.NFKD.Bytes([]byte(mnemonicPassphrase)))
|
||||
|
||||
// Ensure the mnemonic is valid
|
||||
if !bip39.IsMnemonicValid(mnemonic) {
|
||||
@@ -135,10 +139,13 @@ func init() {
|
||||
walletCmd.AddCommand(walletCreateCmd)
|
||||
walletFlags(walletCreateCmd)
|
||||
walletCreateCmd.Flags().String("type", "non-deterministic", "Type of wallet to create (non-deterministic or hierarchical deterministic)")
|
||||
walletCreateCmd.Flags().String("mnemonic", "", "The 24-word mnemonic for a hierarchical deterministic wallet")
|
||||
}
|
||||
|
||||
func walletCreateBindings() {
|
||||
if err := viper.BindPFlag("type", walletCreateCmd.Flags().Lookup("type")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
walletCreateCmd.Flags().String("mnemonic", "", "The 24-word mnemonic for a hierarchical deterministic wallet")
|
||||
if err := viper.BindPFlag("mnemonic", walletCreateCmd.Flags().Lookup("mnemonic")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@ ethdo wallet create --wallet="Launchpad" --type=hd --walletpassphrase=walletsecr
|
||||
|
||||
Launchpad accounts are identified by their path. The path can be seen in the filename of the keystore, for example the filename `keystore-m_12381_3600_1_0_0-1596891358.json` relates to a path of `m/12381/3600/1/0/0`. It is also present directly in the keystore under the `path` key.
|
||||
|
||||
To create an account corresponding to this key you would use the command:
|
||||
To create an account corresponding to this key with the account name "Account 1" you would use the command:
|
||||
|
||||
```sh
|
||||
ethdo account create --wallet="Launchpad" --walletpassphrase=walletsecret --passphrase=secret --path=m/12381/3600/1/0/0
|
||||
ethdo account create --account="Launchpad/Account 1" --walletpassphrase=walletsecret --passphrase=secret --path=m/12381/3600/1/0/0
|
||||
```
|
||||
|
||||
@@ -366,6 +366,7 @@ Current slot: 178
|
||||
Current epoch: 5
|
||||
Genesis timestamp: 1587020563
|
||||
```
|
||||
|
||||
### `validator` commands
|
||||
|
||||
Validator commands focus on interaction with Ethereum 2 validators.
|
||||
@@ -431,6 +432,22 @@ Balance: 3.201850307 Ether
|
||||
Effective balance: 3.1 Ether
|
||||
```
|
||||
|
||||
### `attester` commands
|
||||
|
||||
Attester commands focus on Ethereum 2 validators' actions as attesters.
|
||||
|
||||
#### `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 current epoch)
|
||||
- `account` the account for which to fetch the inclusion information
|
||||
- `pubkey` the public key for which to fetch the inclusion information
|
||||
|
||||
```sh
|
||||
$ ethdo attester inclusion --account=Validators/1 --epoch=6484
|
||||
Attestation included in block 207492 (inclusion delay 1)
|
||||
```
|
||||
|
||||
## Maintainers
|
||||
|
||||
Jim McDonald: [@mcdee](https://github.com/mcdee).
|
||||
|
||||
2
go.mod
2
go.mod
@@ -44,7 +44,7 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
|
||||
golang.org/x/sys v0.0.0-20200821140526-fda516888d29 // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
golang.org/x/text v0.3.3
|
||||
google.golang.org/genproto v0.0.0-20200815001618-f69a88009b70 // indirect
|
||||
google.golang.org/grpc v1.31.0
|
||||
gopkg.in/ini.v1 v1.60.1 // indirect
|
||||
|
||||
@@ -287,8 +287,11 @@ func FetchBlock(conn *grpc.ClientConn, slot uint64) (*ethpb.SignedBeaconBlock, e
|
||||
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
|
||||
defer cancel()
|
||||
|
||||
req := ðpb.ListBlocksRequest{
|
||||
QueryFilter: ðpb.ListBlocksRequest_Slot{Slot: slot},
|
||||
req := ðpb.ListBlocksRequest{}
|
||||
if slot == 0 {
|
||||
req.QueryFilter = ðpb.ListBlocksRequest_Genesis{Genesis: true}
|
||||
} else {
|
||||
req.QueryFilter = ðpb.ListBlocksRequest_Slot{Slot: slot}
|
||||
}
|
||||
resp, err := beaconClient.ListBlocks(ctx, req)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user