mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c3237cd0d | ||
|
|
7215e04a69 | ||
|
|
71e9c0471b | ||
|
|
1a6f402fb8 |
@@ -5,7 +5,7 @@
|
||||
|
||||
A command-line tool for managing common tasks in Ethereum 2.
|
||||
|
||||
** Please note that this library uses standards that are not yet final, and as such may result in changes that alter public and private keys. Do not use this library for production use just yet **
|
||||
**Please note that this tool and its underlying libraries have not yet undergone a security audit; use at your own risk.**
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
pb "github.com/wealdtech/eth2-signer-api/pb/v1"
|
||||
util "github.com/wealdtech/go-eth2-util"
|
||||
)
|
||||
|
||||
var accountInfoCmd = &cobra.Command{
|
||||
@@ -33,6 +34,7 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
assert(rootAccount != "", "--account is required")
|
||||
|
||||
var withdrawalCredentials []byte
|
||||
if remote {
|
||||
listerClient := pb.NewListerClient(remoteGRPCConn)
|
||||
listAccountsReq := &pb.ListAccountsRequest{
|
||||
@@ -44,13 +46,18 @@ In quiet mode this will return 0 if the account exists, otherwise 1.`,
|
||||
errCheck(err, "Failed to access account")
|
||||
assert(resp.State == pb.ResponseState_SUCCEEDED, "No such account")
|
||||
assert(len(resp.Accounts) == 1, "No such account")
|
||||
fmt.Printf("Public key: %#048x\n", resp.Accounts[0].PublicKey)
|
||||
|
||||
fmt.Printf("Public key: %#x\n", resp.Accounts[0].PublicKey)
|
||||
withdrawalCredentials = util.SHA256(resp.Accounts[0].PublicKey)
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
outputIf(verbose, fmt.Sprintf("Withdrawal credentials: %#x", withdrawalCredentials))
|
||||
} else {
|
||||
account, err := accountFromPath(rootAccount)
|
||||
errCheck(err, "Failed to access wallet")
|
||||
outputIf(verbose, fmt.Sprintf("UUID: %v", account.ID()))
|
||||
outputIf(!quiet, fmt.Sprintf("Public key: %#048x", account.PublicKey().Marshal()))
|
||||
outputIf(!quiet, fmt.Sprintf("Public key: %#x", account.PublicKey().Marshal()))
|
||||
withdrawalCredentials = util.SHA256(account.PublicKey().Marshal())
|
||||
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
|
||||
outputIf(verbose, fmt.Sprintf("Withdrawal credentials: %#x", withdrawalCredentials))
|
||||
outputIf(verbose && account.Path() != "", fmt.Sprintf("Path: %s", account.Path()))
|
||||
}
|
||||
|
||||
|
||||
32
cmd/deposit.go
Normal file
32
cmd/deposit.go
Normal 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"
|
||||
)
|
||||
|
||||
// depositCmd represents the deposit command
|
||||
var depositCmd = &cobra.Command{
|
||||
Use: "deposit",
|
||||
Short: "Manage Ethereum 2 deposits",
|
||||
Long: `Manage Ethereum 2 deposits.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(depositCmd)
|
||||
}
|
||||
|
||||
func depositFlags(cmd *cobra.Command) {
|
||||
}
|
||||
234
cmd/depositverify.go
Normal file
234
cmd/depositverify.go
Normal file
@@ -0,0 +1,234 @@
|
||||
// Copyright © 2019, 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 (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||
util "github.com/wealdtech/go-eth2-util"
|
||||
string2eth "github.com/wealdtech/go-string2eth"
|
||||
)
|
||||
|
||||
type depositData struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Account string `json:"account,omitempty"`
|
||||
PublicKey string `json:"pubkey"`
|
||||
WithdrawalCredentials string `json:"withdrawal_credentials"`
|
||||
Signature string `json:"signature"`
|
||||
DepositDataRoot string `json:"deposit_data_root"`
|
||||
Value uint64 `json:"value"`
|
||||
Version uint64 `json:"version"`
|
||||
}
|
||||
|
||||
var depositVerifyData string
|
||||
var depositVerifyWithdrawalPubKey string
|
||||
var depositVerifyValidatorPubKey string
|
||||
var depositVerifyDepositValue string
|
||||
|
||||
var depositVerifyCmd = &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) {
|
||||
assert(depositVerifyData != "", "--data is required")
|
||||
deposits, err := depositDataFromJSON(depositVerifyData)
|
||||
errCheck(err, "Failed to fetch deposit data")
|
||||
|
||||
withdrawalCredentials := ""
|
||||
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)
|
||||
}
|
||||
outputIf(debug, fmt.Sprintf("Withdrawal credentials are %s", withdrawalCredentials))
|
||||
|
||||
depositValue := uint64(0)
|
||||
if depositVerifyDepositValue != "" {
|
||||
depositValue, err = string2eth.StringToGWei(depositVerifyDepositValue)
|
||||
errCheck(err, "Invalid value")
|
||||
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
|
||||
assert(depositValue >= 1000000000, "deposit value must be at least 1 Ether") // MIN_DEPOSIT_AMOUNT
|
||||
}
|
||||
|
||||
validatorPubKeys := make(map[string]bool)
|
||||
if depositVerifyValidatorPubKey != "" {
|
||||
validatorPubKeys, err = validatorPubKeysFromInput(depositVerifyValidatorPubKey)
|
||||
errCheck(err, "Failed to obtain validator public key(s))")
|
||||
}
|
||||
|
||||
failures := false
|
||||
for i, deposit := range deposits {
|
||||
if withdrawalCredentials != "" {
|
||||
if deposit.WithdrawalCredentials != withdrawalCredentials {
|
||||
outputIf(!quiet, fmt.Sprintf("Invalid withdrawal credentials for deposit %d", i))
|
||||
failures = true
|
||||
}
|
||||
}
|
||||
if depositValue != 0 {
|
||||
if deposit.Value != depositValue {
|
||||
outputIf(!quiet, fmt.Sprintf("Invalid deposit value for deposit %d", i))
|
||||
failures = true
|
||||
}
|
||||
}
|
||||
if len(validatorPubKeys) != 0 {
|
||||
if _, exists := validatorPubKeys[deposit.PublicKey]; !exists {
|
||||
outputIf(!quiet, fmt.Sprintf("Unknown validator public key for deposit %d", i))
|
||||
failures = true
|
||||
}
|
||||
}
|
||||
outputIf(verbose, fmt.Sprintf("Deposit %d verified", i))
|
||||
}
|
||||
|
||||
if failures {
|
||||
os.Exit(_exitFailure)
|
||||
}
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
}
|
||||
|
||||
func validatorPubKeysFromInput(input string) (map[string]bool, error) {
|
||||
pubKeys := make(map[string]bool)
|
||||
var err error
|
||||
var data []byte
|
||||
// Input could be a public key or a path to public keys.
|
||||
if strings.HasPrefix(input, "0x") {
|
||||
// Looks like a public key.
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(input, "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "public key is not a hex string")
|
||||
}
|
||||
if len(pubKeyBytes) != 48 {
|
||||
return nil, errors.New("public key should be 48 bytes")
|
||||
}
|
||||
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid public key")
|
||||
}
|
||||
pubKeys[fmt.Sprintf("%x", pubKey.Marshal())] = true
|
||||
} else {
|
||||
// Assume it's a path to a file of public keys.
|
||||
data, err = ioutil.ReadFile(input)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find public key file")
|
||||
}
|
||||
lines := bytes.Split(bytes.Replace(data, []byte("\r\n"), []byte("\n"), -1), []byte("\n"))
|
||||
if len(lines) == 0 {
|
||||
return nil, errors.New("file has no public keys")
|
||||
}
|
||||
for _, line := range lines {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(string(line), "0x"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "public key is not a hex string")
|
||||
}
|
||||
if len(pubKeyBytes) != 48 {
|
||||
return nil, errors.New("public key should be 48 bytes")
|
||||
}
|
||||
pubKey, err := e2types.BLSPublicKeyFromBytes(pubKeyBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "invalid public key")
|
||||
}
|
||||
pubKeys[fmt.Sprintf("%x", pubKey.Marshal())] = true
|
||||
}
|
||||
}
|
||||
|
||||
return pubKeys, nil
|
||||
}
|
||||
|
||||
func depositDataFromJSON(input string) ([]*depositData, error) {
|
||||
var err error
|
||||
var data []byte
|
||||
// Input could be JSON or a path to JSON
|
||||
switch {
|
||||
case strings.HasPrefix(input, "{"):
|
||||
// Looks like JSON
|
||||
data = []byte("[" + input + "]")
|
||||
case strings.HasPrefix(input, "["):
|
||||
// Looks like JSON array
|
||||
data = []byte(input)
|
||||
default:
|
||||
// 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")
|
||||
}
|
||||
if data[0] == '{' {
|
||||
data = []byte("[" + string(data) + "]")
|
||||
}
|
||||
}
|
||||
var depositData []*depositData
|
||||
err = json.Unmarshal(data, &depositData)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "data is not valid JSON")
|
||||
}
|
||||
if len(depositData) == 0 {
|
||||
return nil, errors.New("no deposits supplied")
|
||||
}
|
||||
minVersion := depositData[0].Version
|
||||
maxVersion := depositData[0].Version
|
||||
for i := range depositData {
|
||||
if depositData[i].PublicKey == "" {
|
||||
return nil, fmt.Errorf("no public key for deposit %d", i)
|
||||
}
|
||||
if depositData[i].DepositDataRoot == "" {
|
||||
return nil, fmt.Errorf("no data root for deposit %d", i)
|
||||
}
|
||||
if depositData[i].Signature == "" {
|
||||
return nil, fmt.Errorf("no signature for deposit %d", i)
|
||||
}
|
||||
if depositData[i].WithdrawalCredentials == "" {
|
||||
return nil, fmt.Errorf("no withdrawal credentials for deposit %d", i)
|
||||
}
|
||||
if depositData[i].Value < 1000000000 {
|
||||
return nil, fmt.Errorf("Deposit amount too small for deposit %d", i)
|
||||
}
|
||||
if depositData[i].Version > maxVersion {
|
||||
maxVersion = depositData[i].Version
|
||||
}
|
||||
if depositData[i].Version < minVersion {
|
||||
minVersion = depositData[i].Version
|
||||
}
|
||||
}
|
||||
return depositData, nil
|
||||
}
|
||||
func init() {
|
||||
depositCmd.AddCommand(depositVerifyCmd)
|
||||
depositFlags(depositVerifyCmd)
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyData, "data", "", "JSON data, or path to JSON data")
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyWithdrawalPubKey, "withdrawalpubkey", "", "Public key of the account to which the validator funds will be withdrawn")
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyDepositValue, "depositvalue", "", "Value of the amount to be deposited")
|
||||
depositVerifyCmd.Flags().StringVar(&depositVerifyValidatorPubKey, "validatorpubkey", "", "Public key(s) of the account(s) that will be carrying out validation")
|
||||
}
|
||||
@@ -112,7 +112,10 @@ In quiet mode this will return 0 if the the data can be generated correctly, oth
|
||||
err := connect()
|
||||
errCheck(err, "Failed to connect to beacon node")
|
||||
config, err := grpc.FetchChainConfig(eth2GRPCConn)
|
||||
errCheck(err, "Failed to obtain chain configuration")
|
||||
if err != nil {
|
||||
outputIf(!quiet, "Could not connect to beacon node; supply a connection with --connection or provide a fork version with --forkversion to generate a deposit")
|
||||
os.Exit(_exitFailure)
|
||||
}
|
||||
genesisForkVersion, exists := config["GenesisForkVersion"]
|
||||
assert(exists, "Failed to obtain genesis fork version")
|
||||
forkVersion = genesisForkVersion.([]byte)
|
||||
|
||||
@@ -30,7 +30,7 @@ var versionCmd = &cobra.Command{
|
||||
|
||||
ethdo version.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("1.4.17")
|
||||
fmt.Println("1.4.20")
|
||||
if viper.GetBool("verbose") {
|
||||
buildInfo, ok := dbg.ReadBuildInfo()
|
||||
if ok {
|
||||
|
||||
@@ -14,18 +14,22 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wealdtech/go-bytesutil"
|
||||
"github.com/wealdtech/go-ecodec"
|
||||
wallet "github.com/wealdtech/go-eth2-wallet"
|
||||
)
|
||||
|
||||
var walletImportData string
|
||||
var walletImportPassphrase string
|
||||
var walletImportVerify bool
|
||||
|
||||
var walletImportCmd = &cobra.Command{
|
||||
Use: "import",
|
||||
@@ -52,8 +56,42 @@ In quiet mode this will return 0 if the wallet is imported successfully, otherwi
|
||||
importData, err := bytesutil.FromHexString(walletImportData)
|
||||
errCheck(err, "Failed to decode wallet data")
|
||||
|
||||
_, err = wallet.ImportWallet(importData, []byte(walletImportPassphrase))
|
||||
errCheck(err, "Failed to import wallet")
|
||||
if walletImportVerify {
|
||||
type accountInfo struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type walletInfo struct {
|
||||
ID uuid.UUID `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
type export struct {
|
||||
Wallet *walletInfo `json:"wallet"`
|
||||
Accounts []*accountInfo `json:"accounts"`
|
||||
}
|
||||
|
||||
data, err := ecodec.Decrypt(importData, []byte(walletImportPassphrase))
|
||||
errCheck(err, "Failed to decrypt wallet")
|
||||
ext := &export{}
|
||||
err = json.Unmarshal(data, ext)
|
||||
errCheck(err, "Failed to read wallet")
|
||||
|
||||
outputIf(!quiet, fmt.Sprintf("Wallet name: %s", ext.Wallet.Name))
|
||||
outputIf(!quiet, fmt.Sprintf("Wallet type: %s", ext.Wallet.Type))
|
||||
outputIf(verbose, fmt.Sprintf("Wallet UUID: %s", ext.Wallet.ID))
|
||||
if verbose {
|
||||
fmt.Printf("Wallet accounts:\n")
|
||||
for _, account := range ext.Accounts {
|
||||
outputIf(verbose, fmt.Sprintf(" %s", account.Name))
|
||||
}
|
||||
} else {
|
||||
outputIf(!quiet, fmt.Sprintf("Wallet accounts: %d", len(ext.Accounts)))
|
||||
}
|
||||
|
||||
} else {
|
||||
_, err = wallet.ImportWallet(importData, []byte(walletImportPassphrase))
|
||||
errCheck(err, "Failed to import wallet")
|
||||
}
|
||||
|
||||
os.Exit(_exitSuccess)
|
||||
},
|
||||
@@ -64,4 +102,5 @@ func init() {
|
||||
walletFlags(walletImportCmd)
|
||||
walletImportCmd.Flags().StringVar(&walletImportData, "importdata", "", "The data to import, or the name of a file to read")
|
||||
walletImportCmd.Flags().StringVar(&walletImportPassphrase, "importpassphrase", "", "Passphrase protecting the data to import")
|
||||
walletImportCmd.Flags().BoolVar(&walletImportVerify, "verify", false, "Verify the wallet can be imported, but do not import it")
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ $ ethdo wallet export --wallet="Personal wallet" --exportpassphrase="my export s
|
||||
`ethdo wallet import` imports a wallet and all of its accounts exported by `ethdo wallet export`. Options for importing a wallet include:
|
||||
- `importdata`: the data exported by `ethdo wallet export`
|
||||
- `importpassphrase`: the passphrase that was provided to `ethdo wallet export` to encrypt the data
|
||||
- `verify`: confirm information about the wallet import without importing it
|
||||
|
||||
```sh
|
||||
$ ethdo wallet import --importdata="0x01c7a27ad40d45b4ae5be5f..." --importpassphrase="my export secret"
|
||||
|
||||
1
go.mod
1
go.mod
@@ -26,6 +26,7 @@ require (
|
||||
github.com/tyler-smith/go-bip39 v1.0.2
|
||||
github.com/wealdtech/eth2-signer-api v1.3.0
|
||||
github.com/wealdtech/go-bytesutil v1.1.1
|
||||
github.com/wealdtech/go-ecodec v1.1.0
|
||||
github.com/wealdtech/go-eth2-types/v2 v2.4.0
|
||||
github.com/wealdtech/go-eth2-util v1.2.0
|
||||
github.com/wealdtech/go-eth2-wallet v1.10.0
|
||||
|
||||
Reference in New Issue
Block a user