mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-07 21:24:01 -05:00
@@ -1,6 +1,7 @@
|
||||
dev:
|
||||
- initial support for deneb
|
||||
- add "--generate-keystore" option for "account derive"
|
||||
- update "validator exit" command to be able to generate multiple exits
|
||||
|
||||
1.30.0:
|
||||
- add "chain spec" command
|
||||
|
||||
@@ -42,7 +42,8 @@ type command struct {
|
||||
forkVersion string
|
||||
genesisValidatorsRoot string
|
||||
prepareOffline bool
|
||||
signedOperationInput string
|
||||
signedOperationsInput string
|
||||
epoch string
|
||||
|
||||
// Beacon node connection.
|
||||
timeout time.Duration
|
||||
@@ -58,7 +59,7 @@ type command struct {
|
||||
chainTime chaintime.Service
|
||||
|
||||
// Output.
|
||||
signedOperation *phase0.SignedVoluntaryExit
|
||||
signedOperations []*phase0.SignedVoluntaryExit
|
||||
}
|
||||
|
||||
func newCommand(_ context.Context) (*command, error) {
|
||||
@@ -76,10 +77,12 @@ func newCommand(_ context.Context) (*command, error) {
|
||||
mnemonic: viper.GetString("mnemonic"),
|
||||
path: viper.GetString("path"),
|
||||
privateKey: viper.GetString("private-key"),
|
||||
signedOperationInput: viper.GetString("signed-operation"),
|
||||
signedOperationsInput: viper.GetString("signed-operations"),
|
||||
validator: viper.GetString("validator"),
|
||||
forkVersion: viper.GetString("fork-version"),
|
||||
genesisValidatorsRoot: viper.GetString("genesis-validators-root"),
|
||||
epoch: viper.GetString("epoch"),
|
||||
signedOperations: make([]*phase0.SignedVoluntaryExit, 0),
|
||||
}
|
||||
|
||||
// Account and validator are synonymous.
|
||||
|
||||
@@ -33,15 +33,21 @@ func (c *command) output(_ context.Context) (string, error) {
|
||||
}
|
||||
|
||||
if c.json || c.offline {
|
||||
data, err := json.Marshal(c.signedOperation)
|
||||
var data []byte
|
||||
var err error
|
||||
if len(c.signedOperations) == 1 {
|
||||
data, err = json.Marshal(c.signedOperations[0])
|
||||
} else {
|
||||
data, err = json.Marshal(c.signedOperations)
|
||||
}
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal signed operation")
|
||||
return "", errors.Wrap(err, "failed to marshal signed operations")
|
||||
}
|
||||
if c.json {
|
||||
return string(data), nil
|
||||
}
|
||||
if err := os.WriteFile(exitOperationFilename, data, 0o600); err != nil {
|
||||
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", exitOperationFilename))
|
||||
if err := os.WriteFile(exitOperationsFilename, data, 0o600); err != nil {
|
||||
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", exitOperationsFilename))
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -48,7 +49,7 @@ var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$")
|
||||
|
||||
var (
|
||||
offlinePreparationFilename = "offline-preparation.json"
|
||||
exitOperationFilename = "exit-operation.json"
|
||||
exitOperationsFilename = "exit-operations.json"
|
||||
)
|
||||
|
||||
func (c *command) process(ctx context.Context) error {
|
||||
@@ -68,37 +69,41 @@ func (c *command) process(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.obtainOperation(ctx); err != nil {
|
||||
if err := c.obtainOperations(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if validated, reason := c.validateOperation(ctx); !validated {
|
||||
return fmt.Errorf("operation failed validation: %s", reason)
|
||||
if len(c.signedOperations) == 0 {
|
||||
return errors.New("no suitable validators found; no operations generated")
|
||||
}
|
||||
|
||||
if validated, reason := c.validateOperations(ctx); !validated {
|
||||
return fmt.Errorf("operations failed validation: %s", reason)
|
||||
}
|
||||
|
||||
if c.json || c.offline {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Not broadcasting exit operation\n")
|
||||
fmt.Fprintf(os.Stderr, "Not broadcasting exit operations\n")
|
||||
}
|
||||
// Want JSON output, or cannot broadcast.
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.broadcastOperation(ctx)
|
||||
return c.broadcastOperations(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperation(ctx context.Context) error {
|
||||
if (c.mnemonic == "" || c.path == "") && c.privateKey == "" && c.validator == "" {
|
||||
func (c *command) obtainOperations(ctx context.Context) error {
|
||||
if c.mnemonic == "" && c.privateKey == "" && c.validator == "" {
|
||||
// No input information; fetch the operation from a file.
|
||||
err := c.obtainOperationFromFileOrInput(ctx)
|
||||
err := c.obtainOperationsFromFileOrInput(ctx)
|
||||
if err == nil {
|
||||
// Success.
|
||||
return nil
|
||||
}
|
||||
if c.signedOperationInput != "" {
|
||||
if c.signedOperationsInput != "" {
|
||||
return errors.Wrap(err, "failed to obtain supplied signed operation")
|
||||
}
|
||||
return errors.Wrap(err, fmt.Sprintf("no account, mnemonic or private key specified, and no %s file loaded", exitOperationFilename))
|
||||
return errors.Wrap(err, fmt.Sprintf("no account, mnemonic or private key specified, and no %s file loaded", exitOperationsFilename))
|
||||
}
|
||||
|
||||
if c.mnemonic != "" {
|
||||
@@ -110,7 +115,8 @@ func (c *command) obtainOperation(ctx context.Context) error {
|
||||
// Have a mnemonic and validator.
|
||||
return c.generateOperationFromMnemonicAndValidator(ctx)
|
||||
default:
|
||||
return errors.New("mnemonic must be supplied with either a path or validator")
|
||||
// Have a mnemonic only.
|
||||
return c.generateOperationsFromMnemonic(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +132,12 @@ func (c *command) obtainOperation(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) error {
|
||||
// Turn the validators in to a map for easy lookup.
|
||||
validators := make(map[string]*beacon.ValidatorInfo, 0)
|
||||
for _, validator := range c.chainInfo.Validators {
|
||||
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
|
||||
}
|
||||
|
||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -137,9 +149,13 @@ func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) erro
|
||||
return fmt.Errorf("path %s does not match EIP-2334 format for a validator", c.path)
|
||||
}
|
||||
|
||||
if err := c.generateOperationFromSeedAndPath(ctx, seed, validatorKeyPath); err != nil {
|
||||
found, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate operation from seed and path")
|
||||
}
|
||||
if !found {
|
||||
return errors.New("no validator found with the provided path and mnemonic, please run with --debug to see more information")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -187,6 +203,49 @@ func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
|
||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Turn the validators in to a map for easy lookup.
|
||||
validators := make(map[string]*beacon.ValidatorInfo, 0)
|
||||
for _, validator := range c.chainInfo.Validators {
|
||||
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
|
||||
}
|
||||
|
||||
maxDistance := 1024
|
||||
// Start scanning the validator keys.
|
||||
lastFoundIndex := 0
|
||||
foundValidatorCount := 0
|
||||
for i := 0; ; i++ {
|
||||
// If no validators have been found in the last maxDistance indices, stop scanning.
|
||||
if i-lastFoundIndex > maxDistance {
|
||||
// If no validators were found at all, return an error.
|
||||
if foundValidatorCount == 0 {
|
||||
return fmt.Errorf("failed to find validators using the provided mnemonic: searched %d indices without finding a validator", maxDistance)
|
||||
}
|
||||
break
|
||||
}
|
||||
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
|
||||
|
||||
found, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath)
|
||||
if err != nil {
|
||||
// We log errors but keep going.
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Failed to generate for path %s: %v\n", validatorKeyPath, err.Error())
|
||||
}
|
||||
}
|
||||
if found {
|
||||
lastFoundIndex = i
|
||||
foundValidatorCount++
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromPrivateKey(ctx context.Context) error {
|
||||
validatorAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
|
||||
if err != nil {
|
||||
@@ -205,62 +264,104 @@ func (c *command) generateOperationFromValidator(ctx context.Context) error {
|
||||
return c.generateOperationFromAccount(ctx, validatorAccount)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationFromFileOrInput(ctx context.Context) error {
|
||||
// Start off by attempting to use the provided signed operation.
|
||||
if c.signedOperationInput != "" {
|
||||
return c.obtainOperationFromInput(ctx)
|
||||
func (c *command) obtainOperationsFromFileOrInput(ctx context.Context) error {
|
||||
// Start off by attempting to use the provided signed operations.
|
||||
if c.signedOperationsInput != "" {
|
||||
return c.obtainOperationsFromInput(ctx)
|
||||
}
|
||||
// If not, read it from the file with the standard name.
|
||||
return c.obtainOperationFromFile(ctx)
|
||||
return c.obtainOperationsFromFile(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationFromFile(ctx context.Context) error {
|
||||
_, err := os.Stat(exitOperationFilename)
|
||||
func (c *command) obtainOperationsFromFile(ctx context.Context) error {
|
||||
_, err := os.Stat(exitOperationsFilename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read exit operation file")
|
||||
return errors.Wrap(err, "failed to read exit operations file")
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "%s found; loading operation\n", exitOperationFilename)
|
||||
fmt.Fprintf(os.Stderr, "%s found; loading operations\n", exitOperationsFilename)
|
||||
}
|
||||
data, err := os.ReadFile(exitOperationFilename)
|
||||
data, err := os.ReadFile(exitOperationsFilename)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read exit operation file")
|
||||
return errors.Wrap(err, "failed to read exit operations file")
|
||||
}
|
||||
if err := json.Unmarshal(data, &c.signedOperation); err != nil {
|
||||
return errors.Wrap(err, "failed to parse exit operation file")
|
||||
if err := json.Unmarshal(data, &c.signedOperations); err != nil {
|
||||
return errors.Wrap(err, "failed to parse exit operations file")
|
||||
}
|
||||
|
||||
return c.verifySignedOperation(ctx, c.signedOperation)
|
||||
return c.verifySignedOperations(ctx)
|
||||
}
|
||||
|
||||
func (c *command) obtainOperationFromInput(ctx context.Context) error {
|
||||
if !strings.HasPrefix(c.signedOperationInput, "{") {
|
||||
func (c *command) obtainOperationsFromInput(ctx context.Context) error {
|
||||
if !strings.HasPrefix(c.signedOperationsInput, "{") &&
|
||||
!strings.HasPrefix(c.signedOperationsInput, "[") {
|
||||
// This looks like a file; read it in.
|
||||
data, err := os.ReadFile(c.signedOperationInput)
|
||||
data, err := os.ReadFile(c.signedOperationsInput)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read input file")
|
||||
}
|
||||
c.signedOperationInput = string(data)
|
||||
c.signedOperationsInput = string(data)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(c.signedOperationInput), &c.signedOperation); err != nil {
|
||||
if strings.HasPrefix(c.signedOperationsInput, "{") {
|
||||
// Single operation; put it in an array.
|
||||
c.signedOperationsInput = fmt.Sprintf("[%s]", c.signedOperationsInput)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(c.signedOperationsInput), &c.signedOperations); err != nil {
|
||||
return errors.Wrap(err, "failed to parse exit operation input")
|
||||
}
|
||||
|
||||
return c.verifySignedOperation(ctx, c.signedOperation)
|
||||
return c.verifySignedOperations(ctx)
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
|
||||
validators map[string]*beacon.ValidatorInfo,
|
||||
seed []byte,
|
||||
path string,
|
||||
) error {
|
||||
) (
|
||||
bool,
|
||||
error,
|
||||
) {
|
||||
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, path)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate validator private key")
|
||||
return false, errors.Wrap(err, "failed to generate validator private key")
|
||||
}
|
||||
|
||||
c.privateKey = fmt.Sprintf("%#x", validatorPrivkey.Marshal())
|
||||
return c.generateOperationFromPrivateKey(ctx)
|
||||
privateKey := fmt.Sprintf("%#x", validatorPrivkey.Marshal())
|
||||
|
||||
validatorInfo, exists := validators[fmt.Sprintf("%#x", validatorPrivkey.PublicKey().Marshal())]
|
||||
if !exists {
|
||||
return false, errors.New("unknown validator")
|
||||
}
|
||||
if validatorInfo.State == apiv1.ValidatorStateActiveExiting ||
|
||||
validatorInfo.State == apiv1.ValidatorStateActiveSlashed ||
|
||||
validatorInfo.State == apiv1.ValidatorStateExitedUnslashed ||
|
||||
validatorInfo.State == apiv1.ValidatorStateExitedSlashed ||
|
||||
validatorInfo.State == apiv1.ValidatorStateWithdrawalPossible ||
|
||||
validatorInfo.State == apiv1.ValidatorStateWithdrawalDone {
|
||||
return false, fmt.Errorf("validator is in state %v, not suitable to generate an exit", validatorInfo.State)
|
||||
}
|
||||
|
||||
validatorAccount, err := util.ParseAccount(ctx, privateKey, nil, true)
|
||||
if err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "no validator found at path %s: %v\n", path, err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
if validatorAccount == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := c.generateOperationFromAccount(ctx, validatorAccount); err != nil {
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "failed to generate operation at path %s: %v\n", path, err)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *command) generateOperationFromAccount(ctx context.Context,
|
||||
@@ -276,8 +377,40 @@ func (c *command) generateOperationFromAccount(ctx context.Context,
|
||||
return err
|
||||
}
|
||||
|
||||
c.signedOperation, err = c.createSignedOperation(ctx, info, account, c.chainInfo.Epoch)
|
||||
return err
|
||||
epoch, err := c.selectEpoch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Using %d for epoch\n", epoch)
|
||||
}
|
||||
|
||||
signedOperation, err := c.createSignedOperation(ctx, info, account, epoch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.signedOperations = append(c.signedOperations, signedOperation)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) selectEpoch() (phase0.Epoch, error) {
|
||||
if c.epoch == "" {
|
||||
// No user-supplied epoch; use the one from chain info.
|
||||
return c.chainInfo.Epoch, nil
|
||||
}
|
||||
|
||||
epoch, err := strconv.ParseInt(c.epoch, 10, 64)
|
||||
if err != nil {
|
||||
return 0, errors.New("epoch invalid")
|
||||
}
|
||||
|
||||
if epoch < 0 {
|
||||
// Relative epoch.
|
||||
return c.chainInfo.Epoch - phase0.Epoch(-epoch), nil
|
||||
}
|
||||
|
||||
return phase0.Epoch(epoch), nil
|
||||
}
|
||||
|
||||
func (c *command) createSignedOperation(ctx context.Context,
|
||||
@@ -322,6 +455,15 @@ func (c *command) createSignedOperation(ctx context.Context,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *command) verifySignedOperations(ctx context.Context) error {
|
||||
for _, op := range c.signedOperations {
|
||||
if err := c.verifySignedOperation(ctx, op); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) verifySignedOperation(ctx context.Context, op *phase0.SignedVoluntaryExit) error {
|
||||
root, err := op.Message.HashTreeRoot()
|
||||
if err != nil {
|
||||
@@ -366,14 +508,26 @@ func (c *command) verifySignedOperation(ctx context.Context, op *phase0.SignedVo
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) validateOperations(ctx context.Context) (bool, string) {
|
||||
for _, op := range c.signedOperations {
|
||||
valid, issue := c.validateOperation(ctx, op)
|
||||
if !valid {
|
||||
return valid, issue
|
||||
}
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (c *command) validateOperation(_ context.Context,
|
||||
op *phase0.SignedVoluntaryExit,
|
||||
) (
|
||||
bool,
|
||||
string,
|
||||
) {
|
||||
var validatorInfo *beacon.ValidatorInfo
|
||||
for _, chainValidatorInfo := range c.chainInfo.Validators {
|
||||
if chainValidatorInfo.Index == c.signedOperation.Message.ValidatorIndex {
|
||||
if chainValidatorInfo.Index == op.Message.ValidatorIndex {
|
||||
validatorInfo = chainValidatorInfo
|
||||
break
|
||||
}
|
||||
@@ -382,7 +536,7 @@ func (c *command) validateOperation(_ context.Context,
|
||||
return false, "validator not known on chain"
|
||||
}
|
||||
if c.debug {
|
||||
fmt.Fprintf(os.Stderr, "Validator exit operation: %v", c.signedOperation)
|
||||
fmt.Fprintf(os.Stderr, "Validator exit operation: %v", op)
|
||||
fmt.Fprintf(os.Stderr, "On-chain validator info: %v\n", validatorInfo)
|
||||
}
|
||||
|
||||
@@ -398,8 +552,20 @@ func (c *command) validateOperation(_ context.Context,
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (c *command) broadcastOperation(ctx context.Context) error {
|
||||
return c.consensusClient.(consensusclient.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, c.signedOperation)
|
||||
func (c *command) broadcastOperations(ctx context.Context) error {
|
||||
for _, op := range c.signedOperations {
|
||||
if c.debug {
|
||||
data, err := json.Marshal(op)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Broadcasting %s\n", string(data))
|
||||
}
|
||||
}
|
||||
if err := c.consensusClient.(consensusclient.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, op); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *command) setup(ctx context.Context) error {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/wealdtech/ethdo/beacon"
|
||||
@@ -95,7 +96,7 @@ func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperation)
|
||||
require.Equal(t, test.expected, test.command.signedOperations[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -187,7 +188,7 @@ func TestGenerateOperationFromMnemonicAndValidator(t *testing.T) {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperation)
|
||||
require.Equal(t, test.expected, test.command.signedOperations[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -198,29 +199,44 @@ func TestGenerateOperationFromSeedAndPath(t *testing.T) {
|
||||
|
||||
require.NoError(t, e2types.InitBLS())
|
||||
|
||||
validator0 := &beacon.ValidatorInfo{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
State: apiv1.ValidatorStateActiveOngoing,
|
||||
}
|
||||
validator1 := &beacon.ValidatorInfo{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
State: apiv1.ValidatorStateActiveOngoing,
|
||||
}
|
||||
validator2 := &beacon.ValidatorInfo{
|
||||
Index: 2,
|
||||
Pubkey: phase0.BLSPubKey{0xaf, 0x9c, 0xe4, 0x4f, 0x50, 0x14, 0x8d, 0xb4, 0x12, 0x19, 0x4a, 0xf0, 0xba, 0xf0, 0xba, 0xb3, 0x6b, 0xd5, 0xc3, 0xe0, 0xc4, 0x93, 0x89, 0x11, 0xa4, 0xe5, 0x02, 0xe3, 0x98, 0xb5, 0x9e, 0x5c, 0xca, 0x7c, 0x78, 0xe3, 0xfe, 0x03, 0x41, 0x95, 0x47, 0x88, 0x79, 0xee, 0xb2, 0x3d, 0xb0, 0xa6},
|
||||
WithdrawalCredentials: []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
State: apiv1.ValidatorStateActiveOngoing,
|
||||
}
|
||||
validator3 := &beacon.ValidatorInfo{
|
||||
Index: 3,
|
||||
Pubkey: phase0.BLSPubKey{0x86, 0xd3, 0x30, 0xaf, 0x51, 0xfa, 0x59, 0x3f, 0xa9, 0xf9, 0x3e, 0xdb, 0x9d, 0x16, 0x64, 0x01, 0x86, 0xbe, 0x2e, 0x93, 0xea, 0x94, 0xd2, 0x59, 0x78, 0x1e, 0x1e, 0xb3, 0x4d, 0xeb, 0x84, 0x4c, 0x39, 0x68, 0xd7, 0x5e, 0xa9, 0x1d, 0x19, 0xf1, 0x59, 0xdb, 0xd0, 0x52, 0x3c, 0x6c, 0x5b, 0xa5},
|
||||
WithdrawalCredentials: []byte{0x00, 0x81, 0x68, 0x45, 0x6b, 0x6d, 0x9a, 0x32, 0x83, 0x93, 0x1f, 0xea, 0x52, 0x10, 0xda, 0x12, 0x2d, 0x1e, 0x65, 0xe8, 0xed, 0x50, 0xb8, 0xe8, 0xf5, 0x91, 0x11, 0x83, 0xb0, 0x2f, 0xd1, 0x25},
|
||||
State: apiv1.ValidatorStateActiveOngoing,
|
||||
}
|
||||
|
||||
validators := map[string]*beacon.ValidatorInfo{
|
||||
"0xb384f767d964e100c8a9b21018d08c25ffebae268b3ab6d610353897541971726dbfc3c7463884c68a531515aab94c87": validator0,
|
||||
"0xb3d89e2f29c712c6a9f8e5a269b97617c4a94dd6f6662ab3b07ce9e5434573f15b5c988cd14bbd5804f77156a8af1cfa": validator1,
|
||||
"0xaf9ce44f50148db412194af0baf0bab36bd5c3e0c4938911a4e502e398b59e5cca7c78e3fe034195478879eeb23db0a6": validator2,
|
||||
"0x86d330af51fa593fa9f93edb9d16640186be2e93ea94d259781e1eb34deb844c3968d75ea91d19f159dbd0523c6c5ba5": validator3,
|
||||
}
|
||||
chainInfo := &beacon.ChainInfo{
|
||||
Version: 1,
|
||||
Validators: []*beacon.ValidatorInfo{
|
||||
{
|
||||
Index: 0,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0x84, 0xf7, 0x67, 0xd9, 0x64, 0xe1, 0x00, 0xc8, 0xa9, 0xb2, 0x10, 0x18, 0xd0, 0x8c, 0x25, 0xff, 0xeb, 0xae, 0x26, 0x8b, 0x3a, 0xb6, 0xd6, 0x10, 0x35, 0x38, 0x97, 0x54, 0x19, 0x71, 0x72, 0x6d, 0xbf, 0xc3, 0xc7, 0x46, 0x38, 0x84, 0xc6, 0x8a, 0x53, 0x15, 0x15, 0xaa, 0xb9, 0x4c, 0x87},
|
||||
WithdrawalCredentials: []byte{0x00, 0x8b, 0xa1, 0xcc, 0x4b, 0x09, 0x1b, 0x91, 0xc1, 0x20, 0x2b, 0xba, 0x3f, 0x50, 0x80, 0x75, 0xd6, 0xff, 0x56, 0x5c, 0x77, 0xe5, 0x59, 0xf0, 0x80, 0x3c, 0x07, 0x92, 0xe0, 0x30, 0x2b, 0xf1},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Pubkey: phase0.BLSPubKey{0xb3, 0xd8, 0x9e, 0x2f, 0x29, 0xc7, 0x12, 0xc6, 0xa9, 0xf8, 0xe5, 0xa2, 0x69, 0xb9, 0x76, 0x17, 0xc4, 0xa9, 0x4d, 0xd6, 0xf6, 0x66, 0x2a, 0xb3, 0xb0, 0x7c, 0xe9, 0xe5, 0x43, 0x45, 0x73, 0xf1, 0x5b, 0x5c, 0x98, 0x8c, 0xd1, 0x4b, 0xbd, 0x58, 0x04, 0xf7, 0x71, 0x56, 0xa8, 0xaf, 0x1c, 0xfa},
|
||||
WithdrawalCredentials: []byte{0x00, 0x78, 0x6c, 0xb0, 0x2e, 0xd2, 0x8e, 0x5f, 0xbb, 0x1f, 0x7f, 0x9e, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
{
|
||||
Index: 2,
|
||||
Pubkey: phase0.BLSPubKey{0xaf, 0x9c, 0xe4, 0x4f, 0x50, 0x14, 0x8d, 0xb4, 0x12, 0x19, 0x4a, 0xf0, 0xba, 0xf0, 0xba, 0xb3, 0x6b, 0xd5, 0xc3, 0xe0, 0xc4, 0x93, 0x89, 0x11, 0xa4, 0xe5, 0x02, 0xe3, 0x98, 0xb5, 0x9e, 0x5c, 0xca, 0x7c, 0x78, 0xe3, 0xfe, 0x03, 0x41, 0x95, 0x47, 0x88, 0x79, 0xee, 0xb2, 0x3d, 0xb0, 0xa6},
|
||||
WithdrawalCredentials: []byte{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x2b, 0x72, 0x69, 0x29, 0x06, 0xe6, 0xb1, 0x2c, 0xe4, 0x64, 0x39, 0x75, 0xe3, 0x2b, 0x51, 0x76, 0x91, 0xf2},
|
||||
},
|
||||
{
|
||||
Index: 3,
|
||||
Pubkey: phase0.BLSPubKey{0x86, 0xd3, 0x30, 0xaf, 0x51, 0xfa, 0x59, 0x3f, 0xa9, 0xf9, 0x3e, 0xdb, 0x9d, 0x16, 0x64, 0x01, 0x86, 0xbe, 0x2e, 0x93, 0xea, 0x94, 0xd2, 0x59, 0x78, 0x1e, 0x1e, 0xb3, 0x4d, 0xeb, 0x84, 0x4c, 0x39, 0x68, 0xd7, 0x5e, 0xa9, 0x1d, 0x19, 0xf1, 0x59, 0xdb, 0xd0, 0x52, 0x3c, 0x6c, 0x5b, 0xa5},
|
||||
WithdrawalCredentials: []byte{0x00, 0x81, 0x68, 0x45, 0x6b, 0x6d, 0x9a, 0x32, 0x83, 0x93, 0x1f, 0xea, 0x52, 0x10, 0xda, 0x12, 0x2d, 0x1e, 0x65, 0xe8, 0xed, 0x50, 0xb8, 0xe8, 0xf5, 0x91, 0x11, 0x83, 0xb0, 0x2f, 0xd1, 0x25},
|
||||
},
|
||||
validator0,
|
||||
validator1,
|
||||
validator2,
|
||||
validator3,
|
||||
},
|
||||
GenesisValidatorsRoot: phase0.Root{},
|
||||
Epoch: 1,
|
||||
@@ -296,12 +312,14 @@ func TestGenerateOperationFromSeedAndPath(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.generateOperationFromSeedAndPath(ctx, test.seed, test.path)
|
||||
found, err := test.command.generateOperationFromSeedAndPath(ctx, validators, test.seed, test.path)
|
||||
if test.err != "" {
|
||||
require.False(t, found)
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, test.command.signedOperation)
|
||||
require.True(t, found)
|
||||
require.Equal(t, test.expected, test.command.signedOperations[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -340,10 +358,12 @@ func TestVerifyOperation(t *testing.T) {
|
||||
name: "SignatureMissing",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
signedOperations: []*phase0.SignedVoluntaryExit{
|
||||
{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -353,12 +373,14 @@ func TestVerifyOperation(t *testing.T) {
|
||||
name: "SignatureShort",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
signedOperations: []*phase0.SignedVoluntaryExit{
|
||||
{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
},
|
||||
err: "invalid signature",
|
||||
@@ -367,12 +389,14 @@ func TestVerifyOperation(t *testing.T) {
|
||||
name: "SignatureIncorrect",
|
||||
command: &command{
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
signedOperations: []*phase0.SignedVoluntaryExit{
|
||||
{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x99, 0x78, 0xb4, 0x9c, 0x21, 0x60, 0x3f, 0x04, 0xa3, 0x04, 0x4e, 0x4c, 0x49, 0x0c, 0xb4, 0x68, 0x7c, 0x6e, 0x14, 0xc2, 0xda, 0xed, 0x25, 0x92, 0xe0, 0x02, 0x2d, 0xcd, 0x63, 0xeb, 0xe7, 0x4a, 0xf1, 0x1a, 0xca, 0xba, 0xae, 0x50, 0xe1, 0x8a, 0x1d, 0xae, 0x96, 0xd9, 0xd2, 0x56, 0xbf, 0x9f, 0x02, 0x48, 0x85, 0x05, 0xc1, 0xfb, 0xb3, 0x4a, 0x0b, 0x68, 0xec, 0xc5, 0xb5, 0xf5, 0xea, 0x53, 0xdb, 0xd0, 0x09, 0x08, 0xe3, 0x1e, 0xa8, 0xca, 0x9d, 0x02, 0x08, 0x3b, 0x9e, 0xf1, 0xc7, 0xd2, 0x32, 0xf4, 0xba, 0xd9, 0xea, 0x56, 0x4b, 0xc5, 0x87, 0xd5, 0x27, 0xb7, 0x74, 0x97, 0x8a, 0xee},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x99, 0x78, 0xb4, 0x9c, 0x21, 0x60, 0x3f, 0x04, 0xa3, 0x04, 0x4e, 0x4c, 0x49, 0x0c, 0xb4, 0x68, 0x7c, 0x6e, 0x14, 0xc2, 0xda, 0xed, 0x25, 0x92, 0xe0, 0x02, 0x2d, 0xcd, 0x63, 0xeb, 0xe7, 0x4a, 0xf1, 0x1a, 0xca, 0xba, 0xae, 0x50, 0xe1, 0x8a, 0x1d, 0xae, 0x96, 0xd9, 0xd2, 0x56, 0xbf, 0x9f, 0x02, 0x48, 0x85, 0x05, 0xc1, 0xfb, 0xb3, 0x4a, 0x0b, 0x68, 0xec, 0xc5, 0xb5, 0xf5, 0xea, 0x53, 0xdb, 0xd0, 0x09, 0x08, 0xe3, 0x1e, 0xa8, 0xca, 0x9d, 0x02, 0x08, 0x3b, 0x9e, 0xf1, 0xc7, 0xd2, 0x32, 0xf4, 0xba, 0xd9, 0xea, 0x56, 0x4b, 0xc5, 0x87, 0xd5, 0x27, 0xb7, 0x74, 0x97, 0x8a, 0xee},
|
||||
},
|
||||
},
|
||||
err: "signature does not verify",
|
||||
@@ -383,12 +407,14 @@ func TestVerifyOperation(t *testing.T) {
|
||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||
validator: "0",
|
||||
chainInfo: chainInfo,
|
||||
signedOperation: &phase0.SignedVoluntaryExit{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
signedOperations: []*phase0.SignedVoluntaryExit{
|
||||
{
|
||||
Message: &phase0.VoluntaryExit{
|
||||
Epoch: 1,
|
||||
ValidatorIndex: 0,
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
Signature: phase0.BLSSignature{0x89, 0xf5, 0xc4, 0x42, 0x88, 0xf9, 0x5e, 0x19, 0xb6, 0xc1, 0x39, 0xf2, 0x62, 0x30, 0x05, 0x66, 0x5b, 0x98, 0x34, 0x62, 0xa2, 0x28, 0x12, 0x09, 0x77, 0xd8, 0x1f, 0x2e, 0xf5, 0x47, 0x56, 0x0b, 0xe2, 0x24, 0x46, 0xde, 0x21, 0xa8, 0xa9, 0x37, 0xd9, 0xdd, 0xa4, 0xe2, 0xd2, 0xec, 0x41, 0x75, 0x19, 0x64, 0x96, 0xcd, 0xd1, 0x30, 0x6d, 0xec, 0x4a, 0x12, 0x5f, 0x8c, 0x86, 0x1f, 0x80, 0x61, 0x71, 0x50, 0x4a, 0x9d, 0x6a, 0x61, 0x0e, 0xc4, 0xe1, 0x35, 0x04, 0x7e, 0x4f, 0xb6, 0x70, 0x52, 0xec, 0xc4, 0x56, 0x13, 0x60, 0xd0, 0xc3, 0xde, 0x04, 0xb6, 0xfb, 0xc4, 0x47, 0x42, 0x23, 0xff},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -396,7 +422,7 @@ func TestVerifyOperation(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.verifySignedOperation(ctx, test.command.signedOperation)
|
||||
err := test.command.verifySignedOperation(ctx, test.command.signedOperations[0])
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
@@ -438,39 +464,39 @@ func TestObtainOperationFromInput(t *testing.T) {
|
||||
{
|
||||
name: "InvalidFilename",
|
||||
command: &command{
|
||||
signedOperationInput: `[]`,
|
||||
chainInfo: chainInfo,
|
||||
signedOperationsInput: `missing.json`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "failed to read input file: open []: no such file or directory",
|
||||
err: "failed to read input file: open missing.json: no such file or directory",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON",
|
||||
command: &command{
|
||||
signedOperationInput: `{invalid}`,
|
||||
chainInfo: chainInfo,
|
||||
signedOperationsInput: `{invalid}`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "failed to parse exit operation input: invalid character 'i' looking for beginning of object key string",
|
||||
},
|
||||
{
|
||||
name: "Unverifable",
|
||||
command: &command{
|
||||
signedOperationInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x9978b49c21603f04a3044e4c490cb4687c6e14c2daed2592e0022dcd63ebe74af11acabaae50e18a1dae96d9d256bf9f02488505c1fbb34a0b68ecc5b5f5ea53dbd00908e31ea8ca9d02083b9ef1c7d232f4bad9ea564bc587d527b774978aee"}`,
|
||||
chainInfo: chainInfo,
|
||||
signedOperationsInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x9978b49c21603f04a3044e4c490cb4687c6e14c2daed2592e0022dcd63ebe74af11acabaae50e18a1dae96d9d256bf9f02488505c1fbb34a0b68ecc5b5f5ea53dbd00908e31ea8ca9d02083b9ef1c7d232f4bad9ea564bc587d527b774978aee"}`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
err: "signature does not verify",
|
||||
},
|
||||
{
|
||||
name: "Good",
|
||||
command: &command{
|
||||
signedOperationInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x89f5c44288f95e19b6c139f2623005665b983462a228120977d81f2ef547560be22446de21a8a937d9dda4e2d2ec4175196496cdd1306dec4a125f8c861f806171504a9d6a610ec4e135047e4fb67052ecc4561360d0c3de04b6fbc4474223ff"}`,
|
||||
chainInfo: chainInfo,
|
||||
signedOperationsInput: `{"message":{"epoch":"1","validator_index":"0"},"signature":"0x89f5c44288f95e19b6c139f2623005665b983462a228120977d81f2ef547560be22446de21a8a937d9dda4e2d2ec4175196496cdd1306dec4a125f8c861f806171504a9d6a610ec4e135047e4fb67052ecc4561360d0c3de04b6fbc4474223ff"}`,
|
||||
chainInfo: chainInfo,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := test.command.obtainOperationFromInput(ctx)
|
||||
err := test.command.obtainOperationsFromFileOrInput(ctx)
|
||||
if test.err != "" {
|
||||
require.EqualError(t, err, test.err)
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright © 2020 Weald Technology Trading
|
||||
// Copyright © 2020, 2023 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
|
||||
@@ -23,13 +23,16 @@ import (
|
||||
|
||||
var validatorExitCmd = &cobra.Command{
|
||||
Use: "exit",
|
||||
Short: "Send an exit request for a validator",
|
||||
Long: `Send an exit request for a validator. For example:
|
||||
Short: "Send an exit request for one or more validators",
|
||||
Long: `Send an exit request for one or more validators. For example:
|
||||
|
||||
ethdo validator exit --validator=12345
|
||||
|
||||
The validator and key can be specified in one of a number of ways:
|
||||
|
||||
- mnemonic using --mnemonic; this will scan the mnemonic and generate all applicable operations
|
||||
- mnemonic and path to the validator key using --mnemonic and --path; this will generate a single operation
|
||||
- mnemonic and validator index or public key --mnemonic and --validator; this will generate a single operation
|
||||
- mnemonic and path to the validator using --mnemonic and --path
|
||||
- mnemonic and validator index or public key using --mnemonic and --validator
|
||||
- validator private key using --private-key
|
||||
@@ -57,7 +60,7 @@ func init() {
|
||||
validatorExitCmd.Flags().String("epoch", "", "Epoch at which to exit (defaults to current epoch)")
|
||||
validatorExitCmd.Flags().Bool("prepare-offline", false, "Create files for offline use")
|
||||
validatorExitCmd.Flags().String("validator", "", "Validator to exit")
|
||||
validatorExitCmd.Flags().String("signed-operation", "", "Use pre-defined JSON signed operation as created by --json to transmit the exit operation (reads from exit-operation.json if not present)")
|
||||
validatorExitCmd.Flags().String("signed-operations", "", "Use pre-defined JSON signed operation as created by --json to transmit the exit operations (reads from exit-operations.json if not present)")
|
||||
validatorExitCmd.Flags().Bool("offline", false, "Do not attempt to connect to a beacon node to obtain information for the operation")
|
||||
validatorExitCmd.Flags().String("fork-version", "", "Fork version to use for signing (overrides fetching from beacon node)")
|
||||
validatorExitCmd.Flags().String("genesis-validators-root", "", "Genesis validators root to use for signing (overrides fetching from beacon node)")
|
||||
@@ -73,7 +76,7 @@ func validatorExitBindings(cmd *cobra.Command) {
|
||||
if err := viper.BindPFlag("validator", cmd.Flags().Lookup("validator")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("signed-operation", cmd.Flags().Lookup("signed-operation")); err != nil {
|
||||
if err := viper.BindPFlag("signed-operations", cmd.Flags().Lookup("signed-operations")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := viper.BindPFlag("offline", cmd.Flags().Lookup("offline")); err != nil {
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
|
||||
// ReleaseVersion is the release version of the codebase.
|
||||
// Usually overridden by tag names when building binaries.
|
||||
var ReleaseVersion = "local build (latest release 1.30.0)"
|
||||
var ReleaseVersion = "local build (latest release 1.31.0)"
|
||||
|
||||
// versionCmd represents the version command.
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
204
docs/exitingvalidators.md
Normal file
204
docs/exitingvalidators.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# Exiting validators
|
||||
Exiting a validator relieves the validator of its duties and makes the initial deposit eligible for withdrawal. This document provides information on how to exit one or more validators given account information.
|
||||
|
||||
## Concepts
|
||||
The following concepts are useful when understanding the rest of this guide.
|
||||
|
||||
### Validator
|
||||
A validator is a logical entity that secures the Ethereum beacon chain (and hence the execution chain) by proposing blocks and attesting to blocks proposed by other validators.
|
||||
|
||||
### Private key
|
||||
A private key is a hexadecimal string (_e.g._ 0x010203…a1a2a3) that can be used to generate a public key and (in the case of the execution chain) Ethereum address.
|
||||
|
||||
### Mnemonic
|
||||
A mnemonic is a 24-word phrase that can be used to generate multiple private keys with the use of _paths_. Mnemonics are supported in the following languages:
|
||||
* chinese simplified
|
||||
* chinese traditional
|
||||
* czech
|
||||
* english
|
||||
* french
|
||||
* italian
|
||||
* japanese
|
||||
* korean
|
||||
* spanish
|
||||
|
||||
### Path
|
||||
A path is a string starting with "m" and containing a number of components separated by "/", for example "m/12381/3600/0/0". The process to obtain a key from a mnemonic and path is known as "hierarchical derivation".
|
||||
|
||||
### Online and Offline
|
||||
An _online_ computer is one that is is connected to the internet. It should be running a consensus node connected to the larger Ethereum network. An online computer is required to carry out the process, to obtain information from the consensus node and to broadcast your actions to the rest of the Ethereum network.
|
||||
|
||||
An _offline_ computer is one that is not connected to the internet. As such, it will not be running a consensus node. It can optionally be used in conjunction with an online computer to provide higher levels of security for your mnemonic or private key, but is less convenient because it requires manual transfer of files from the online computer to the offline computer, and back.
|
||||
|
||||
If you use your mnemonic when generating exit operations you should use the offline process. If you use a private key or keystore then the online process should be safe.
|
||||
|
||||
With only an online computer the flow of information is roughly as follows:
|
||||
|
||||

|
||||
|
||||
Here it can be seen that a copy of `ethdo` with access to private keys connects to a consensus node with access to the internet. Due to its connection to the internet it is possible that the computer on which `ethdo` and the consensus node runs has been compromised, and as such would expose the private keys to an attacker.
|
||||
|
||||
With both an offline and an online computer the flow of information is roughly as follows:
|
||||
|
||||

|
||||
|
||||
Here the copy of `ethdo` with access to private keys is on an offline computer, which protects it from being compromised via the internet. Data is physically moved from the offline to the online computer via a USB storage key or similar, and none of the information on the online computer is sensitive.
|
||||
|
||||
## Preparation
|
||||
Regardless of the method selected, preparation must take place on the online computer to ensure that `ethdo` can access your consensus node. `ethdo` will attempt to find a local consensus node automatically, but if not then an explicit connection value will be required. To find out if `ethdo` has access to the consensus node run:
|
||||
|
||||
```sh
|
||||
ethdo node info
|
||||
```
|
||||
|
||||
The result should be something similar to the following:
|
||||
|
||||
```
|
||||
Syncing: false
|
||||
```
|
||||
|
||||
Alternatively, the result may look like this:
|
||||
|
||||
```
|
||||
No connection supplied; using mainnet public access endpoint
|
||||
Syncing: false
|
||||
```
|
||||
|
||||
which means that a local consensus node was not accessed and instead a public endpoint specifically assigned to handle these operations was used instead. If you do have a local consensus node but see this message it means that the local node could not be accessed, usually because it is running on a non-standard port. If this is the case for your configuration, you need to let `ethdo` know where the consensus node's REST API is. For example, if your consensus node is serving its REST API on port 12345 then you should add `--connection=http://localhost:12345` to all `ethdo` commands in this process, for example:
|
||||
|
||||
```sh
|
||||
ethdo --connection=http://localhost:12345 node info
|
||||
```
|
||||
|
||||
Note that some consensus nodes may require configuration to serve their REST API. Please refer to the documentation of your specific consensus node to enable this.
|
||||
|
||||
Regardless of your method used above, it is important to confirm that the "Syncing" value is "false". If this is "true" it means that the node is currently syncing, and you will need to wait for the process to finish before proceeding.
|
||||
|
||||
Once the preparation is complete you should select either basic or advanced operation, depending on your requirements.
|
||||
|
||||
## Basic operation
|
||||
Given the above concepts, the purpose of this guide is to exit one or more active validators, allowing the initial deposit to be returned.
|
||||
|
||||
Basic operation is suitable in the majority of cases. If you:
|
||||
|
||||
- generated your validators using a mnemonic (_e.g._ using the deposit CLI or launchpad)
|
||||
- want to exit all of your validators at the same time
|
||||
|
||||
then this method is for you. If any of the above does not apply then please go to the "Advanced operation" section.
|
||||
|
||||
### Online process
|
||||
The online process generates and broadcasts the operations to exit all of your validators tied to a mnemonic in a single action.
|
||||
|
||||
One piece of information are required for carrying out this process online: the mnemonic.
|
||||
|
||||
On your _online_ computer run the following:
|
||||
|
||||
```
|
||||
ethdo validator exit --mnemonic="abandon abandon abandon … art"
|
||||
```
|
||||
|
||||
Replacing the `mnemonic` value with your own values. This command will:
|
||||
|
||||
1. obtain information from your consensus node about all currently-running validators and various additional information required to generate the operations
|
||||
2. scan your mnemonic to find any validators that were generated by it, and create the operations to exit
|
||||
3. broadcast the exit operations to the Ethereum network
|
||||
|
||||
### Online and Offline process
|
||||
The online and offline process contains three steps. In the first, data is gathered on the online computer. In the second, the exit operations are generated on the offline computer. In the third, the operations are broadcast on the online computer.
|
||||
|
||||
One piece of information are required for carrying out this process online: the mnemonic from which the validators were derived.
|
||||
|
||||
On your _online_ computer run the following:
|
||||
|
||||
```
|
||||
ethdo validator exit --prepare-offline
|
||||
```
|
||||
|
||||
This command will:
|
||||
|
||||
1. obtain information from your consensus node about all currently-running validators and various additional information required to generate the operations
|
||||
2. write this information to a file called `offline-preparation.json`
|
||||
|
||||
The `offline-preparation.json` file must be copied to your _offline_ computer. Once this has been done, on your _offline_ computer run the following:
|
||||
|
||||
```
|
||||
ethdo validator exit --offline --mnemonic="abandon abandon abandon … art"
|
||||
```
|
||||
|
||||
Replacing the `mnemonic` value with your own value. This command will:
|
||||
|
||||
1. read the `offline-preparation.json` file to obtain information about all currently-running validators and various additional information required to generate the operations
|
||||
2. scan your mnemonic to find any validators that were generated by it, and create the operations to exit
|
||||
3. write this information to a file called `exit-operations.json`
|
||||
|
||||
The `exit-operations.json` file must be copied to your _online_ computer. Once this has been done, on your _online_ computer run the following:
|
||||
|
||||
```
|
||||
ethdo validator exit
|
||||
```
|
||||
|
||||
This command will:
|
||||
|
||||
1. read the `exit-operations.json` file to obtain the operations to exit the validators
|
||||
2. broadcast the exit operations to the Ethereum network
|
||||
|
||||
## Advanced operation
|
||||
Advanced operation is required when any of the following conditions are met:
|
||||
|
||||
- your validators were created using something other than the deposit CLI or launchpad (_e.g._ `ethdo`)
|
||||
- you want to exit your validators individually
|
||||
|
||||
### Validator reference
|
||||
There are three options to reference a validator:
|
||||
|
||||
- the `ethdo` account of the validator (in format wallet/account)
|
||||
- the validator's public key (in format 0x…)
|
||||
- the validator's on-chain index (in format 123…)
|
||||
- the validator's keystore, either provided directly or as a path to the keystore on the local filesystem
|
||||
|
||||
Any of these can be passed to the following commands with the `--validator` parameter. You need to ensure that you have this information before starting the process.
|
||||
|
||||
**In the following examples we will use the validator with index 123. Please replace this with the reference to your validator in all commands.**
|
||||
|
||||
### Generating exit operations
|
||||
Note that if you are carrying out this process offline then you still need to carry out the first and third steps outlined in the "Basic operation" section above. This is to ensure that the offline computer has the correct information to generate the operations, and that the operations are made available to the online computer for broadcasting to the network.
|
||||
|
||||
If using the online and offline process run the commands below on the offline computer, and add the `--offline` flag to the commands below. You will need to copy the resultant `exit-operations.json` file to the online computer to broadcast to the network.
|
||||
|
||||
If using the online process run the commands below on the online computer. The operation will be broadcast to the network automatically.
|
||||
|
||||
#### Using a mnemonic and path.
|
||||
A mnemonic is a 24-word phrase from which withdrawal and validator keys are derived using a _path_. Commonly, keys will have been generated using the path m/12381/3600/_i_/0/0, where _i_ starts at 0 for the first validator, 1 for the second validator, _etc._
|
||||
|
||||
however this is only a standard and not a restriction, and it is possible for users to have created validators using paths of their own choice.
|
||||
|
||||
```
|
||||
ethdo validator exit --mnemonic="abandon abandon abandon … art" --path='m/12381/3600/0/0/0'
|
||||
```
|
||||
|
||||
replacing the path with the path to your validator key, and all other parameters with your own values.
|
||||
|
||||
#### Using a mnemonic and validator.
|
||||
Similar to the previous section, however instead of specifying a path instead the index, public key or account of the validator is provided.
|
||||
|
||||
```
|
||||
ethdo validator exit --mnemonic="abandon abandon abandon … art" --validator=123
|
||||
```
|
||||
|
||||
#### Using an account
|
||||
If you used `ethdo` to create your validator you can specify the accout of the validator to generate and broadcast the exit operation with the following command:
|
||||
|
||||
```
|
||||
ethdo validator exit --account=Wallet/Account --passphrase=secret
|
||||
```
|
||||
|
||||
replacing the parameters with your own values. Note that the passphrase here is the passphrsae of the validator account.
|
||||
|
||||
## Confirming the process has succeeded
|
||||
The final step is confirming the operation has taken place. To do so, run the following command on an online server:
|
||||
|
||||
```sh
|
||||
ethdo validator info --validator=123
|
||||
```
|
||||
|
||||
The result should show the state of the validator as exiting or exited.
|
||||
BIN
docs/images/exit-offline .png
Normal file
BIN
docs/images/exit-offline .png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
BIN
docs/images/exit-online.png
Normal file
BIN
docs/images/exit-online.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
@@ -650,12 +650,7 @@ $ ethdo validator credentials set --validator=Validators/1 --withdrawal-address=
|
||||
|
||||
#### `exit`
|
||||
|
||||
`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
|
||||
- `validator`: the validator for which to exit, as a [validator specifier](https://github.com/wealdtech/ethdo#validator-specifier)
|
||||
- `json` generate JSON output rather than sending a transaction immediately
|
||||
- `exit` use JSON exit input created by the `--json` option rather than generate data from scratch
|
||||
`ethdo validator exit` sends a transaction to the chain to tell an active validator to exit the validation queue. Full information about using this command can be found in the [specific documentation](./exitingvalidators.md).
|
||||
|
||||
```sh
|
||||
$ ethdo validator exit --validator=Validators/1 --passphrase="my validator secret"
|
||||
|
||||
Reference in New Issue
Block a user