Compare commits

...

12 Commits

Author SHA1 Message Date
Jim McDonald
48d63398d4 Bump version 2020-08-08 14:25:35 +01:00
Jim McDonald
4e1f47e187 Add ability to create HD accounts with a specific path 2020-08-08 14:25:02 +01:00
Jim McDonald
85c7c7fc55 Bump version 2020-08-02 21:54:43 +01:00
Jim McDonald
05406e8d81 Allow fetching wallet from account variable as well as wallet 2020-08-02 17:02:45 +01:00
Jim McDonald
70d451cea5 Bump version 2020-08-01 16:05:02 +01:00
Jim McDonald
c270d7a2f7 Fix bad path spec for validator deposits 2020-08-01 16:04:38 +01:00
Jim McDonald
115d037948 Add 'exit verify' command 2020-07-31 16:31:06 +01:00
Jim McDonald
b34a633e53 Bump version 2020-07-30 12:33:10 +01:00
Jim McDonald
d1b989a711 Add ability to see the private key for a dynamic account. 2020-07-30 12:32:00 +01:00
Jim McDonald
8078359eab Show account info for dynamically generated HD accounts 2020-07-28 20:16:41 +01:00
Jim McDonald
7de8dc7424 Show account info for dynamically generated HD accounts 2020-07-28 20:14:43 +01:00
Jim McDonald
987dbd26c6 Provide deposit info prior to chain-based info 2020-07-24 13:56:53 +01:00
27 changed files with 498 additions and 250 deletions

View File

@@ -70,7 +70,7 @@ jobs:
mv ethdo-linux-arm64 ethdo
tar zcf ethdo-${RELEASE_VERSION}-linux-arm64.tar.gz ethdo
- name: Create draft release
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
@@ -78,7 +78,7 @@ jobs:
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ env.RELEASE_VERSION }}
draft: true
draft: false
prerelease: false
- name: Upload windows zip file

View File

@@ -17,6 +17,7 @@ import (
"context"
"fmt"
"os"
"regexp"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -33,9 +34,12 @@ var accountCreateCmd = &cobra.Command{
In quiet mode this will return 0 if the account is created successfully, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
wallet, err := walletFromInput(ctx)
errCheck(err, "Failed to access wallet")
outputIf(debug, fmt.Sprintf("Opened wallet %q of type %s", wallet.Name(), wallet.Type()))
if wallet.Type() == "hierarchical deterministic" {
@@ -43,8 +47,6 @@ In quiet mode this will return 0 if the account is created successfully, otherwi
}
locker, isLocker := wallet.(e2wtypes.WalletLocker)
if isLocker {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
errCheck(locker.Unlock(ctx, []byte(getWalletPassphrase())), "Failed to unlock wallet")
}
@@ -61,12 +63,25 @@ In quiet mode this will return 0 if the account is created successfully, otherwi
defer cancel()
account, err = distributedCreator.CreateDistributedAccount(ctx, accountName, viper.GetUint32("participants"), viper.GetUint32("signing-threshold"), []byte(getPassphrase()))
} else {
// Want a standard account.
creator, isCreator := wallet.(e2wtypes.WalletAccountCreator)
assert(isCreator, "Wallet does not support account creation")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err = creator.CreateAccount(ctx, accountName, []byte(getPassphrase()))
if viper.GetString("path") != "" {
// Want a pathed account
creator, isCreator := wallet.(e2wtypes.WalletPathedAccountCreator)
assert(isCreator, "Wallet does not support account creation with an explicit path")
var match bool
match, err = regexp.Match("^m/[0-9]+/[0-9]+(/[0-9+])+", []byte(viper.GetString("path")))
errCheck(err, "Unable to match path to regular expression")
assert(match, "Path does not match expected format m/...")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err = creator.CreatePathedAccount(ctx, viper.GetString("path"), accountName, []byte(getPassphrase()))
} else {
// Want a standard account.
creator, isCreator := wallet.(e2wtypes.WalletAccountCreator)
assert(isCreator, "Wallet does not support account creation")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err = creator.CreateAccount(ctx, accountName, []byte(getPassphrase()))
}
}
errCheck(err, "Failed to create account")
@@ -91,4 +106,8 @@ func init() {
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)
}
}

View File

@@ -47,13 +47,13 @@ In quiet mode this will return 0 if the account is imported successfully, otherw
key, err := bytesutil.FromHexString(accountImportKey)
errCheck(err, "Invalid key")
w, err := walletFromPath(viper.GetString("account"))
w, err := walletFromPath(ctx, viper.GetString("account"))
errCheck(err, "Failed to access wallet")
_, ok := w.(e2wtypes.WalletAccountImporter)
assert(ok, fmt.Sprintf("wallets of type %q do not allow importing accounts", w.Type()))
_, err = accountFromPath(ctx, viper.GetString("account"))
_, _, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
assert(err != nil, "Account already exists")
locker, isLocker := w.(e2wtypes.WalletLocker)

View File

@@ -22,7 +22,6 @@ import (
"github.com/spf13/viper"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -35,19 +34,11 @@ var accountInfoCmd = &cobra.Command{
In quiet mode this will return 0 if the account exists, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet cannot obtain accounts by name")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err := accountByNameProvider.AccountByName(ctx, accountName)
assert(viper.GetString("account") != "", "--account is required")
wallet, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
// Disallow wildcards (for now)

View File

@@ -33,25 +33,28 @@ var accountKeyCmd = &cobra.Command{
In quiet mode this will return 0 if the key can be obtained, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(!remote, "account keys not available with remote wallets")
assert(viper.GetString("account") != "", "--account is required")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err := accountFromPath(ctx, viper.GetString("account"))
errCheck(err, "Failed to access account")
assert(!remote, "account keys not available with remote wallets")
assert(viper.GetString("account") != "", "--account is required")
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
privateKeyProvider, isPrivateKeyProvider := account.(e2wtypes.AccountPrivateKeyProvider)
assert(isPrivateKeyProvider, fmt.Sprintf("account %q does not provide its private key", viper.GetString("account")))
if locker, isLocker := account.(e2wtypes.AccountLocker); isLocker {
unlocked := false
for _, passphrase := range getPassphrases() {
err = locker.Unlock(ctx, []byte(passphrase))
if err == nil {
unlocked = true
break
unlocked, err := locker.IsUnlocked(ctx)
errCheck(err, "Failed to find out if account is locked")
if !unlocked {
for _, passphrase := range getPassphrases() {
err = locker.Unlock(ctx, []byte(passphrase))
if err == nil {
unlocked = true
break
}
}
}
assert(unlocked, "Failed to unlock account to obtain private key")

View File

@@ -18,7 +18,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -31,27 +30,18 @@ var accountLockCmd = &cobra.Command{
In quiet mode this will return 0 if the account is locked, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet cannot obtain accounts by name")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err := accountByNameProvider.AccountByName(ctx, accountName)
assert(viper.GetString("account") != "", "--account is required")
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
locker, isLocker := account.(e2wtypes.AccountLocker)
assert(isLocker, "Account does not support locking")
ctx, cancel = context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
err = locker.Lock(ctx)
cancel()
errCheck(err, "Failed to lock account")
},
}

View File

@@ -19,7 +19,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -32,19 +31,12 @@ var accountUnlockCmd = &cobra.Command{
In quiet mode this will return 0 if the account is unlocked, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet cannot obtain accounts by name")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err := accountByNameProvider.AccountByName(ctx, accountName)
assert(viper.GetString("account") != "", "--account is required")
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
locker, isLocker := account.(e2wtypes.AccountLocker)

View File

@@ -23,8 +23,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
util "github.com/wealdtech/go-eth2-util"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
var accountWithdrawalCredentialsCmd = &cobra.Command{
@@ -47,15 +45,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
pubKey, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("pubkey"), "0x"))
errCheck(err, "Failed to decode supplied public key")
} else {
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet cannot obtain accounts by name")
account, err := accountByNameProvider.AccountByName(ctx, accountName)
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
key, err := bestPublicKey(account)

View File

@@ -60,18 +60,17 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
deposits, err := depositDataFromJSON(depositVerifyData)
errCheck(err, "Failed to fetch deposit data")
withdrawalCredentials := ""
var withdrawalCredentials []byte
if depositVerifyWithdrawalPubKey != "" {
withdrawalPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(depositVerifyWithdrawalPubKey, "0x"))
errCheck(err, "Invalid withdrawal public key")
assert(len(withdrawalPubKeyBytes) == 48, "Public key should be 48 bytes")
withdrawalPubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes)
errCheck(err, "Value supplied with --withdrawalpubkey is not a valid public key")
withdrawalBytes := util.SHA256(withdrawalPubKey.Marshal())
withdrawalBytes[0] = 0 // BLS_WITHDRAWAL_PREFIX
withdrawalCredentials = fmt.Sprintf("%x", withdrawalBytes)
withdrawalCredentials = util.SHA256(withdrawalPubKey.Marshal())
withdrawalCredentials[0] = 0 // BLS_WITHDRAWAL_PREFIX
}
outputIf(debug, fmt.Sprintf("Withdrawal credentials are %s", withdrawalCredentials))
outputIf(debug, fmt.Sprintf("Withdrawal credentials are %#x", withdrawalCredentials))
depositValue := uint64(0)
if depositVerifyDepositValue != "" {
@@ -81,7 +80,7 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
assert(depositValue >= 1000000000, "deposit value must be at least 1 Ether") // MIN_DEPOSIT_AMOUNT
}
validatorPubKeys := make(map[string]bool)
validatorPubKeys := make(map[[48]byte]bool)
if depositVerifyValidatorPubKey != "" {
validatorPubKeys, err = validatorPubKeysFromInput(depositVerifyValidatorPubKey)
errCheck(err, "Failed to obtain validator public key(s))")
@@ -89,8 +88,10 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
failures := false
for i, deposit := range deposits {
if withdrawalCredentials != "" {
if deposit.WithdrawalCredentials != withdrawalCredentials {
if withdrawalCredentials != nil {
depositWithdrawalCredentials, err := hex.DecodeString(strings.TrimPrefix(deposit.WithdrawalCredentials, "0x"))
errCheck(err, fmt.Sprintf("Invalid withdrawal public key for deposit %d", i))
if !bytes.Equal(depositWithdrawalCredentials, withdrawalCredentials) {
outputIf(!quiet, fmt.Sprintf("Invalid withdrawal credentials for deposit %d", i))
failures = true
}
@@ -102,7 +103,11 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
}
}
if len(validatorPubKeys) != 0 {
if _, exists := validatorPubKeys[deposit.PublicKey]; !exists {
depositValidatorPubKey, err := hex.DecodeString(strings.TrimPrefix(deposit.PublicKey, "0x"))
errCheck(err, fmt.Sprintf("Invalid validator public key for deposit %d", i))
var key [48]byte
copy(key[:], depositValidatorPubKey)
if _, exists := validatorPubKeys[key]; !exists {
outputIf(!quiet, fmt.Sprintf("Unknown validator public key for deposit %d", i))
failures = true
}
@@ -117,8 +122,8 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
},
}
func validatorPubKeysFromInput(input string) (map[string]bool, error) {
pubKeys := make(map[string]bool)
func validatorPubKeysFromInput(input string) (map[[48]byte]bool, error) {
pubKeys := make(map[[48]byte]bool)
var err error
var data []byte
// Input could be a public key or a path to public keys.
@@ -135,7 +140,9 @@ func validatorPubKeysFromInput(input string) (map[string]bool, error) {
if err != nil {
return nil, errors.Wrap(err, "invalid public key")
}
pubKeys[fmt.Sprintf("%x", pubKey.Marshal())] = true
var key [48]byte
copy(key[:], pubKey.Marshal())
pubKeys[key] = true
} else {
// Assume it's a path to a file of public keys.
data, err = ioutil.ReadFile(input)
@@ -161,7 +168,9 @@ func validatorPubKeysFromInput(input string) (map[string]bool, error) {
if err != nil {
return nil, errors.Wrap(err, "invalid public key")
}
pubKeys[fmt.Sprintf("%x", pubKey.Marshal())] = true
var key [48]byte
copy(key[:], pubKey.Marshal())
pubKeys[key] = true
}
}

32
cmd/exit.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright © 2019 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"
)
// exitCmd represents the exit command
var exitCmd = &cobra.Command{
Use: "exit",
Short: "Manage Ethereum 2 voluntary exits",
Long: `Manage Ethereum 2 voluntary exits.`,
}
func init() {
RootCmd.AddCommand(exitCmd)
}
func exitFlags(cmd *cobra.Command) {
}

137
cmd/exitverify.go Normal file
View File

@@ -0,0 +1,137 @@
// 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"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/grpc"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
var exitVerifyPubKey string
var exitVerifyCmd = &cobra.Command{
Use: "verify",
Short: "Verify deposit data matches requirements",
Long: `Verify deposit data matches requirements. For example:
ethdo deposit verify --data=depositdata.json --withdrawalaccount=primary/current --value="32 Ether"
The information generated can be passed to ethereal to create a deposit from the Ethereum 1 chain.
In quiet mode this will return 0 if the the data can be generated correctly, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
assert(viper.GetString("account") != "" || exitVerifyPubKey != "", "account or public key is required")
account, err := exitVerifyAccount(ctx)
errCheck(err, "Failed to obtain account")
assert(viper.GetString("exit.data") != "", "exit data is required")
data, err := obtainExitData(viper.GetString("exit.Data"))
errCheck(err, "Failed to obtain exit data")
// Confirm signature is good.
err = connect()
errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node")
genesisValidatorsRoot, err := grpc.FetchGenesisValidatorsRoot(eth2GRPCConn)
outputIf(debug, fmt.Sprintf("Genesis validators root is %x", genesisValidatorsRoot))
errCheck(err, "Failed to obtain genesis validators root")
domain := e2types.Domain(e2types.DomainVoluntaryExit, data.ForkVersion, genesisValidatorsRoot)
exit := &ethpb.VoluntaryExit{
Epoch: data.Epoch,
ValidatorIndex: data.ValidatorIndex,
}
sig, err := e2types.BLSSignatureFromBytes(data.Signature)
errCheck(err, "Invalid signature")
verified, err := verifyStruct(account, exit, domain, sig)
errCheck(err, "Failed to verify voluntary exit")
assert(verified, "Voluntary exit failed to verify")
// TODO confirm fork version is valid (once we have a way of obtaining the current fork version).
outputIf(verbose, "Verified")
os.Exit(_exitSuccess)
},
}
// obtainExitData obtains exit data from an input, could be JSON itself or a path to JSON.
func obtainExitData(input string) (*validatorExitData, error) {
var err error
var data []byte
// Input could be JSON or a path to JSON
if strings.HasPrefix(input, "{") {
// Looks like JSON
data = []byte(input)
} else {
// Assume it's a path to JSON
data, err = ioutil.ReadFile(input)
if err != nil {
return nil, errors.Wrap(err, "failed to find deposit data file")
}
}
exitData := &validatorExitData{}
err = json.Unmarshal([]byte(data), exitData)
if err != nil {
return nil, errors.Wrap(err, "data is not valid JSON")
}
return exitData, nil
}
// exitVerifyAccount obtains the account for the exitVerify command.
func exitVerifyAccount(ctx context.Context) (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
_, account, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(exitVerifyPubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", exitVerifyPubKey))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", exitVerifyPubKey))
}
}
return account, nil
}
func init() {
exitCmd.AddCommand(exitVerifyCmd)
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 {
panic(err)
}
}

View File

@@ -233,12 +233,36 @@ func outputIf(condition bool, msg string) {
}
}
// walletFromInput obtains a wallet given the information in the viper variable
// "account", or if not present the viper variable "wallet".
func walletFromInput(ctx context.Context) (e2wtypes.Wallet, error) {
if viper.GetString("account") != "" {
return walletFromPath(ctx, viper.GetString("account"))
}
return walletFromPath(ctx, viper.GetString("wallet"))
}
// walletFromPath obtains a wallet given a path specification.
func walletFromPath(path string) (e2wtypes.Wallet, error) {
func walletFromPath(ctx context.Context, path string) (e2wtypes.Wallet, error) {
walletName, _, err := e2wallet.WalletAndAccountNames(path)
if err != nil {
return nil, err
}
if viper.GetString("remote") != "" {
assert(viper.GetString("client-cert") != "", "remote connections require client-cert")
assert(viper.GetString("client-key") != "", "remote connections require client-key")
credentials, err := dirk.ComposeCredentials(ctx, viper.GetString("client-cert"), viper.GetString("client-key"), viper.GetString("server-ca-cert"))
if err != nil {
return nil, errors.Wrap(err, "failed to build dirk credentials")
}
endpoints, err := remotesToEndpoints([]string{viper.GetString("remote")})
if err != nil {
return nil, errors.Wrap(err, "failed to parse remote servers")
}
return dirk.OpenWallet(ctx, walletName, credentials, endpoints)
}
wallet, err := e2wallet.OpenWallet(walletName)
if err != nil {
if strings.Contains(err.Error(), "failed to decrypt wallet") {
@@ -249,18 +273,23 @@ func walletFromPath(path string) (e2wtypes.Wallet, error) {
return wallet, nil
}
// accountFromPath obtains an account given a path specification.
func accountFromPath(ctx context.Context, path string) (e2wtypes.Account, error) {
wallet, err := walletFromPath(path)
// walletAndAccountFromInput obtains the wallet and account given the information in the viper variable "account".
func walletAndAccountFromInput(ctx context.Context) (e2wtypes.Wallet, e2wtypes.Account, error) {
return walletAndAccountFromPath(ctx, viper.GetString("account"))
}
// walletAndAccountFromPath obtains the wallet and account given a path specification.
func walletAndAccountFromPath(ctx context.Context, path string) (e2wtypes.Wallet, e2wtypes.Account, error) {
wallet, err := walletFromPath(ctx, path)
if err != nil {
return nil, err
return nil, nil, errors.Wrap(err, "faild to open wallet for account")
}
_, accountName, err := e2wallet.WalletAndAccountNames(path)
if err != nil {
return nil, err
return nil, nil, errors.Wrap(err, "failed to obtain accout name")
}
if accountName == "" {
return nil, errors.New("no account name")
return nil, nil, errors.New("no account name")
}
if wallet.Type() == "hierarchical deterministic" && strings.HasPrefix(accountName, "m/") {
@@ -270,7 +299,7 @@ func accountFromPath(ctx context.Context, path string) (e2wtypes.Account, error)
if isLocker {
err = locker.Unlock(ctx, []byte(viper.GetString("wallet-passphrase")))
if err != nil {
return nil, errors.New("invalid wallet passphrase")
return nil, nil, errors.New("failed to unlock wallet")
}
defer relockAccount(locker)
}
@@ -278,15 +307,26 @@ func accountFromPath(ctx context.Context, path string) (e2wtypes.Account, error)
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
if !isAccountByNameProvider {
return nil, errors.New("wallet cannot obtain accounts by name")
return nil, nil, errors.New("wallet cannot obtain accounts by name")
}
return accountByNameProvider.AccountByName(ctx, accountName)
account, err := accountByNameProvider.AccountByName(ctx, accountName)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to obtain account")
}
return wallet, account, nil
}
// accountsFromPath obtains 0 or more accounts given a path specification.
func accountsFromPath(ctx context.Context, wallet e2wtypes.Wallet, accountSpec string) ([]e2wtypes.Account, error) {
accounts := make([]e2wtypes.Account, 0)
// walletAndAccountsFromPath obtains the wallet and matching accounts given a path specification.
func walletAndAccountsFromPath(ctx context.Context, path string) (e2wtypes.Wallet, []e2wtypes.Account, error) {
wallet, err := walletFromPath(ctx, path)
if err != nil {
return nil, nil, errors.Wrap(err, "faild to open wallet for account")
}
_, accountSpec, err := e2wallet.WalletAndAccountNames(path)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to obtain account specification")
}
if accountSpec == "" {
accountSpec = "^.*$"
} else {
@@ -294,6 +334,7 @@ func accountsFromPath(ctx context.Context, wallet e2wtypes.Wallet, accountSpec s
}
re := regexp.MustCompile(accountSpec)
accounts := make([]e2wtypes.Account, 0)
for account := range wallet.Accounts(ctx) {
if re.Match([]byte(account.Name())) {
accounts = append(accounts, account)
@@ -305,7 +346,7 @@ func accountsFromPath(ctx context.Context, wallet e2wtypes.Wallet, accountSpec s
return accounts[i].Name() < accounts[j].Name()
})
return accounts, nil
return wallet, accounts, nil
}
// connect connects to an Ethereum 2 endpoint.
@@ -369,46 +410,6 @@ func remotesToEndpoints(remotes []string) ([]*dirk.Endpoint, error) {
return endpoints, nil
}
// Oepn a wallet, local or remote.
func openWallet() (e2wtypes.Wallet, error) {
var err error
// Obtain the name of the wallet.
walletName := viper.GetString("wallet")
if walletName == "" {
walletName, _, err = e2wallet.WalletAndAccountNames(viper.GetString("account"))
}
if err != nil {
return nil, errors.Wrap(err, "failed to obtain wallet name")
}
if walletName == "" {
return nil, errors.New("no wallet name provided")
}
return openNamedWallet(walletName)
}
// Open a named wallet, local or remote.
func openNamedWallet(walletName string) (e2wtypes.Wallet, error) {
if viper.GetString("remote") != "" {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
assert(viper.GetString("client-cert") != "", "remote connections require client-cert")
assert(viper.GetString("client-key") != "", "remote connections require client-key")
credentials, err := dirk.ComposeCredentials(ctx, viper.GetString("client-cert"), viper.GetString("client-key"), viper.GetString("server-ca-cert"))
if err != nil {
return nil, errors.Wrap(err, "failed to build dirk credentials")
}
endpoints, err := remotesToEndpoints([]string{viper.GetString("remote")})
if err != nil {
return nil, errors.Wrap(err, "failed to parse remote servers")
}
return dirk.OpenWallet(ctx, walletName, credentials, endpoints)
}
return walletFromPath(walletName)
}
// relockAccount locks an account; generally called as a defer after an account is unlocked.
func relockAccount(locker e2wtypes.AccountLocker) {
errCheck(locker.Lock(context.Background()), "failed to re-lock account")

View File

@@ -22,8 +22,6 @@ import (
"github.com/spf13/viper"
"github.com/wealdtech/go-bytesutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// signatureSignCmd represents the signature sign command
@@ -53,17 +51,7 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
outputIf(debug, fmt.Sprintf("Domain is %#x", domain))
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet does not support obtaining accounts by name")
account, err := accountByNameProvider.AccountByName(ctx, accountName)
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
var fixedSizeData [32]byte

View File

@@ -15,15 +15,17 @@ package cmd
import (
"context"
"encoding/hex"
"fmt"
"os"
"strings"
"github.com/prysmaticlabs/go-ssz"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
"github.com/wealdtech/go-bytesutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -58,49 +60,44 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
assert(len(domain) == 32, "Domain data invalid")
}
var pubKey e2types.PublicKey
assert(signatureVerifySigner != "" || viper.GetString("account") != "", "Either --signer or --account should be supplied")
if viper.GetString("account") != "" {
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
account, err := signatureVerifyAccount()
errCheck(err, "Failed to obtain account")
outputIf(debug, fmt.Sprintf("Public key is %#x", account.PublicKey().Marshal()))
accountByNameProvider, isAccountByNameProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
assert(isAccountByNameProvider, "wallet cannot obtain accounts by name")
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err := accountByNameProvider.AccountByName(ctx, accountName)
errCheck(err, "Failed to obtain account")
pubKey, err = bestPublicKey(account)
errCheck(err, "Failed to obtain account's public key")
} else {
pubKeyBytes, err := bytesutil.FromHexString(signatureVerifySigner)
errCheck(err, "Invalid public key")
pubKey, err = e2types.BLSPublicKeyFromBytes(pubKeyBytes)
errCheck(err, "Invalid public key")
}
outputIf(debug, fmt.Sprintf("Public key is %#x", pubKey.Marshal()))
container := &signingContainer{
Root: data,
Domain: domain,
}
outputIf(debug, fmt.Sprintf("Data root is %#x", data))
outputIf(debug, fmt.Sprintf("Domain is %#x", domain))
root, err := ssz.HashTreeRoot(container)
errCheck(err, "Failed to create signing root")
outputIf(debug, fmt.Sprintf("Signing root is %#x", root))
var root [32]byte
copy(root[:], data)
verified, err := verifyRoot(account, root, domain, signature)
errCheck(err, "Failed to verify data")
assert(verified, "Failed to verify")
verified := signature.Verify(root[:], pubKey)
if !verified {
outputIf(!quiet, "Not verified")
os.Exit(_exitFailure)
}
outputIf(!quiet, "Verified")
outputIf(verbose, "Verified")
os.Exit(_exitSuccess)
},
}
// signatureVerifyAccount obtains the account for the signature verify command.
func signatureVerifyAccount() (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 {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(signatureVerifySigner, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", signatureVerifySigner))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", signatureVerifySigner))
}
}
return account, nil
}
func init() {
signatureCmd.AddCommand(signatureVerifyCmd)
signatureFlags(signatureVerifyCmd)

View File

@@ -36,6 +36,17 @@ func signStruct(account wtypes.Account, data interface{}, domain []byte) (e2type
return signRoot(account, objRoot, domain)
}
// verifyStruct verifies the signature of an arbitrary structure.
func verifyStruct(account wtypes.Account, data interface{}, domain []byte, signature e2types.Signature) (bool, error) {
objRoot, err := ssz.HashTreeRoot(data)
outputIf(debug, fmt.Sprintf("Object root is %#x", objRoot))
if err != nil {
return false, err
}
return verifyRoot(account, objRoot, domain, signature)
}
// SigningContainer is the container for signing roots with a domain.
// Contains SSZ sizes to allow for correct calculation of root.
type signingContainer struct {
@@ -64,6 +75,21 @@ func signRoot(account wtypes.Account, root [32]byte, domain []byte) (e2types.Sig
return sign(account, signingRoot[:])
}
func verifyRoot(account wtypes.Account, root [32]byte, domain []byte, signature e2types.Signature) (bool, error) {
// Build the signing data manually.
container := &signingContainer{
Root: root[:],
Domain: domain,
}
outputIf(debug, fmt.Sprintf("Signing container:\n root: %#x\n domain: %#x", container.Root, container.Domain))
signingRoot, err := ssz.HashTreeRoot(container)
if err != nil {
return false, err
}
outputIf(debug, fmt.Sprintf("Signing root: %#x", signingRoot))
return verify(account, signingRoot[:], signature)
}
func signGeneric(account wtypes.Account, data []byte, domain []byte) (e2types.Signature, error) {
alreadyUnlocked, err := unlock(account)
if err != nil {
@@ -113,6 +139,15 @@ func sign(account wtypes.Account, data []byte) (e2types.Signature, error) {
return signature, err
}
// verify the signature of arbitrary data.
func verify(account wtypes.Account, data []byte, signature e2types.Signature) (bool, error) {
pubKey, err := bestPublicKey(account)
if err != nil {
return false, errors.Wrap(err, "failed to obtain account public key")
}
return signature.Verify(data, pubKey), nil
}
// unlock attempts to unlock an account. It returns true if the account was already unlocked.
func unlock(account e2wtypes.Account) (bool, error) {
locker, isAccountLocker := account.(e2wtypes.AccountLocker)

View File

@@ -26,7 +26,6 @@ import (
"github.com/wealdtech/ethdo/grpc"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
e2wallet "github.com/wealdtech/go-eth2-wallet"
string2eth "github.com/wealdtech/go-string2eth"
)
@@ -54,12 +53,8 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
defer cancel()
assert(validatorDepositDataValidatorAccount != "", "--validatoraccount is required")
validatorWalletName, validatorAccountSpec, err := e2wallet.WalletAndAccountNames(validatorDepositDataValidatorAccount)
errCheck(err, "Failed to obtain wallet and account names")
validatorWallet, err := openNamedWallet(validatorWalletName)
errCheck(err, "Failed to obtain validator wallet")
validatorAccounts, err := accountsFromPath(ctx, validatorWallet, validatorAccountSpec)
errCheck(err, "Failed to obtain validator account")
validatorWallet, validatorAccounts, err := walletAndAccountsFromPath(ctx, validatorDepositDataValidatorAccount)
errCheck(err, "Failed to obtain validator accounts")
assert(len(validatorAccounts) > 0, "Failed to obtain validator account")
for _, validatorAccount := range validatorAccounts {
@@ -72,7 +67,7 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
assert(validatorDepositDataWithdrawalAccount != "" || validatorDepositDataWithdrawalPubKey != "", "--withdrawalaccount or --withdrawalpubkey is required")
var withdrawalCredentials []byte
if validatorDepositDataWithdrawalAccount != "" {
withdrawalAccount, err := accountFromPath(ctx, validatorDepositDataWithdrawalAccount)
_, withdrawalAccount, err := walletAndAccountFromPath(ctx, validatorDepositDataWithdrawalAccount)
errCheck(err, "Failed to obtain withdrawal account")
pubKey, err := bestPublicKey(withdrawalAccount)
errCheck(err, "Withdrawal account does not provide a public key")

View File

@@ -52,19 +52,20 @@ In quiet mode this will return 0 if the transaction has been generated, otherwis
err := connect()
errCheck(err, "Failed to obtain connect to Ethereum 2 beacon chain node")
exit, signature := validatorExitHandleInput(ctx)
validatorExitHandleExit(ctx, exit, signature)
exit, signature, forkVersion := validatorExitHandleInput(ctx)
validatorExitHandleExit(ctx, exit, signature, forkVersion)
os.Exit(_exitSuccess)
},
}
func validatorExitHandleInput(ctx context.Context) (*ethpb.VoluntaryExit, e2types.Signature) {
func validatorExitHandleInput(ctx context.Context) (*ethpb.VoluntaryExit, e2types.Signature, []byte) {
if validatorExitJSON != "" {
return validatorExitHandleJSONInput(validatorExitJSON)
}
if viper.GetString("account") != "" {
account, err := accountFromPath(ctx, viper.GetString("account"))
errCheck(err, "Failed to access account")
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
outputIf(debug, fmt.Sprintf("Account %s obtained", account.Name()))
return validatorExitHandleAccountInput(ctx, account)
}
if validatorExitKey != "" {
@@ -75,10 +76,10 @@ func validatorExitHandleInput(ctx context.Context) (*ethpb.VoluntaryExit, e2type
return validatorExitHandleAccountInput(ctx, account)
}
die("one of --json, --account or --key is required")
return nil, nil
return nil, nil, nil
}
func validatorExitHandleJSONInput(input string) (*ethpb.VoluntaryExit, e2types.Signature) {
func validatorExitHandleJSONInput(input string) (*ethpb.VoluntaryExit, e2types.Signature, []byte) {
data := &validatorExitData{}
err := json.Unmarshal([]byte(input), data)
errCheck(err, "Invalid JSON input")
@@ -88,10 +89,10 @@ func validatorExitHandleJSONInput(input string) (*ethpb.VoluntaryExit, e2types.S
}
signature, err := e2types.BLSSignatureFromBytes(data.Signature)
errCheck(err, "Invalid signature")
return exit, signature
return exit, signature, data.ForkVersion
}
func validatorExitHandleAccountInput(ctx context.Context, account e2wtypes.Account) (*ethpb.VoluntaryExit, e2types.Signature) {
func validatorExitHandleAccountInput(ctx context.Context, account e2wtypes.Account) (*ethpb.VoluntaryExit, e2types.Signature, []byte) {
exit := &ethpb.VoluntaryExit{}
// Beacon chain config required for later work.
@@ -137,12 +138,12 @@ func validatorExitHandleAccountInput(ctx context.Context, account e2wtypes.Accou
}
// TODO fetch current fork version from config (currently using genesis fork version)
currentForkVersion := config["GenesisForkVersion"].([]byte)
outputIf(debug, fmt.Sprintf("Current fork version is %x", currentForkVersion))
forkVersion := config["GenesisForkVersion"].([]byte)
outputIf(debug, fmt.Sprintf("Current fork version is %x", forkVersion))
genesisValidatorsRoot, err := grpc.FetchGenesisValidatorsRoot(eth2GRPCConn)
outputIf(debug, fmt.Sprintf("Genesis validators root is %x", genesisValidatorsRoot))
errCheck(err, "Failed to obtain genesis validators root")
domain := e2types.Domain(e2types.DomainVoluntaryExit, currentForkVersion, genesisValidatorsRoot)
domain := e2types.Domain(e2types.DomainVoluntaryExit, forkVersion, genesisValidatorsRoot)
alreadyUnlocked, err := unlock(account)
errCheck(err, "Failed to unlock account; please confirm passphrase is correct")
@@ -152,16 +153,17 @@ func validatorExitHandleAccountInput(ctx context.Context, account e2wtypes.Accou
}
errCheck(err, "Failed to sign exit proposal")
return exit, signature
return exit, signature, forkVersion
}
// validatorExitHandleExit handles the exit request.
func validatorExitHandleExit(ctx context.Context, exit *ethpb.VoluntaryExit, signature e2types.Signature) {
func validatorExitHandleExit(ctx context.Context, exit *ethpb.VoluntaryExit, signature e2types.Signature, forkVersion []byte) {
if validatorExitJSONOutput {
data := &validatorExitData{
Epoch: exit.Epoch,
ValidatorIndex: exit.ValidatorIndex,
Signature: signature.Marshal(),
ForkVersion: forkVersion,
}
res, err := json.Marshal(data)
errCheck(err, "Failed to generate JSON")
@@ -192,11 +194,12 @@ type validatorExitData struct {
Epoch uint64 `json:"epoch"`
ValidatorIndex uint64 `json:"validator_index"`
Signature []byte `json:"signature"`
ForkVersion []byte `json:"fork_version"`
}
// MarshalJSON implements custom JSON marshaller.
func (d *validatorExitData) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"epoch":%d,"validator_index":%d,"signature":"%#x"}`, d.Epoch, d.ValidatorIndex, d.Signature)), nil
return []byte(fmt.Sprintf(`{"epoch":%d,"validator_index":%d,"signature":"%#x","fork_version":"%#x"}`, d.Epoch, d.ValidatorIndex, d.Signature, d.ForkVersion)), nil
}
// UnmarshalJSON implements custom JSON unmarshaller.
@@ -242,5 +245,19 @@ func (d *validatorExitData) UnmarshalJSON(data []byte) error {
return errors.New("signature missing")
}
if val, exists := v["fork_version"]; exists {
forkVersionBytes, ok := val.(string)
if !ok {
return errors.New("fork version invalid")
}
forkVersion, err := hex.DecodeString(strings.TrimPrefix(forkVersionBytes, "0x"))
if err != nil {
return errors.Wrap(err, "fork version invalid")
}
d.ForkVersion = forkVersion
} else {
return errors.New("fork version missing")
}
return nil
}

View File

@@ -32,7 +32,6 @@ import (
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/grpc"
"github.com/wealdtech/ethdo/util"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
string2eth "github.com/wealdtech/go-string2eth"
)
@@ -56,6 +55,19 @@ In quiet mode this will return 0 if the validator information can be obtained, o
account, err := validatorInfoAccount()
errCheck(err, "Failed to obtain validator account")
if verbose {
network := network()
outputIf(debug, fmt.Sprintf("Network is %s", network))
pubKey, err := bestPublicKey(account)
if err == nil {
deposits, totalDeposited, err := graphData(network, pubKey.Marshal())
if err == nil {
fmt.Printf("Number of deposits: %d\n", deposits)
fmt.Printf("Total deposited: %s\n", string2eth.GWeiToString(totalDeposited, true))
}
}
}
validatorInfo, err := grpc.FetchValidatorInfo(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator information")
validator, err := grpc.FetchValidator(eth2GRPCConn, account)
@@ -79,16 +91,6 @@ In quiet mode this will return 0 if the validator information can be obtained, o
fmt.Printf("Status: %s\n", strings.Title(strings.ToLower(validatorInfo.Status.String())))
fmt.Printf("Balance: %s\n", string2eth.GWeiToString(validatorInfo.Balance, true))
if verbose {
network := network()
outputIf(debug, fmt.Sprintf("Network is %s", network))
deposits, totalDeposited, err := graphData(network, validator.PublicKey)
if err == nil {
fmt.Printf("Number of deposits: %d\n", deposits)
fmt.Printf("Total deposited: %s\n", string2eth.GWeiToString(totalDeposited, true))
}
}
if validatorInfo.Status == ethpb.ValidatorStatus_ACTIVE ||
validatorInfo.Status == ethpb.ValidatorStatus_EXITING ||
validatorInfo.Status == ethpb.ValidatorStatus_SLASHING {
@@ -125,22 +127,11 @@ In quiet mode this will return 0 if the validator information can be obtained, o
// validatorInfoAccount obtains the account for the validator info command.
func validatorInfoAccount() (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
wallet, err := openWallet()
if err != nil {
return nil, errors.Wrap(err, "failed to open wallet")
}
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account name")
}
accountByNameProvider, isProvider := wallet.(e2wtypes.WalletAccountByNameProvider)
if !isProvider {
return nil, errors.New("failed to ask wallet for account by name")
}
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
account, err = accountByNameProvider.AccountByName(ctx, accountName)
_, account, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}

View File

@@ -22,7 +22,7 @@ import (
"github.com/spf13/viper"
)
var ReleaseVersion = "local build from v1.5.1"
var ReleaseVersion = "local build from v1.5.7"
// versionCmd represents the version command
var versionCmd = &cobra.Command{

View File

@@ -40,8 +40,8 @@ In quiet mode this will return 0 if the wallet holds any addresses, otherwise 1.
assert(viper.GetString("wallet") != "", "wallet is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
wallet, err := walletFromInput(ctx)
errCheck(err, "Failed to obtain wallet")
accounts := make([]e2wtypes.Account, 0, 128)
for account := range wallet.Accounts(ctx) {

View File

@@ -14,6 +14,7 @@
package cmd
import (
"context"
"os"
"path/filepath"
@@ -34,7 +35,10 @@ In quiet mode this will return 0 if the wallet has been deleted, otherwise 1.`,
assert(viper.GetString("remote") == "", "wallet delete not available with remote wallets")
assert(viper.GetString("wallet") != "", "--wallet is required")
wallet, err := walletFromPath(viper.GetString("wallet"))
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
wallet, err := walletFromPath(ctx, viper.GetString("wallet"))
errCheck(err, "Failed to access wallet")
storeProvider, ok := wallet.(wtypes.StoreProvider)

View File

@@ -41,7 +41,7 @@ In quiet mode this will return 0 if the wallet is able to be exported, otherwise
assert(viper.GetString("wallet") != "", "--wallet is required")
assert(walletExportPassphrase != "", "--exportpassphrase is required")
wallet, err := walletFromPath(viper.GetString("wallet"))
wallet, err := walletFromPath(ctx, viper.GetString("wallet"))
errCheck(err, "Failed to access wallet")
_, ok := wallet.(types.WalletExporter)

View File

@@ -39,7 +39,7 @@ In quiet mode this will return 0 if the wallet exists, otherwise 1.`,
assert(viper.GetString("remote") == "", "wallet info not available with remote wallets")
assert(viper.GetString("wallet") != "", "--wallet is required")
wallet, err := walletFromPath(viper.GetString("wallet"))
wallet, err := walletFromPath(ctx, viper.GetString("wallet"))
errCheck(err, "unknown wallet")
if quiet {

View File

@@ -65,3 +65,21 @@ ethdo wallet info --verbose --wallet="My wallet"
```
This will provide, amongst other information, a `Location` line giving the directory where the wallet information resides.
## Recreate launchpad wallet and accounts
Recreating launchpad accounts requires two steps: recreating the wallet, and recreating the individual accounts. All that is required is the mnemonic from the launchpad process.
To recreate the wallet with the given mnemonic run the following command (changing the wallet name, passphrase and mnemonic as required):
```sh
ethdo wallet create --wallet="Launchpad" --type=hd --walletpassphrase=walletsecret --mnemonic="faculty key lamp panel appear choose express off absent dance strike twenty elephant expect swift that resist bicycle kind sun favorite evoke engage thumb"
```
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:
```sh
ethdo account create --wallet="Launchpad" --walletpassphrase=walletsecret --passphrase=secret --path=m/12381/3600/1/0/0
```

View File

@@ -112,9 +112,12 @@ Account commands focus on information about local accounts, generally those used
`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
- `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 `--walletpassphrase` to unlock the wallet seed.
For distributed accounts you will also need to supply `--participants` and `--signing-threshold`.
```sh
$ ethdo account create --account="Personal wallet/Operations" --walletpassphrase="my wallet secret" --passphrase="my account secret"
```
@@ -200,11 +203,10 @@ $ ethdo signature sign --data="0x08140077a94642919041503caf5cc1c89c7744a2a08d43c
```sh
$ ethdo signature verify --data="0x08140077a94642919041503caf5cc1c89c7744a2a08d43cec91df1795b23ecf2" --signature="0x87c83b31081744667406a11170c5585a11195621d0d3f796bd9006ac4cb5f61c10bf8c5b3014cd4f792b143a644cae100cb3155e8b00a961287bd9e7a5e18cb3b80930708bc9074d11ff47f1e8b9dd0b633e71bcea725fc3e550fdc259c3d130" --account="Personal wallet/Operations"
Verified
$ ethdo signature verify --data="0x08140077a94642919041503caf5cc1c89c7744a2a08d43cec91df1795b23ecf2" --signature="0x87c83b31081744667406a11170c5585a11195621d0d3f796bd9006ac4cb5f61c10bf8c5b3014cd4f792b143a644cae100cb3155e8b00a961287bd9e7a5e18cb3b80930708bc9074d11ff47f1e8b9dd0b633e71bcea725fc3e550fdc259c3d130" --account="Personal wallet/Auctions"
Not verified
$ ethdo signature verify --data="0x08140077a94642919041503caf5cc1c89c7744a2a08d43cec91df1795b23ecf2" --signature="0x89abe2e544ef3eafe397db036103b1d066ba86497f36ed4ab0264162eadc89c7744a2a08d43cec91df128660e70ecbbe11031b4c2e53682d2b91e67b886429bf8fac9bad8c7b63c5f231cc8d66b1377e06e27138b1ddc64b27c6e593e07ebb4b" --signer="0x8e2f9e8cc29658ff37ecc30e95a0807579b224586c185d128cb7a7490784c1ad9b0ab93dbe604ab075b40079931e6670"
$ ethdo signature verify --data="0x08140077a94642919041503caf5cc1c89c7744a2a08d43cec91df1795b23ecf2" --signature="0x87c83b31081744667406a11170c5585a11195621d0d3f796bd9006ac4cb5f61c10bf8c5b3014cd4f792b143a644cae100cb3155e8b00a961287bd9e7a5e18cb3b80930708bc9074d11ff47f1e8b9dd0b633e71bcea725fc3e550fdc259c3d130" --signer="0xad1868210a0cff7aff22633c003c503d4c199c8dcca13bba5b3232fc784d39d3855936e94ce184c3ce27bf15d4347695"
$ ethdo signature verify --data="0x08140077a94642919041503caf5cc1c89c7744a2a08d43cec91df1795b23ecf2" --signature="0x87c83b31081744667406a11170c5585a11195621d0d3f796bd9006ac4cb5f61c10bf8c5b3014cd4f792b143a644cae100cb3155e8b00a961287bd9e7a5e18cb3b80930708bc9074d11ff47f1e8b9dd0b633e71bcea725fc3e550fdc259c3d130" --signer="0xad1868210a0cff7aff22633c003c503d4c199c8dcca13bba5b3232fc784d39d3855936e94ce184c3ce27bf15d4347695" --verbose
Verified
```
@@ -308,6 +310,37 @@ Prior justified epoch: 3
Prior justified epoch distance: 4
```
### `deposit` comands
Deposit commands focus on information about deposit data information in a JSON file generated by the `ethdo validator depositdata` command.
#### `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 or the JSON itself
- `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
```
### `exit` comands
Exit commands focus on information about validator exits generated by the `ethdo validator exit` command.
#### `verify`
`ethdo exit verify` verifies the validator exit information in a JSON file generated by the `ethdo validator exit` command. Options include:
- `data`: either a path to the JSON file or the JSON itself
- `account`: the account that generated the exit transaction (if available as an account)
- `pubkey`: the public key of the account that generated the exit transaction
```sh
$ ethdo exit verify --data=${HOME}/exit.json --pubkey=0xa951530887ae2494a8cc4f11cf186963b0051ac4f7942375585b9cf98324db1e532a67e521d0fcaab510edad1352394c
```
### `node` commands
Node commands focus on information from an Ethereum 2 node.

6
go.mod
View File

@@ -30,15 +30,15 @@ require (
github.com/wealdtech/go-ecodec v1.1.0
github.com/wealdtech/go-eth2-types/v2 v2.5.0
github.com/wealdtech/go-eth2-util v1.5.0
github.com/wealdtech/go-eth2-wallet v1.12.0
github.com/wealdtech/go-eth2-wallet v1.13.0
github.com/wealdtech/go-eth2-wallet-dirk v1.0.1
github.com/wealdtech/go-eth2-wallet-distributed v1.1.0
github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.1.0
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.3.0
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.4.0
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.3.0
github.com/wealdtech/go-eth2-wallet-store-filesystem v1.16.1
github.com/wealdtech/go-eth2-wallet-store-s3 v1.8.0
github.com/wealdtech/go-eth2-wallet-types/v2 v2.6.0
github.com/wealdtech/go-eth2-wallet-types/v2 v2.7.0
github.com/wealdtech/go-string2eth v1.1.0
golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6 // indirect
golang.org/x/text v0.3.3 // indirect

6
go.sum
View File

@@ -334,6 +334,8 @@ github.com/wealdtech/go-eth2-wallet v1.11.0 h1:2KfrWDqF4sWGgk4N5+DaYmh0hOnqiCl0P
github.com/wealdtech/go-eth2-wallet v1.11.0/go.mod h1:E9ZRNO4JNdi27ys7oc+xWWucXu4IGfV5q1vWC9X3oqg=
github.com/wealdtech/go-eth2-wallet v1.12.0 h1:nrwI3jPhehUhJGlBtNv/UmIo/57llvuVZZavLnfdQHI=
github.com/wealdtech/go-eth2-wallet v1.12.0/go.mod h1:ouV+YSMbzk2dyecmofm8jhaMKdSigdIPMSnSqmWEfW8=
github.com/wealdtech/go-eth2-wallet v1.13.0 h1:ayy/jBcKVZhqU4OMlZDieRzhwSiF3Ozmc3e3LrdpWro=
github.com/wealdtech/go-eth2-wallet v1.13.0/go.mod h1:O4efkfrSBRRa7Q8vZQ8usUCBRki+/zE531b3JqScIII=
github.com/wealdtech/go-eth2-wallet-dirk v1.0.0 h1:1QUcWILF3h4OLCgTPpWklvRSuPu0fqrt15jwSm7CSC4=
github.com/wealdtech/go-eth2-wallet-dirk v1.0.0/go.mod h1:VTzjJ51dedvYPr4huI7g7KXZVTpGR6ZrCDQwBxJpLck=
github.com/wealdtech/go-eth2-wallet-dirk v1.0.1 h1:YUE1QlJPun8b+xbz0JM71/3t1i9zp9KjcZdJvtJQL+E=
@@ -351,6 +353,8 @@ github.com/wealdtech/go-eth2-wallet-hd/v2 v2.2.0 h1:L+yrAn8TC9DQUw+S7moOJxQTp2jr
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.2.0/go.mod h1:lhSwtkIO/Pfg5kz8k50yrDgj7ZQaElCPsXnixlrQn/I=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.3.0 h1:UORXUYRoUYgYF96Y+QiBq33OKQVtn/nEjnSoQbe1UOA=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.3.0/go.mod h1:Kc/8WcqMTczfH2xy5mDfCRd0NI/ca/j2jXmqJ7gz8yk=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.4.0 h1:bqG/i1fpDpnQFgJ+NBLgOLdmGhXiinwlrFVD1OAgZlo=
github.com/wealdtech/go-eth2-wallet-hd/v2 v2.4.0/go.mod h1:xBbzc+aRD06dTL5phnMshpM4sryyWYM8m86sjQKG5u0=
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.1.2/go.mod h1:IssxoHII0ewO1VysMfCmdJP1D00tRhRhXIhhaEXIOVE=
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.2.0 h1:h4eePfG0ANOJYMonmIYOvxJ9uLmBEX4APb2O8Vhtv6k=
github.com/wealdtech/go-eth2-wallet-nd/v2 v2.2.0/go.mod h1:Un2EtseZWSObmTBjgkt7Qz2am54S/0115jrF83lto1U=
@@ -380,6 +384,8 @@ github.com/wealdtech/go-eth2-wallet-types/v2 v2.5.0 h1:J29mbkSCUMl2xdu8Lg6U+JptF
github.com/wealdtech/go-eth2-wallet-types/v2 v2.5.0/go.mod h1:X9kYUH/E5YMqFMZ4xL6MJanABUkJGaH/yPZRT2o+yYA=
github.com/wealdtech/go-eth2-wallet-types/v2 v2.6.0 h1:vBrH5icPPSeb14cdShA7/P2PBZOgZscJ2IhBlTIaFrA=
github.com/wealdtech/go-eth2-wallet-types/v2 v2.6.0/go.mod h1:X9kYUH/E5YMqFMZ4xL6MJanABUkJGaH/yPZRT2o+yYA=
github.com/wealdtech/go-eth2-wallet-types/v2 v2.7.0 h1:pquFQdIWEiSYrpIpFuvsRuialI8t9KhFsPvbIBPnzic=
github.com/wealdtech/go-eth2-wallet-types/v2 v2.7.0/go.mod h1:X9kYUH/E5YMqFMZ4xL6MJanABUkJGaH/yPZRT2o+yYA=
github.com/wealdtech/go-indexer v1.0.0 h1:/S4rfWQbSOnnYmwnvuTVatDibZ8o1s9bmTCHO16XINg=
github.com/wealdtech/go-indexer v1.0.0/go.mod h1:u1cjsbsOXsm5jzJDyLmZY7GsrdX8KYXKBXkZcAmk3Zg=
github.com/wealdtech/go-string2eth v1.1.0 h1:USJQmysUrBYYmZs7d45pMb90hRSyEwizP7lZaOZLDAw=