Compare commits

...

3 Commits

Author SHA1 Message Date
Jim McDonald
9ca4406a80 Updates for validator exit 2020-04-23 09:56:04 +01:00
Jim McDonald
1ad82adf80 Tidy up chain info output 2020-04-21 14:48:31 +01:00
Jim McDonald
11eb440df2 Update README with wallet locations 2020-04-19 11:44:01 +01:00
7 changed files with 225 additions and 53 deletions

View File

@@ -24,13 +24,19 @@ A command-line tool for managing common tasks in Ethereum 2.
GO111MODULE=on go get github.com/wealdtech/ethdo
```
If this does not work please see the [#troubleshooting](troubleshooting section).
## Usage
ethdo contains a large number of features that are useful for day-to-day interactions with the Ethereum 2 blockchain.
### Wallets and accounts
ethdo uses the [go-eth2-wallet](https://github.com/wealdtech/go-eth2-wallet) system to provide unified access to different wallet types.
ethdo uses the [go-eth2-wallet](https://github.com/wealdtech/go-eth2-wallet) system to provide unified access to different wallet types. When on the filesystem the locations of the created wallets and accounts are:
- for Linux: $HOME/.config/ethereum2/wallets
- for OSX: $HOME/Library/Application Support/ethereum2/wallets
- for Windows: %APPDATA%\ethereum2\wallets
All ethdo comands take the following parameters:

View File

@@ -43,10 +43,11 @@ In quiet mode this will return 0 if the chain information can be obtained, other
os.Exit(_exitSuccess)
}
fmt.Printf("Genesis time:\t\t%s\n", genesisTime.Format(time.UnixDate))
outputIf(verbose, fmt.Sprintf("Genesis fork version:\t%0x", config["GenesisForkVersion"].([]byte)))
outputIf(verbose, fmt.Sprintf("Seconds per slot:\t%v", config["SecondsPerSlot"].(uint64)))
outputIf(verbose, fmt.Sprintf("Slots per epoch:\t%v", config["SlotsPerEpoch"].(uint64)))
fmt.Printf("Genesis time: %s\n", genesisTime.Format(time.UnixDate))
outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesisTime.Unix()))
outputIf(verbose, fmt.Sprintf("Genesis fork version: %0x", config["GenesisForkVersion"].([]byte)))
outputIf(verbose, fmt.Sprintf("Seconds per slot: %v", config["SecondsPerSlot"].(uint64)))
outputIf(verbose, fmt.Sprintf("Slots per epoch: %v", config["SlotsPerEpoch"].(uint64)))
os.Exit(_exitSuccess)
},

View File

@@ -15,18 +15,27 @@ package cmd
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"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 validatorExitEpoch int64
var validatorExitKey string
var validatorExitJSON string
var validatorExitJSONOutput bool
var validatorExitCmd = &cobra.Command{
Use: "exit",
@@ -35,60 +44,120 @@ var validatorExitCmd = &cobra.Command{
ethdo validator exit --account=primary/validator --passphrase=secret
In quiet mode this will return 0 if the transaction has been sent, otherwise 1.`,
In quiet mode this will return 0 if the transaction has been generated, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
// Sanity checking and setup.
assert(rootAccount != "", "--account is required")
account, err := accountFromPath(rootAccount)
errCheck(err, "Failed to access account")
err = connect()
err := connect()
errCheck(err, "Failed to obtain connect to Ethereum 2 beacon chain node")
// Beacon chain config required for later work.
config, err := grpc.FetchChainConfig(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain configuration")
exit, signature := validatorExitHandleInput()
validatorExitHandleExit(exit, signature)
os.Exit(_exitSuccess)
},
}
// Fetch the validator's index.
index, err := grpc.FetchValidatorIndex(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator index")
outputIf(debug, fmt.Sprintf("Validator index is %d", index))
func validatorExitHandleInput() (*ethpb.VoluntaryExit, e2types.Signature) {
if validatorExitJSON != "" {
return validatorExitHandleJSONInput(validatorExitJSON)
}
if rootAccount != "" {
account, err := accountFromPath(rootAccount)
errCheck(err, "Failed to access account")
return validatorExitHandleAccountInput(account)
}
if validatorExitKey != "" {
privKeyBytes, err := hex.DecodeString(strings.TrimPrefix(validatorExitKey, "0x"))
errCheck(err, fmt.Sprintf("Failed to decode key %s", validatorExitKey))
account, err := util.NewScratchAccount(privKeyBytes, nil)
errCheck(err, "Invalid private key")
return validatorExitHandleAccountInput(account)
}
die("one of --json, --account or --key is required")
return nil, nil
}
// Ensure the validator is active.
state, err := grpc.FetchValidatorState(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator state")
outputIf(debug, fmt.Sprintf("Validator state is %v", state))
assert(state == ethpb.ValidatorStatus_ACTIVE, "Validator must be active to exit")
func validatorExitHandleJSONInput(input string) (*ethpb.VoluntaryExit, e2types.Signature) {
data := &validatorExitData{}
err := json.Unmarshal([]byte(input), data)
errCheck(err, "Invalid JSON input")
exit := &ethpb.VoluntaryExit{
Epoch: data.Epoch,
ValidatorIndex: data.ValidatorIndex,
}
signature, err := e2types.BLSSignatureFromBytes(data.Signature)
errCheck(err, "Invalid signature")
return exit, signature
}
func validatorExitHandleAccountInput(account e2wtypes.Account) (*ethpb.VoluntaryExit, e2types.Signature) {
exit := &ethpb.VoluntaryExit{}
// Beacon chain config required for later work.
config, err := grpc.FetchChainConfig(eth2GRPCConn)
errCheck(err, "Failed to obtain beacon chain configuration")
secondsPerEpoch := config["SecondsPerSlot"].(uint64) * config["SlotsPerEpoch"].(uint64)
// Fetch the validator's index.
index, err := grpc.FetchValidatorIndex(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator index")
outputIf(debug, fmt.Sprintf("Validator index is %d", index))
exit.ValidatorIndex = index
// Ensure the validator is active.
state, err := grpc.FetchValidatorState(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator state")
outputIf(debug, fmt.Sprintf("Validator state is %v", state))
assert(state == ethpb.ValidatorStatus_ACTIVE, "Validator must be active to exit")
if validatorExitEpoch < 0 {
// Ensure the validator has been active long enough to exit.
validator, err := grpc.FetchValidator(eth2GRPCConn, account)
errCheck(err, "Failed to obtain validator information")
outputIf(debug, fmt.Sprintf("Activation epoch is %v", validator.ActivationEpoch))
earliestExitEpoch := validator.ActivationEpoch + config["PersistentCommitteePeriod"].(uint64)
secondsPerEpoch := config["SecondsPerSlot"].(uint64) * config["SlotsPerEpoch"].(uint64)
genesisTime, err := grpc.FetchGenesis(eth2GRPCConn)
errCheck(err, "Failed to obtain genesis time")
currentEpoch := uint64(time.Since(genesisTime).Seconds()) / secondsPerEpoch
assert(currentEpoch >= earliestExitEpoch, fmt.Sprintf("Validator cannot exit until %s ( epoch %d)", genesisTime.Add(time.Duration(secondsPerEpoch*earliestExitEpoch)*time.Second).Format(time.UnixDate), earliestExitEpoch))
outputIf(verbose, "Validator confirmed to be in a suitable state")
exit.Epoch = currentEpoch
} else {
// User-specified epoch; no checks.
exit.Epoch = uint64(validatorExitEpoch)
}
// Set up the transaction.
exit := &ethpb.VoluntaryExit{
Epoch: currentEpoch,
ValidatorIndex: index,
// TODO fetch current fork version from config (currently using genesis fork version)
currentForkVersion := config["GenesisForkVersion"].([]byte)
// TODO fetch genesis validators root from API.
genesisValidatorsRoot := []byte{
0x55, 0x13, 0x8e, 0x46, 0xa2, 0x44, 0x2d, 0x2f,
0xfd, 0x89, 0x55, 0x0a, 0x0f, 0x30, 0x56, 0x21,
0x27, 0xbc, 0x56, 0xe6, 0x24, 0x4d, 0x0f, 0xa2,
0xb5, 0x18, 0xa3, 0xf4, 0xce, 0x19, 0x33, 0x7e,
}
domain := e2types.Domain(e2types.DomainVoluntaryExit, currentForkVersion, genesisValidatorsRoot)
err = account.Unlock([]byte(rootAccountPassphrase))
errCheck(err, "Failed to unlock account; please confirm passphrase is correct")
signature, err := signStruct(account, exit, domain)
errCheck(err, "Failed to sign exit proposal")
return exit, signature
}
// validatorExitHandleExit handles the exit request.
func validatorExitHandleExit(exit *ethpb.VoluntaryExit, signature e2types.Signature) {
if validatorExitJSONOutput {
data := &validatorExitData{
Epoch: exit.Epoch,
ValidatorIndex: exit.ValidatorIndex,
Signature: signature.Marshal(),
}
// TODO fetch current fork version from config (currently using genesis fork version)
currentForkVersion := config["GenesisForkVersion"].([]byte)
// TODO fetch genesis validators root from somewhere.
//domain := e2types.Domain(e2types.DomainVoluntaryExit, currentForkVersion, genesisValidatorsRoot)
domain := e2types.Domain(e2types.DomainVoluntaryExit, currentForkVersion, e2types.ZeroGenesisValidatorsRoot)
err = account.Unlock([]byte(rootAccountPassphrase))
errCheck(err, "Failed to unlock account; please confirm passphrase is correct")
signature, err := signStruct(account, exit, domain)
errCheck(err, "Failed to sign exit proposal")
res, err := json.Marshal(data)
errCheck(err, "Failed to generate JSON")
outputIf(!quiet, string(res))
} else {
proposal := &ethpb.SignedVoluntaryExit{
Exit: exit,
Signature: signature.Marshal(),
@@ -97,17 +166,75 @@ In quiet mode this will return 0 if the transaction has been sent, otherwise 1.`
validatorClient := ethpb.NewBeaconNodeValidatorClient(eth2GRPCConn)
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, err = validatorClient.ProposeExit(ctx, proposal)
_, err := validatorClient.ProposeExit(ctx, proposal)
errCheck(err, "Failed to propose exit")
outputIf(!quiet, "Validator exit transaction sent")
os.Exit(_exitSuccess)
},
}
}
func init() {
validatorCmd.AddCommand(validatorExitCmd)
validatorFlags(validatorExitCmd)
validatorExitCmd.Flags().Int64Var(&validatorExitEpoch, "epoch", -1, "Epoch at which to exit (defaults to now)")
validatorExitCmd.Flags().Int64Var(&validatorExitEpoch, "epoch", -1, "Epoch at which to exit (defaults to current epoch)")
validatorExitCmd.Flags().StringVar(&validatorExitKey, "key", "", "Private key if account not known by ethdo")
validatorExitCmd.Flags().BoolVar(&validatorExitJSONOutput, "json-output", false, "Print JSON transaction; do not broadcast to network")
validatorExitCmd.Flags().StringVar(&validatorExitJSON, "json", "", "Use JSON as created by --json-output to exit")
addTransactionFlags(validatorExitCmd)
}
type validatorExitData struct {
Epoch uint64 `json:"epoch"`
ValidatorIndex uint64 `json:"validator_index"`
Signature []byte `json:"signature"`
}
// 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
}
// UnmarshalJSON implements custom JSON unmarshaller.
func (d *validatorExitData) UnmarshalJSON(data []byte) error {
var v map[string]interface{}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
if val, exists := v["epoch"]; exists {
var ok bool
epoch, ok := val.(float64)
if !ok {
return errors.New("epoch invalid")
}
d.Epoch = uint64(epoch)
} else {
return errors.New("epoch missing")
}
if val, exists := v["validator_index"]; exists {
var ok bool
validatorIndex, ok := val.(float64)
if !ok {
return errors.New("validator_index invalid")
}
d.ValidatorIndex = uint64(validatorIndex)
} else {
return errors.New("validator_index missing")
}
if val, exists := v["signature"]; exists {
signatureBytes, ok := val.(string)
if !ok {
return errors.New("signature invalid")
}
signature, err := hex.DecodeString(strings.TrimPrefix(signatureBytes, "0x"))
if err != nil {
return errors.Wrap(err, "signature invalid")
}
d.Signature = signature
} else {
return errors.New("signature missing")
}
return nil
}

View File

@@ -52,7 +52,7 @@ In quiet mode this will return 0 if the validator information can be obtained, o
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(validatorInfoPubKey, "0x"))
errCheck(err, fmt.Sprintf("Failed to decode public key %s", validatorInfoPubKey))
account, err = util.NewScratchAccount(pubKeyBytes)
account, err = util.NewScratchAccount(nil, pubKeyBytes)
errCheck(err, fmt.Sprintf("Invalid public key %s", validatorInfoPubKey))
}

View File

@@ -30,7 +30,7 @@ var versionCmd = &cobra.Command{
ethdo version.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("1.4.2")
fmt.Println("1.4.3")
if viper.GetBool("verbose") {
buildInfo, ok := dbg.ReadBuildInfo()
if ok {

View File

@@ -339,12 +339,21 @@ Validator commands focus on interaction with Ethereum 2 validators.
#### `exit`
`ethdo validator exit` sends a transaction to the chain to tell an active validator to exit the validation queue.
`ethdo validator exit` sends a transaction to the chain to tell an active validator to exit the validation queue. Options include:
- `epoch` specify an epoch before which this exit is not valid
- `json-output` generate JSON output rather than sending a transaction immediately
- `json` use JSON input created by the `--json-output` option rather than generate data from scratch
```sh
$ ethdo validator exit --account=Validators/1 --passphrase="my validator secret"
```
To send a transaction when the account is not accessible to ethdo accout you can use the validator's private key instead:
```sh
$ ethdo validator exit --key=0x01e748d098d3bcb477d636f19d510399ae18205fadf9814ee67052f88c1f88c0
```
#### `info`
`ethdo validator info` provides information for a given validator.

View File

@@ -22,17 +22,38 @@ import (
// ScratchAccount is an account that exists temporarily.
type ScratchAccount struct {
id uuid.UUID
pubKey types.PublicKey
id uuid.UUID
privKey types.PrivateKey
pubKey types.PublicKey
unlocked bool
}
// NewScratchAccount creates a new local account.
func NewScratchAccount(pubKey []byte) (*ScratchAccount, error) {
func NewScratchAccount(privKey []byte, pubKey []byte) (*ScratchAccount, error) {
if len(privKey) > 0 {
return newScratchAccountFromPrivKey(privKey)
} else {
return newScratchAccountFromPubKey(privKey)
}
}
func newScratchAccountFromPrivKey(privKey []byte) (*ScratchAccount, error) {
key, err := types.BLSPrivateKeyFromBytes(privKey)
if err != nil {
return nil, err
}
return &ScratchAccount{
id: uuid.New(),
privKey: key,
pubKey: key.PublicKey(),
}, nil
}
func newScratchAccountFromPubKey(pubKey []byte) (*ScratchAccount, error) {
key, err := types.BLSPublicKeyFromBytes(pubKey)
if err != nil {
return nil, err
}
return &ScratchAccount{
id: uuid.New(),
pubKey: key,
@@ -56,16 +77,24 @@ func (a *ScratchAccount) Path() string {
}
func (a *ScratchAccount) Lock() {
a.unlocked = false
}
func (a *ScratchAccount) Unlock([]byte) error {
a.unlocked = true
return nil
}
func (a *ScratchAccount) IsUnlocked() bool {
return false
return a.unlocked
}
func (a *ScratchAccount) Sign(data []byte) (types.Signature, error) {
return nil, errors.New("Not implemented")
if !a.IsUnlocked() {
return nil, errors.New("locked")
}
if a.privKey == nil {
return nil, errors.New("no private key")
}
return a.privKey.Sign(data), nil
}