mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ca4406a80 | ||
|
|
1ad82adf80 | ||
|
|
11eb440df2 |
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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 := ðpb.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 := ðpb.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 := ðpb.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 := ðpb.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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user