Add 'exit verify' command

This commit is contained in:
Jim McDonald
2020-07-31 16:31:06 +01:00
parent b34a633e53
commit 115d037948
24 changed files with 417 additions and 270 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

@@ -33,9 +33,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 +46,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")
}

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

@@ -17,13 +17,11 @@ import (
"context"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"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"
)
@@ -36,30 +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")
outputIf(debug, fmt.Sprintf("Opened wallet %q of type %s", wallet.Name(), wallet.Type()))
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
if wallet.Type() == "hierarchical deterministic" && strings.HasPrefix(accountName, "m/") {
assert(getWalletPassphrase() != "", "walletpassphrase is required to show information about dynamically generated hierarchical deterministic accounts")
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")
}
}
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

@@ -17,11 +17,9 @@ import (
"context"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
e2wallet "github.com/wealdtech/go-eth2-wallet"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
@@ -35,31 +33,13 @@ 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) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
assert(!remote, "account keys not available with remote wallets")
assert(viper.GetString("account") != "", "--account is required")
wallet, err := openWallet()
errCheck(err, "Failed to access wallet")
outputIf(debug, fmt.Sprintf("Opened wallet %q of type %s", wallet.Name(), wallet.Type()))
_, accountName, err := e2wallet.WalletAndAccountNames(viper.GetString("account"))
errCheck(err, "Failed to obtain account name")
if wallet.Type() == "hierarchical deterministic" && strings.HasPrefix(accountName, "m/") {
assert(getWalletPassphrase() != "", "walletpassphrase is required to show information about dynamically generated hierarchical deterministic accounts")
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")
}
}
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)
_, account, err := walletAndAccountFromInput(ctx)
errCheck(err, "Failed to obtain account")
privateKeyProvider, isPrivateKeyProvider := account.(e2wtypes.AccountPrivateKeyProvider)

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,32 @@ func outputIf(condition bool, msg string) {
}
}
// walletFromInput obtains a wallet given the information in the viper variable "wallet".
func walletFromInput(ctx context.Context) (e2wtypes.Wallet, error) {
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 +269,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 +295,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,21 +303,30 @@ 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) {
// 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")
}
accounts := make([]e2wtypes.Account, 0)
if accountSpec == "" {
accountSpec = "^.*$"
if path == "" {
path = "^.*$"
} else {
accountSpec = fmt.Sprintf("^%s$", accountSpec)
path = fmt.Sprintf("^%s$", path)
}
re := regexp.MustCompile(accountSpec)
re := regexp.MustCompile(path)
for account := range wallet.Accounts(ctx) {
if re.Match([]byte(account.Name())) {
@@ -305,7 +339,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 +403,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"
)
@@ -128,33 +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")
}
if wallet.Type() == "hierarchical deterministic" && strings.HasPrefix(accountName, "m/") {
assert(getWalletPassphrase() != "", "walletpassphrase is required to obtain information about validators with dynamically generated hierarchical deterministic accounts")
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")
}
}
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.3"
var ReleaseVersion = "local build from v1.5.4"
// 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

@@ -200,11 +200,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 +307,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.