Update code for spec change.

This commit is contained in:
Jim McDonald
2023-01-13 22:34:04 +00:00
parent 582930d982
commit ca5eec9c8c
41 changed files with 2504 additions and 1516 deletions

View File

@@ -1,6 +1,7 @@
dev:
1.27.0:
- use new build system
- support S3 credentials
- update operation of validator exit to match validator credentials set
1.26.5:
- provide validator information in "chain status" verbose output

300
beacon/chaininfo.go Normal file
View File

@@ -0,0 +1,300 @@
// Copyright © 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
//
// 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 beacon
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"strconv"
"strings"
consensusclient "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/services/chaintime"
"github.com/wealdtech/ethdo/util"
)
type ChainInfo struct {
Version uint64
Validators []*ValidatorInfo
GenesisValidatorsRoot phase0.Root
Epoch phase0.Epoch
GenesisForkVersion phase0.Version
CurrentForkVersion phase0.Version
BLSToExecutionChangeDomainType phase0.DomainType
VoluntaryExitDomainType phase0.DomainType
}
type chainInfoJSON struct {
Version string `json:"version"`
Validators []*ValidatorInfo `json:"validators"`
GenesisValidatorsRoot string `json:"genesis_validators_root"`
Epoch string `json:"epoch"`
GenesisForkVersion string `json:"genesis_fork_version"`
CurrentForkVersion string `json:"current_fork_version"`
BLSToExecutionChangeDomainType string `json:"bls_to_execution_change_domain_type"`
VoluntaryExitDomainType string `json:"voluntary_exit_domain_type"`
}
type chainInfoVersionJSON struct {
Version string `json:"version"`
}
// MarshalJSON implements json.Marshaler.
func (c *ChainInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(&chainInfoJSON{
Version: fmt.Sprintf("%d", c.Version),
Validators: c.Validators,
GenesisValidatorsRoot: fmt.Sprintf("%#x", c.GenesisValidatorsRoot),
Epoch: fmt.Sprintf("%d", c.Epoch),
GenesisForkVersion: fmt.Sprintf("%#x", c.GenesisForkVersion),
CurrentForkVersion: fmt.Sprintf("%#x", c.CurrentForkVersion),
BLSToExecutionChangeDomainType: fmt.Sprintf("%#x", c.BLSToExecutionChangeDomainType),
VoluntaryExitDomainType: fmt.Sprintf("%#x", c.VoluntaryExitDomainType),
})
}
// UnmarshalJSON implements json.Unmarshaler.
func (c *ChainInfo) UnmarshalJSON(input []byte) error {
// See which version we are dealing with.
var metadata chainInfoVersionJSON
if err := json.Unmarshal(input, &metadata); err != nil {
return errors.Wrap(err, "invalid JSON")
}
if metadata.Version == "" {
return errors.New("version missing")
}
version, err := strconv.ParseUint(metadata.Version, 10, 64)
if err != nil {
return errors.Wrap(err, "version invalid")
}
if version < 2 {
return errors.New("outdated version; please regenerate your offline data")
}
c.Version = version
var data chainInfoJSON
if err := json.Unmarshal(input, &data); err != nil {
return errors.Wrap(err, "invalid JSON")
}
if len(data.Validators) == 0 {
return errors.New("validators missing")
}
c.Validators = data.Validators
if data.GenesisValidatorsRoot == "" {
return errors.New("genesis validators root missing")
}
genesisValidatorsRootBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisValidatorsRoot, "0x"))
if err != nil {
return errors.Wrap(err, "genesis validators root invalid")
}
if len(genesisValidatorsRootBytes) != phase0.RootLength {
return errors.New("genesis validators root incorrect length")
}
copy(c.GenesisValidatorsRoot[:], genesisValidatorsRootBytes)
if data.Epoch == "" {
return errors.New("epoch missing")
}
epoch, err := strconv.ParseUint(data.Epoch, 10, 64)
if err != nil {
return errors.Wrap(err, "epoch invalid")
}
c.Epoch = phase0.Epoch(epoch)
if data.GenesisForkVersion == "" {
return errors.New("genesis fork version missing")
}
genesisForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisForkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "genesis fork version invalid")
}
if len(genesisForkVersionBytes) != phase0.ForkVersionLength {
return errors.New("genesis fork version incorrect length")
}
copy(c.GenesisForkVersion[:], genesisForkVersionBytes)
if data.CurrentForkVersion == "" {
return errors.New("current fork version missing")
}
currentForkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.CurrentForkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "current fork version invalid")
}
if len(currentForkVersionBytes) != phase0.ForkVersionLength {
return errors.New("current fork version incorrect length")
}
copy(c.CurrentForkVersion[:], currentForkVersionBytes)
if data.BLSToExecutionChangeDomainType == "" {
return errors.New("bls to execution domain type missing")
}
blsToExecutionChangeDomainType, err := hex.DecodeString(strings.TrimPrefix(data.BLSToExecutionChangeDomainType, "0x"))
if err != nil {
return errors.Wrap(err, "bls to execution domain type invalid")
}
if len(blsToExecutionChangeDomainType) != phase0.DomainTypeLength {
return errors.New("bls to execution domain type incorrect length")
}
copy(c.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType)
if data.VoluntaryExitDomainType == "" {
return errors.New("voluntary exit domain type missing")
}
voluntaryExitDomainType, err := hex.DecodeString(strings.TrimPrefix(data.VoluntaryExitDomainType, "0x"))
if err != nil {
return errors.Wrap(err, "voluntary exit domain type invalid")
}
if len(voluntaryExitDomainType) != phase0.DomainTypeLength {
return errors.New("voluntary exit domain type incorrect length")
}
copy(c.VoluntaryExitDomainType[:], voluntaryExitDomainType)
return nil
}
// FetchValidatorInfo fetches validator info given a validator identifier.
func (c *ChainInfo) FetchValidatorInfo(ctx context.Context, id string) (*ValidatorInfo, error) {
var validatorInfo *ValidatorInfo
switch {
case id == "":
return nil, errors.New("no validator specified")
case strings.HasPrefix(id, "0x"):
// A public key.
for _, validator := range c.Validators {
if strings.EqualFold(id, fmt.Sprintf("%#x", validator.Pubkey)) {
validatorInfo = validator
break
}
}
case strings.Contains(id, "/"):
// An account.
_, account, err := util.WalletAndAccountFromPath(ctx, id)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain account")
}
accPubKey, err := util.BestPublicKey(account)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain public key for account")
}
pubkey := fmt.Sprintf("%#x", accPubKey.Marshal())
for _, validator := range c.Validators {
if strings.EqualFold(pubkey, fmt.Sprintf("%#x", validator.Pubkey)) {
validatorInfo = validator
break
}
}
default:
// An index.
index, err := strconv.ParseUint(id, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse validator index")
}
validatorIndex := phase0.ValidatorIndex(index)
for _, validator := range c.Validators {
if validator.Index == validatorIndex {
validatorInfo = validator
break
}
}
}
if validatorInfo == nil {
return nil, errors.New("unknown validator")
}
return validatorInfo, nil
}
// ObtainChainInfoFromNode obtains the chain information from a node.
func ObtainChainInfoFromNode(ctx context.Context,
consensusClient consensusclient.Service,
chainTime chaintime.Service,
) (
*ChainInfo,
error,
) {
res := &ChainInfo{
Version: 2,
Validators: make([]*ValidatorInfo, 0),
Epoch: chainTime.CurrentEpoch(),
}
// Obtain validators.
validators, err := consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, "head", nil)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validators {
res.Validators = append(res.Validators, &ValidatorInfo{
Index: validator.Index,
Pubkey: validator.Validator.PublicKey,
WithdrawalCredentials: validator.Validator.WithdrawalCredentials,
State: validator.Status,
})
}
// Genesis validators root obtained from beacon node.
genesis, err := consensusClient.(consensusclient.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain genesis information")
}
res.GenesisValidatorsRoot = genesis.GenesisValidatorsRoot
// Fetch the genesis fork version from the specification.
spec, err := consensusClient.(consensusclient.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["GENESIS_FORK_VERSION"]
if !exists {
return nil, errors.New("capella fork version not known by chain")
}
var isForkVersion bool
res.GenesisForkVersion, isForkVersion = tmp.(phase0.Version)
if !isForkVersion {
return nil, errors.New("could not obtain GENESIS_FORK_VERSION")
}
// Fetch the current fork version from the fork schedule.
forkSchedule, err := consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain fork schedule")
}
for i := range forkSchedule {
if forkSchedule[i].Epoch <= res.Epoch {
res.CurrentForkVersion = forkSchedule[i].CurrentVersion
}
}
blsToExecutionChangeDomainType, exists := spec["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
if !exists {
return nil, errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
}
copy(res.BLSToExecutionChangeDomainType[:], blsToExecutionChangeDomainType[:])
voluntaryExitDomainType, exists := spec["DOMAIN_VOLUNTARY_EXIT"].(phase0.DomainType)
if !exists {
return nil, errors.New("failed to obtain DOMAIN_VOLUNTARY_EXIT")
}
copy(res.VoluntaryExitDomainType[:], voluntaryExitDomainType[:])
return res, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 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
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package validatorcredentialsset
package beacon
import (
"encoding/hex"
@@ -20,33 +20,37 @@ import (
"strconv"
"strings"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)
type validatorInfo struct {
type ValidatorInfo struct {
Index phase0.ValidatorIndex
Pubkey phase0.BLSPubKey
State apiv1.ValidatorState
WithdrawalCredentials []byte
}
type validatorInfoJSON struct {
Index string `json:"index"`
Pubkey string `json:"pubkey"`
WithdrawalCredentials string `json:"withdrawal_credentials"`
Index string `json:"index"`
Pubkey string `json:"pubkey"`
State apiv1.ValidatorState `json:"state"`
WithdrawalCredentials string `json:"withdrawal_credentials"`
}
// MarshalJSON implements json.Marshaler.
func (v *validatorInfo) MarshalJSON() ([]byte, error) {
func (v *ValidatorInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(&validatorInfoJSON{
Index: fmt.Sprintf("%d", v.Index),
Pubkey: fmt.Sprintf("%#x", v.Pubkey),
State: v.State,
WithdrawalCredentials: fmt.Sprintf("%#x", v.WithdrawalCredentials),
})
}
// UnmarshalJSON implements json.Unmarshaler.
func (v *validatorInfo) UnmarshalJSON(input []byte) error {
func (v *ValidatorInfo) UnmarshalJSON(input []byte) error {
var data validatorInfoJSON
if err := json.Unmarshal(input, &data); err != nil {
return errors.Wrap(err, "invalid JSON")
@@ -73,6 +77,11 @@ func (v *validatorInfo) UnmarshalJSON(input []byte) error {
}
copy(v.Pubkey[:], pubkey)
if data.State == apiv1.ValidatorStateUnknown {
return errors.New("state unknown")
}
v.State = data.State
if data.WithdrawalCredentials == "" {
return errors.New("withdrawal credentials missing")
}
@@ -88,7 +97,7 @@ func (v *validatorInfo) UnmarshalJSON(input []byte) error {
}
// String implements the Stringer interface.
func (v *validatorInfo) String() string {
func (v *ValidatorInfo) String() string {
data, err := json.Marshal(v)
if err != nil {
return fmt.Sprintf("Err: %v\n", err)

View File

@@ -35,7 +35,6 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) {
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(data.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -266,7 +266,6 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -121,7 +121,6 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -65,7 +65,6 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -46,7 +46,6 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithForkScheduleProvider(eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
)
errCheck(err, "Failed to configure chaintime service")

View File

@@ -335,7 +335,6 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -53,7 +53,6 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -1,4 +1,4 @@
// Copyright © 2017-2020 Weald Technology Trading
// Copyright © 2017-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
@@ -24,6 +24,7 @@ import (
"github.com/wealdtech/ethdo/util"
"github.com/wealdtech/go-bytesutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// signatureSignCmd represents the signature sign command
@@ -52,14 +53,20 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
}
outputIf(debug, fmt.Sprintf("Domain is %#x", domain))
assert(viper.GetString("account") != "", "--account is required")
_, account, err := walletAndAccountFromInput(ctx)
var account e2wtypes.Account
switch {
case viper.GetString("account") != "":
account, err = util.ParseAccount(ctx, viper.GetString("account"), util.GetPassphrases(), true)
case viper.GetString("private-key") != "":
account, err = util.ParseAccount(ctx, viper.GetString("private-key"), nil, true)
}
errCheck(err, "Failed to obtain account")
var specDomain spec.Domain
copy(specDomain[:], domain)
var fixedSizeData [32]byte
copy(fixedSizeData[:], data)
fmt.Printf("Signing %#x with domain %#x by public key %#x\n", fixedSizeData, specDomain, account.PublicKey().Marshal())
signature, err := util.SignRoot(account, fixedSizeData, specDomain)
errCheck(err, "Failed to sign")

View File

@@ -1,4 +1,4 @@
// Copyright © 2017-2020 Weald Technology Trading
// Copyright © 2017-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
@@ -15,13 +15,10 @@ package cmd
import (
"context"
"encoding/hex"
"fmt"
"os"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
@@ -43,6 +40,9 @@ var signatureVerifyCmd = &cobra.Command{
In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
assert(viper.GetString("signature-data") != "", "--data is required")
data, err := bytesutil.FromHexString(viper.GetString("signature-data"))
errCheck(err, "Failed to parse data")
@@ -61,7 +61,15 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
assert(len(domain) == 32, "Domain data invalid")
}
account, err := signatureVerifyAccount()
var account e2wtypes.Account
switch {
case viper.GetString("account") != "":
account, err = util.ParseAccount(ctx, viper.GetString("account"), nil, false)
case viper.GetString("private-key") != "":
account, err = util.ParseAccount(ctx, viper.GetString("private-key"), nil, false)
case viper.GetString("public-key") != "":
account, err = util.ParseAccount(ctx, viper.GetString("public-key"), nil, false)
}
errCheck(err, "Failed to obtain account")
outputIf(debug, fmt.Sprintf("Public key is %#x", account.PublicKey().Marshal()))
@@ -78,29 +86,6 @@ In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
},
}
// signatureVerifyAccount obtains the account for the signature verify command.
func signatureVerifyAccount() (e2wtypes.Account, error) {
var account e2wtypes.Account
var err error
if viper.GetString("account") != "" {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
_, account, err = walletAndAccountFromPath(ctx, viper.GetString("account"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain account")
}
} else {
pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(signatureVerifySigner, "0x"))
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", signatureVerifySigner))
}
account, err = util.NewScratchAccount(nil, pubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", signatureVerifySigner))
}
}
return account, nil
}
func init() {
signatureCmd.AddCommand(signatureVerifyCmd)
signatureFlags(signatureVerifyCmd)

View File

@@ -114,7 +114,6 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -59,7 +59,6 @@ func input(ctx context.Context) (*dataIn, error) {
// Chain time.
data.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithForkScheduleProvider(data.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
)
if err != nil {

View File

@@ -37,7 +37,6 @@ func TestProcess(t *testing.T) {
chainTime, err := standardchaintime.New(context.Background(),
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
standardchaintime.WithForkScheduleProvider(eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
)
require.NoError(t, err)

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 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
@@ -14,114 +14,92 @@
package validatorcredentialsset
import (
"encoding/hex"
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"os"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/beacon"
)
type chainInfo struct {
Version uint64
Validators []*validatorInfo
GenesisValidatorsRoot phase0.Root
Epoch phase0.Epoch
ForkVersion phase0.Version
Domain phase0.Domain
}
type chainInfoJSON struct {
Version string `json:"version"`
Validators []*validatorInfo `json:"validators"`
GenesisValidatorsRoot string `json:"genesis_validators_root"`
Epoch string `json:"epoch"`
ForkVersion string `json:"fork_version"`
Domain string `json:"domain"`
}
// MarshalJSON implements json.Marshaler.
func (v *chainInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(&chainInfoJSON{
Version: fmt.Sprintf("%d", v.Version),
Validators: v.Validators,
GenesisValidatorsRoot: fmt.Sprintf("%#x", v.GenesisValidatorsRoot),
Epoch: fmt.Sprintf("%d", v.Epoch),
ForkVersion: fmt.Sprintf("%#x", v.ForkVersion),
Domain: fmt.Sprintf("%#x", v.Domain),
})
}
// UnmarshalJSON implements json.Unmarshaler.
func (v *chainInfo) UnmarshalJSON(input []byte) error {
var data chainInfoJSON
if err := json.Unmarshal(input, &data); err != nil {
return errors.Wrap(err, "invalid JSON")
}
if data.Version == "" {
// Default to 1.
v.Version = 1
} else {
version, err := strconv.ParseUint(data.Version, 10, 64)
if err != nil {
return errors.Wrap(err, "version invalid")
// obtainChainInfo obtains the chain information required to create a withdrawal credentials change operation.
func (c *command) obtainChainInfo(ctx context.Context) error {
// Use the offline preparation file if present (and we haven't been asked to recreate it).
if !c.prepareOffline {
err := c.obtainChainInfoFromFile(ctx)
if err == nil {
return nil
}
v.Version = version
}
if len(data.Validators) == 0 {
return errors.New("validators missing")
}
v.Validators = data.Validators
if data.GenesisValidatorsRoot == "" {
return errors.New("genesis validators root missing")
if c.offline {
return fmt.Errorf("%s is unavailable or outdated; this is required to have been previously generated using --offline-preparation on an online machine and be readable in the directory in which this command is being run", offlinePreparationFilename)
}
genesisValidatorsRootBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisValidatorsRoot, "0x"))
if err != nil {
return errors.Wrap(err, "genesis validators root invalid")
if err := c.obtainChainInfoFromNode(ctx); err != nil {
return err
}
return nil
}
// obtainChainInfoFromFile obtains chain information from a pre-generated file.
func (c *command) obtainChainInfoFromFile(_ context.Context) error {
_, err := os.Stat(offlinePreparationFilename)
if err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "Failed to read offline preparation file: %v\n", err)
}
return errors.Wrap(err, fmt.Sprintf("cannot find %s", offlinePreparationFilename))
}
if c.debug {
fmt.Fprintf(os.Stderr, "%s found; loading chain state\n", offlinePreparationFilename)
}
data, err := os.ReadFile(offlinePreparationFilename)
if err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "failed to load chain state: %v\n", err)
}
return errors.Wrap(err, "failed to read offline preparation file")
}
c.chainInfo = &beacon.ChainInfo{}
if err := json.Unmarshal(data, c.chainInfo); err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "chain state invalid: %v\n", err)
}
return errors.Wrap(err, "failed to parse offline preparation file")
}
return nil
}
// obtainChainInfoFromNode obtains chain info from a beacon node.
func (c *command) obtainChainInfoFromNode(ctx context.Context) error {
if c.debug {
fmt.Fprintf(os.Stderr, "Populating chain info from beacon node\n")
}
var err error
c.chainInfo, err = beacon.ObtainChainInfoFromNode(ctx, c.consensusClient, c.chainTime)
if err != nil {
return err
}
return nil
}
// writeChainInfoToFile prepares for an offline run of this command by dumping
// the chain information to a file.
func (c *command) writeChainInfoToFile(_ context.Context) error {
data, err := json.Marshal(c.chainInfo)
if err != nil {
return err
}
if err := os.WriteFile(offlinePreparationFilename, data, 0600); err != nil {
return err
}
if len(genesisValidatorsRootBytes) != phase0.RootLength {
return errors.New("genesis validators root incorrect length")
}
copy(v.GenesisValidatorsRoot[:], genesisValidatorsRootBytes)
if data.Epoch == "" {
return errors.New("epoch missing")
}
epoch, err := strconv.ParseUint(data.Epoch, 10, 64)
if err != nil {
return errors.Wrap(err, "epoch invalid")
}
v.Epoch = phase0.Epoch(epoch)
if data.ForkVersion == "" {
return errors.New("fork version missing")
}
forkVersionBytes, err := hex.DecodeString(strings.TrimPrefix(data.ForkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "fork version invalid")
}
if len(forkVersionBytes) != phase0.ForkVersionLength {
return errors.New("fork version incorrect length")
}
copy(v.ForkVersion[:], forkVersionBytes)
if data.Domain == "" {
return errors.New("domain missing")
}
domainBytes, err := hex.DecodeString(strings.TrimPrefix(data.Domain, "0x"))
if err != nil {
return errors.Wrap(err, "domain invalid")
}
if len(domainBytes) != phase0.DomainLength {
return errors.New("domain incorrect length")
}
copy(v.Domain[:], domainBytes)
return nil
}

View File

@@ -20,8 +20,10 @@ import (
consensusclient "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
capella "github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/beacon"
"github.com/wealdtech/ethdo/services/chaintime"
"github.com/wealdtech/ethdo/util"
)
@@ -54,7 +56,8 @@ type command struct {
// Information required to generate the operations.
withdrawalAddress bellatrix.ExecutionAddress
chainInfo *chainInfo
chainInfo *beacon.ChainInfo
domain phase0.Domain
// Processing.
consensusClient consensusclient.Service

View File

@@ -28,6 +28,10 @@ func (c *command) output(_ context.Context) (string, error) {
return "", nil
}
if c.prepareOffline {
return fmt.Sprintf("%s generated", offlinePreparationFilename), nil
}
if c.json || c.offline {
data, err := json.Marshal(c.signedOperations)
if err != nil {

View File

@@ -1,4 +1,4 @@
// Copyright © 2022 Weald Technology Trading.
// Copyright © 2022, 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
@@ -21,7 +21,6 @@ import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
consensusclient "github.com/attestantio/go-eth2-client"
@@ -30,6 +29,7 @@ import (
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-ssz"
"github.com/wealdtech/ethdo/beacon"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/signing"
"github.com/wealdtech/ethdo/util"
@@ -49,15 +49,19 @@ func (c *command) process(ctx context.Context) error {
return err
}
if err := c.obtainRequiredInformation(ctx); err != nil {
if err := c.obtainChainInfo(ctx); err != nil {
return err
}
if c.prepareOffline {
return c.dumpRequiredInformation(ctx)
return c.writeChainInfoToFile(ctx)
}
if err := c.generateOperations(ctx); err != nil {
if err := c.generateDomain(ctx); err != nil {
return err
}
if err := c.obtainOperations(ctx); err != nil {
return err
}
@@ -76,204 +80,28 @@ func (c *command) process(ctx context.Context) error {
return c.broadcastOperations(ctx)
}
// obtainRequiredInformation obtains the information required to create a
// withdrawal credentials change operation.
func (c *command) obtainRequiredInformation(ctx context.Context) error {
c.chainInfo = &chainInfo{
Validators: make([]*validatorInfo, 0),
}
// Use the offline preparation file if present (and we haven't been asked to recreate it).
if !c.prepareOffline {
err := c.loadChainInfo(ctx)
if err == nil {
return nil
}
}
if c.offline {
return fmt.Errorf("could not find the %s file; this is required to have been previously generated using --offline-preparation on an online machine and be readable in the directory in which this command is being run", offlinePreparationFilename)
}
if err := c.populateChainInfo(ctx); err != nil {
return err
}
return nil
}
// populateChainInfo populates chain info structure from a beacon node.
func (c *command) populateChainInfo(ctx context.Context) error {
if c.debug {
fmt.Fprintf(os.Stderr, "Populating chain info from beacon node\n")
}
// Obtain validators.
validators, err := c.consensusClient.(consensusclient.ValidatorsProvider).Validators(ctx, "head", nil)
if err != nil {
return errors.Wrap(err, "failed to obtain validators")
}
for _, validator := range validators {
c.chainInfo.Validators = append(c.chainInfo.Validators, &validatorInfo{
Index: validator.Index,
Pubkey: validator.Validator.PublicKey,
WithdrawalCredentials: validator.Validator.WithdrawalCredentials,
})
}
// Obtain genesis validators root.
if c.genesisValidatorsRoot != "" {
// Genesis validators root supplied manually.
genesisValidatorsRoot, err := hex.DecodeString(strings.TrimPrefix(c.genesisValidatorsRoot, "0x"))
if err != nil {
return errors.Wrap(err, "invalid genesis validators root supplied")
}
if len(genesisValidatorsRoot) != phase0.RootLength {
return errors.New("invalid length for genesis validators root")
}
copy(c.chainInfo.GenesisValidatorsRoot[:], genesisValidatorsRoot)
} else {
// Genesis validators root obtained from beacon node.
genesis, err := c.consensusClient.(consensusclient.GenesisProvider).Genesis(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain genesis information")
}
c.chainInfo.GenesisValidatorsRoot = genesis.GenesisValidatorsRoot
}
if c.debug {
fmt.Fprintf(os.Stderr, "Genesis validators root is %#x\n", c.chainInfo.GenesisValidatorsRoot)
}
// Obtain epoch.
c.chainInfo.Epoch = c.chainTime.CurrentEpoch()
// Obtain fork version.
if c.forkVersion != "" {
if err := c.populateChainInfoForkVersionFromInput(ctx); err != nil {
return err
}
} else {
if err := c.populateChainInfoForkVersionFromChain(ctx); err != nil {
return err
}
}
if c.debug {
fmt.Fprintf(os.Stderr, "Fork version is %#x\n", c.chainInfo.ForkVersion)
}
// Calculate domain.
spec, err := c.consensusClient.(consensusclient.SpecProvider).Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
domainType, exists := spec["DOMAIN_BLS_TO_EXECUTION_CHANGE"].(phase0.DomainType)
if !exists {
return errors.New("failed to obtain DOMAIN_BLS_TO_EXECUTION_CHANGE")
}
if c.debug {
fmt.Fprintf(os.Stderr, "Domain type is %#x\n", domainType)
}
copy(c.chainInfo.Domain[:], domainType[:])
root, err := (&phase0.ForkData{
CurrentVersion: c.chainInfo.ForkVersion,
GenesisValidatorsRoot: c.chainInfo.GenesisValidatorsRoot,
}).HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to calculate signature domain")
}
copy(c.chainInfo.Domain[4:], root[:])
if c.debug {
fmt.Fprintf(os.Stderr, "Domain is %#x\n", c.chainInfo.Domain)
}
return nil
}
func (c *command) populateChainInfoForkVersionFromInput(_ context.Context) error {
// Fork version supplied manually.
forkVersion, err := hex.DecodeString(strings.TrimPrefix(c.forkVersion, "0x"))
if err != nil {
return errors.Wrap(err, "invalid fork version supplied")
}
if len(forkVersion) != phase0.ForkVersionLength {
return errors.New("invalid length for fork version")
}
copy(c.chainInfo.ForkVersion[:], forkVersion)
return nil
}
func (c *command) populateChainInfoForkVersionFromChain(ctx context.Context) error {
// Fetch the capella fork version from the specification.
spec, err := c.consensusClient.(consensusclient.SpecProvider).Spec(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain spec")
}
tmp, exists := spec["CAPELLA_FORK_VERSION"]
if !exists {
return errors.New("capella fork version not known by chain")
}
capellaForkVersion, isForkVersion := tmp.(phase0.Version)
if !isForkVersion {
//nolint:revive
return errors.New("CAPELLA_FORK_VERSION is not a fork version!")
}
c.chainInfo.ForkVersion = capellaForkVersion
// Work through the fork schedule to find the latest current fork post-Capella.
forkSchedule, err := c.consensusClient.(consensusclient.ForkScheduleProvider).ForkSchedule(ctx)
if err != nil {
return errors.Wrap(err, "failed to obtain fork schedule")
}
foundCapella := false
for i := range forkSchedule {
if foundCapella && forkSchedule[i].Epoch <= c.chainInfo.Epoch {
c.chainInfo.ForkVersion = forkSchedule[i].CurrentVersion
}
if bytes.Equal(forkSchedule[i].CurrentVersion[:], capellaForkVersion[:]) {
foundCapella = true
}
}
return nil
}
// dumpRequiredInformation prepares for an offline run of this command by dumping
// the chain information to a file.
func (c *command) dumpRequiredInformation(_ context.Context) error {
data, err := json.Marshal(c.chainInfo)
if err != nil {
return err
}
if err := os.WriteFile(offlinePreparationFilename, data, 0600); err != nil {
return err
}
return nil
}
func (c *command) generateOperations(ctx context.Context) error {
func (c *command) obtainOperations(ctx context.Context) error {
if c.account == "" && c.mnemonic == "" && c.privateKey == "" && c.validator == "" {
// No input information; fetch the operations from a file.
err := c.loadOperations(ctx)
err := c.obtainOperationsFromFileOrInput(ctx)
if err == nil {
// Success.
return nil
}
return fmt.Errorf("no account, mnemonic or private key specified and no %s file loaded: %v", changeOperationsFilename, err)
if c.signedOperationsInput != "" {
return errors.Wrap(err, "failed to obtain supplied signed operations")
}
return errors.Wrap(err, fmt.Sprintf("no account, mnemonic or private key specified, and no %s file loaded", changeOperationsFilename))
}
if c.mnemonic != "" {
switch {
case c.path != "":
// Have a mnemonic and path.
return c.generateOperationsFromMnemonicAndPath(ctx)
return c.generateOperationFromMnemonicAndPath(ctx)
case c.validator != "":
// Have a mnemonic and validator.
return c.generateOperationsFromMnemonicAndValidator(ctx)
return c.generateOperationFromMnemonicAndValidator(ctx)
case c.privateKey != "":
// Have a mnemonic and a private key for the withdrawal address.
return c.generateOperationsFromMnemonicAndPrivateKey(ctx)
@@ -302,117 +130,82 @@ func (c *command) generateOperations(ctx context.Context) error {
return errors.New("unsupported combination of inputs; see help for details of supported combinations")
}
func (c *command) loadChainInfo(_ context.Context) error {
_, err := os.Stat(offlinePreparationFilename)
func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) error {
seed, err := util.SeedFromMnemonic(c.mnemonic)
if err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "Failed to read offline preparation file: %v\n", err)
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
}
validatorKeyPath := c.path
match := validatorPath.Match([]byte(c.path))
if !match {
return fmt.Errorf("path %s does not match EIP-2334 format for a validator", c.path)
}
if _, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath); err != nil {
return errors.Wrap(err, "failed to generate operation from seed and path")
}
return nil
}
func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context) error {
seed, err := util.SeedFromMnemonic(c.mnemonic)
if err != nil {
return err
}
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, c.validator)
if err != nil {
return err
}
// Scan the keys from the seed to find the path.
maxDistance := 1024
// Start scanning the validator keys.
var withdrawalAccount e2wtypes.Account
for i := 0; ; i++ {
if i == maxDistance {
if c.debug {
fmt.Fprintf(os.Stderr, "Gone %d indices without finding the validator, not scanning any further\n", maxDistance)
}
break
}
return errors.Wrap(err, fmt.Sprintf("cannot find %s", offlinePreparationFilename))
}
if c.debug {
fmt.Fprintf(os.Stderr, "%s found; loading chain state\n", offlinePreparationFilename)
}
data, err := os.ReadFile(offlinePreparationFilename)
if err != nil {
return errors.Wrap(err, "failed to read offline preparation file")
}
if err := json.Unmarshal(data, c.chainInfo); err != nil {
return errors.Wrap(err, "failed to parse offline preparation file")
}
return nil
}
func (c *command) loadOperations(ctx context.Context) error {
// Start off by attempting to use the provided signed operations.
if c.signedOperationsInput != "" {
return c.loadOperationsFromInput(ctx)
}
// If not, read it from the file with the standard name.
_, err := os.Stat(changeOperationsFilename)
if err != nil {
return errors.Wrap(err, "failed to read change operations file")
}
if c.debug {
fmt.Fprintf(os.Stderr, "%s found; loading operations\n", changeOperationsFilename)
}
data, err := os.ReadFile(changeOperationsFilename)
if err != nil {
return errors.Wrap(err, "failed to read change operations file")
}
if err := json.Unmarshal(data, &c.signedOperations); err != nil {
return errors.Wrap(err, "failed to parse change operations file")
}
for _, op := range c.signedOperations {
if err := c.verifyOperation(ctx, op); err != nil {
return err
}
}
return nil
}
func (c *command) verifyOperation(ctx context.Context, op *capella.SignedBLSToExecutionChange) error {
root, err := op.Message.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to generate message root")
}
sigBytes := make([]byte, len(op.Signature))
copy(sigBytes, op.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
return errors.Wrap(err, "invalid signature")
}
container := &phase0.SigningData{
ObjectRoot: root,
Domain: c.chainInfo.Domain,
}
signingRoot, err := ssz.HashTreeRoot(container)
if err != nil {
return errors.Wrap(err, "failed to generate signing root")
}
pubkeyBytes := make([]byte, len(op.Message.FromBLSPubkey))
copy(pubkeyBytes, op.Message.FromBLSPubkey[:])
pubkey, err := e2types.BLSPublicKeyFromBytes(pubkeyBytes)
if err != nil {
return errors.Wrap(err, "invalid public key")
}
if !sig.Verify(signingRoot[:], pubkey) {
return errors.New("signature does not verify")
}
return nil
}
func (c *command) loadOperationsFromInput(_ context.Context) error {
if strings.HasPrefix(c.signedOperationsInput, "{") {
// This looks like a single entry; turn it in to an array.
c.signedOperationsInput = fmt.Sprintf("[%s]", c.signedOperationsInput)
}
if !strings.HasPrefix(c.signedOperationsInput, "[") {
// This looks like a file; read it in.
data, err := os.ReadFile(c.signedOperationsInput)
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, validatorKeyPath)
if err != nil {
return errors.Wrap(err, "failed to read input file")
return errors.Wrap(err, "failed to generate validator private key")
}
c.signedOperationsInput = string(data)
}
validatorPubkey := validatorPrivkey.PublicKey().Marshal()
if bytes.Equal(validatorPubkey, validatorInfo.Pubkey[:]) {
withdrawalKeyPath := strings.TrimSuffix(validatorKeyPath, "/0")
withdrawalAccount, err = util.ParseAccount(ctx, c.mnemonic, []string{withdrawalKeyPath}, true)
if err != nil {
return errors.Wrap(err, "failed to create withdrawal account")
}
if err := json.Unmarshal([]byte(c.signedOperationsInput), &c.signedOperations); err != nil {
return errors.Wrap(err, "failed to parse change operations input")
err = c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount)
if err != nil {
return err
}
break
}
}
return nil
}
func (c *command) generateOperationsFromMnemonicAndPrivateKey(ctx context.Context) error {
// Functionally identical to a simple scan, so use that.
return c.generateOperationsFromMnemonic(ctx)
}
func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
seed, err := util.SeedFromMnemonic(c.mnemonic)
if err != nil {
@@ -420,7 +213,7 @@ func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
}
// Turn the validators in to a map for easy lookup.
validators := make(map[string]*validatorInfo, 0)
validators := make(map[string]*beacon.ValidatorInfo, 0)
for _, validator := range c.chainInfo.Validators {
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
}
@@ -448,70 +241,141 @@ func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
return nil
}
func (c *command) generateOperationsFromMnemonicAndPrivateKey(ctx context.Context) error {
// Functionally identical to a simple scan, so use that.
return c.generateOperationsFromMnemonic(ctx)
func (c *command) generateOperationsFromAccountAndWithdrawalAccount(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
if err != nil {
return err
}
withdrawalAccount, err := util.ParseAccount(ctx, c.withdrawalAccount, c.passphrases, true)
if err != nil {
return err
}
validatorPubkey, err := util.BestPublicKey(validatorAccount)
if err != nil {
return err
}
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%#x", validatorPubkey.Marshal()))
if err != nil {
return errors.Wrap(err, "failed to obtain validator info")
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
return nil
}
func (c *command) generateOperationsFromMnemonicAndValidator(ctx context.Context) error {
seed, err := util.SeedFromMnemonic(c.mnemonic)
func (c *command) generateOperationsFromAccountAndPrivateKey(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
if err != nil {
return err
}
validator, err := c.fetchValidatorInfo(ctx)
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return err
}
// Scan the keys from the seed to find the path.
maxDistance := 1024
// Start scanning the validator keys.
for i := 0; ; i++ {
if i == maxDistance {
if c.debug {
fmt.Fprintf(os.Stderr, "Gone %d indices without finding the validator, not scanning any further\n", maxDistance)
}
break
}
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, validatorKeyPath)
if err != nil {
return errors.Wrap(err, "failed to generate validator private key")
}
validatorPubkey := validatorPrivkey.PublicKey().Marshal()
if bytes.Equal(validatorPubkey, validator.Pubkey[:]) {
// Recreate the withdrawal credentials to ensure a match.
withdrawalKeyPath := strings.TrimSuffix(validatorKeyPath, "/0")
withdrawalPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, withdrawalKeyPath)
if err != nil {
return errors.Wrap(err, "failed to generate withdrawal private key")
}
withdrawalPubkey := withdrawalPrivkey.PublicKey()
withdrawalCredentials := ethutil.SHA256(withdrawalPubkey.Marshal())
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
if !bytes.Equal(withdrawalCredentials, validator.WithdrawalCredentials) {
return fmt.Errorf("validator %#x withdrawal credentials %#x do not match expected credentials, cannot update", validatorPubkey, validator.WithdrawalCredentials)
}
validatorPubkey, err := util.BestPublicKey(validatorAccount)
if err != nil {
return err
}
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%#x", validatorPubkey.Marshal()))
if err != nil {
return errors.Wrap(err, "failed to obtain validator info")
}
withdrawalAccount, err := util.ParseAccount(ctx, c.mnemonic, []string{withdrawalKeyPath}, true)
if err != nil {
return errors.Wrap(err, "failed to create withdrawal account")
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
err = c.generateOperationFromAccount(ctx, validator, withdrawalAccount)
if err != nil {
return err
}
break
return nil
}
func (c *command) generateOperationsFromValidatorAndPrivateKey(ctx context.Context) error {
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, c.validator)
if err != nil {
return err
}
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return err
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
return nil
}
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.obtainOperationsFromFile(ctx)
}
func (c *command) obtainOperationsFromFile(ctx context.Context) error {
_, err := os.Stat(changeOperationsFilename)
if err != nil {
return errors.Wrap(err, "failed to read change operations file")
}
if c.debug {
fmt.Fprintf(os.Stderr, "%s found; loading operations\n", changeOperationsFilename)
}
data, err := os.ReadFile(changeOperationsFilename)
if err != nil {
return errors.Wrap(err, "failed to read change operations file")
}
if err := json.Unmarshal(data, &c.signedOperations); err != nil {
return errors.Wrap(err, "failed to parse change operations file")
}
for _, op := range c.signedOperations {
if err := c.verifyOperation(ctx, op); err != nil {
return err
}
}
return nil
}
func (c *command) obtainOperationsFromInput(ctx context.Context) error {
if strings.HasPrefix(c.signedOperationsInput, "{") {
// This looks like a single entry; turn it in to an array.
c.signedOperationsInput = fmt.Sprintf("[%s]", c.signedOperationsInput)
}
if !strings.HasPrefix(c.signedOperationsInput, "[") {
// This looks like a file; read it in.
data, err := os.ReadFile(c.signedOperationsInput)
if err != nil {
return errors.Wrap(err, "failed to read input file")
}
c.signedOperationsInput = string(data)
}
if err := json.Unmarshal([]byte(c.signedOperationsInput), &c.signedOperations); err != nil {
return errors.Wrap(err, "failed to parse change operations input")
}
for _, op := range c.signedOperations {
if err := c.verifyOperation(ctx, op); err != nil {
return err
}
}
return nil
}
func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
validators map[string]*validatorInfo,
validators map[string]*beacon.ValidatorInfo,
seed []byte,
path string,
) (
@@ -587,7 +451,7 @@ func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
}
func (c *command) generateOperationFromAccount(ctx context.Context,
validator *validatorInfo,
validator *beacon.ValidatorInfo,
withdrawalAccount e2wtypes.Account,
) error {
signedOperation, err := c.createSignedOperation(ctx, validator, withdrawalAccount)
@@ -599,7 +463,7 @@ func (c *command) generateOperationFromAccount(ctx context.Context,
}
func (c *command) createSignedOperation(ctx context.Context,
validator *validatorInfo,
validator *beacon.ValidatorInfo,
withdrawalAccount e2wtypes.Account,
) (
*capella.SignedBLSToExecutionChange,
@@ -630,7 +494,10 @@ func (c *command) createSignedOperation(ctx context.Context,
}
// Sign the operation.
signature, err := signing.SignRoot(ctx, withdrawalAccount, nil, root, c.chainInfo.Domain)
if c.debug {
fmt.Fprintf(os.Stderr, "Signing %#x with domain %#x by public key %#x\n", root, c.domain, withdrawalAccount.PublicKey().Marshal())
}
signature, err := signing.SignRoot(ctx, withdrawalAccount, nil, root, c.domain)
if err != nil {
return nil, errors.Wrap(err, "failed to sign credentials change operation")
}
@@ -661,7 +528,7 @@ func (c *command) parseWithdrawalAddress(_ context.Context) error {
func (c *command) validateOperations(ctx context.Context) (bool, string) {
// Turn the validators in to a map for easy lookup.
validators := make(map[phase0.ValidatorIndex]*validatorInfo, 0)
validators := make(map[phase0.ValidatorIndex]*beacon.ValidatorInfo, 0)
for _, validator := range c.chainInfo.Validators {
validators[validator.Index] = validator
}
@@ -674,8 +541,43 @@ func (c *command) validateOperations(ctx context.Context) (bool, string) {
return true, ""
}
func (c *command) verifyOperation(ctx context.Context, op *capella.SignedBLSToExecutionChange) error {
root, err := op.Message.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to generate message root")
}
sigBytes := make([]byte, len(op.Signature))
copy(sigBytes, op.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
return errors.Wrap(err, "invalid signature")
}
container := &phase0.SigningData{
ObjectRoot: root,
Domain: c.domain,
}
signingRoot, err := ssz.HashTreeRoot(container)
if err != nil {
return errors.Wrap(err, "failed to generate signing root")
}
pubkeyBytes := make([]byte, len(op.Message.FromBLSPubkey))
copy(pubkeyBytes, op.Message.FromBLSPubkey[:])
pubkey, err := e2types.BLSPublicKeyFromBytes(pubkeyBytes)
if err != nil {
return errors.Wrap(err, "invalid public key")
}
if !sig.Verify(signingRoot[:], pubkey) {
return errors.New("signature does not verify")
}
return nil
}
func (c *command) validateOperation(_ context.Context,
validators map[phase0.ValidatorIndex]*validatorInfo,
validators map[phase0.ValidatorIndex]*beacon.ValidatorInfo,
signedOperation *capella.SignedBLSToExecutionChange,
) (
bool,
@@ -725,7 +627,6 @@ func (c *command) setup(ctx context.Context) error {
// Set up chaintime.
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithGenesisTimeProvider(c.consensusClient.(consensusclient.GenesisTimeProvider)),
standardchaintime.WithForkScheduleProvider(c.consensusClient.(consensusclient.ForkScheduleProvider)),
standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)),
)
if err != nil {
@@ -735,56 +636,88 @@ func (c *command) setup(ctx context.Context) error {
return nil
}
func (c *command) fetchValidatorInfo(ctx context.Context) (*validatorInfo, error) {
var validatorInfo *validatorInfo
switch {
case c.validator == "":
return nil, errors.New("no validator specified")
case strings.HasPrefix(c.validator, "0x"):
// A public key
for _, validator := range c.chainInfo.Validators {
if strings.EqualFold(c.validator, fmt.Sprintf("%#x", validator.Pubkey)) {
validatorInfo = validator
break
}
}
case strings.Contains(c.validator, "/"):
// An account.
_, account, err := util.WalletAndAccountFromPath(ctx, c.validator)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain account")
}
accPubKey, err := util.BestPublicKey(account)
if err != nil {
return nil, errors.Wrap(err, "unable to obtain public key for account")
}
pubkey := fmt.Sprintf("%#x", accPubKey.Marshal())
for _, validator := range c.chainInfo.Validators {
if strings.EqualFold(pubkey, fmt.Sprintf("%#x", validator.Pubkey)) {
validatorInfo = validator
break
}
}
default:
// An index.
index, err := strconv.ParseUint(c.validator, 10, 64)
if err != nil {
return nil, errors.Wrap(err, "failed to parse validator index")
}
validatorIndex := phase0.ValidatorIndex(index)
for _, validator := range c.chainInfo.Validators {
if validator.Index == validatorIndex {
validatorInfo = validator
break
}
}
func (c *command) generateDomain(ctx context.Context) error {
genesisValidatorsRoot, err := c.obtainGenesisValidatorsRoot(ctx)
if err != nil {
return err
}
forkVersion, err := c.obtainForkVersion(ctx)
if err != nil {
return err
}
if validatorInfo == nil {
return nil, errors.New("unknown validator")
root, err := (&phase0.ForkData{
CurrentVersion: forkVersion,
GenesisValidatorsRoot: genesisValidatorsRoot,
}).HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to calculate signature domain")
}
return validatorInfo, nil
copy(c.domain[:], c.chainInfo.BLSToExecutionChangeDomainType[:])
copy(c.domain[4:], root[:])
if c.debug {
fmt.Fprintf(os.Stderr, "Domain is %#x\n", c.domain)
}
return nil
}
func (c *command) obtainGenesisValidatorsRoot(ctx context.Context) (phase0.Root, error) {
genesisValidatorsRoot := phase0.Root{}
if c.genesisValidatorsRoot != "" {
if c.debug {
fmt.Fprintf(os.Stderr, "Genesis validators root supplied on the command line\n")
}
root, err := hex.DecodeString(strings.TrimPrefix(c.genesisValidatorsRoot, "0x"))
if err != nil {
return phase0.Root{}, errors.Wrap(err, "invalid genesis validators root supplied")
}
if len(root) != phase0.RootLength {
return phase0.Root{}, errors.New("invalid length for genesis validators root")
}
copy(genesisValidatorsRoot[:], root)
} else {
if c.debug {
fmt.Fprintf(os.Stderr, "Genesis validators root obtained from chain info\n")
}
copy(genesisValidatorsRoot[:], c.chainInfo.GenesisValidatorsRoot[:])
}
if c.debug {
fmt.Fprintf(os.Stderr, "Using genesis validators root %#x\n", genesisValidatorsRoot)
}
return genesisValidatorsRoot, nil
}
func (c *command) obtainForkVersion(ctx context.Context) (phase0.Version, error) {
forkVersion := phase0.Version{}
if c.forkVersion != "" {
if c.debug {
fmt.Fprintf(os.Stderr, "Fork version supplied on the command line\n")
}
version, err := hex.DecodeString(strings.TrimPrefix(c.forkVersion, "0x"))
if err != nil {
return phase0.Version{}, errors.Wrap(err, "invalid fork version supplied")
}
if len(version) != phase0.ForkVersionLength {
return phase0.Version{}, errors.New("invalid length for fork version")
}
copy(forkVersion[:], version)
} else {
if c.debug {
fmt.Fprintf(os.Stderr, "Fork version obtained from chain info\n")
}
// Use the genesis fork version for setting credentials as per the spec.
copy(forkVersion[:], c.chainInfo.GenesisForkVersion[:])
}
if c.debug {
fmt.Fprintf(os.Stderr, "Using fork version %#x\n", forkVersion)
}
return forkVersion, nil
}
// addressBytesToEIP55 converts a byte array in to an EIP-55 string format.
@@ -805,126 +738,3 @@ func addressBytesToEIP55(address []byte) string {
return fmt.Sprintf("0x%s", string(bytes))
}
func (c *command) generateOperationsFromMnemonicAndPath(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]*validatorInfo, 0)
for _, validator := range c.chainInfo.Validators {
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
}
validatorKeyPath := c.path
match := validatorPath.Match([]byte(c.path))
if !match {
return fmt.Errorf("path %s does not match EIP-2334 format", c.path)
}
if _, err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath); err != nil {
return errors.Wrap(err, "failed to generate operation from seed and path")
}
return nil
}
func (c *command) generateOperationsFromAccountAndWithdrawalAccount(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
if err != nil {
return err
}
withdrawalAccount, err := util.ParseAccount(ctx, c.withdrawalAccount, c.passphrases, true)
if err != nil {
return err
}
// Find the validator info given its account information.
validatorPubkey := validatorAccount.PublicKey().Marshal()
var validatorInfo *validatorInfo
for _, validator := range c.chainInfo.Validators {
if bytes.Equal(validator.Pubkey[:], validatorPubkey) {
// Found it.
validatorInfo = validator
}
}
if validatorInfo == nil {
return errors.New("could not find information for that validator on the chain")
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
return nil
}
func (c *command) generateOperationsFromAccountAndPrivateKey(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
if err != nil {
return err
}
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return err
}
// Find the validator info given its account information.
validatorPubkey := validatorAccount.PublicKey().Marshal()
var validatorInfo *validatorInfo
for _, validator := range c.chainInfo.Validators {
if bytes.Equal(validator.Pubkey[:], validatorPubkey) {
// Found it.
validatorInfo = validator
}
}
if validatorInfo == nil {
return errors.New("could not find information for that validator on the chain")
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
return nil
}
func (c *command) generateOperationsFromValidatorAndPrivateKey(ctx context.Context) error {
validator, err := c.fetchValidatorInfo(ctx)
if err != nil {
return err
}
validatorAccount, err := util.ParseAccount(ctx, validator.Pubkey.String(), nil, false)
if err != nil {
return err
}
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return err
}
// Find the validator info given its account information.
validatorPubkey := validatorAccount.PublicKey().Marshal()
var validatorInfo *validatorInfo
for _, validator := range c.chainInfo.Validators {
if bytes.Equal(validator.Pubkey[:], validatorPubkey) {
// Found it.
validatorInfo = validator
}
}
if validatorInfo == nil {
return errors.New("could not find information for that validator on the chain")
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,372 @@
// Copyright © 2022 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 validatorcredentialsset
import (
"context"
"fmt"
"testing"
"github.com/attestantio/go-eth2-client/spec/bellatrix"
capella "github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/beacon"
e2types "github.com/wealdtech/go-eth2-types/v2"
)
func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
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},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
tests := []struct {
name string
command *command
expected []*capella.SignedBLSToExecutionChange
err string
}{
{
name: "MnemonicInvalid",
command: &command{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
path: "m/12381/3600/0/0/0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "mnemonic is invalid",
},
{
name: "PathInvalid",
command: &command{
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",
path: "m/12381/3600/0/0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "path m/12381/3600/0/0 does not match EIP-2334 format for a validator",
},
{
name: "Good",
command: &command{
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",
path: "m/12381/3600/0/0/0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
expected: []*capella.SignedBLSToExecutionChange{
{
Message: &capella.BLSToExecutionChange{
ValidatorIndex: 0,
FromBLSPubkey: phase0.BLSPubKey{0x99, 0xb1, 0xf1, 0xd8, 0x4d, 0x76, 0x18, 0x54, 0x66, 0xd8, 0x6c, 0x34, 0xbd, 0xe1, 0x10, 0x13, 0x16, 0xaf, 0xdd, 0xae, 0x76, 0x21, 0x7a, 0xa8, 0x6c, 0xd0, 0x66, 0x97, 0x9b, 0x19, 0x85, 0x8c, 0x2c, 0x9d, 0x9e, 0x56, 0xee, 0xbc, 0x1e, 0x06, 0x7a, 0xc5, 0x42, 0x77, 0xa6, 0x17, 0x90, 0xdb},
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
},
Signature: phase0.BLSSignature{0xb7, 0x8a, 0x05, 0xba, 0xd9, 0x27, 0xfc, 0x89, 0x6f, 0x14, 0x06, 0xb3, 0x2d, 0x64, 0x4a, 0xe1, 0x69, 0xce, 0xcd, 0x89, 0x86, 0xc1, 0xef, 0x8c, 0x0d, 0x03, 0x7d, 0x70, 0x86, 0xf8, 0x5f, 0x13, 0xe1, 0xe1, 0x88, 0xb4, 0x30, 0x96, 0x43, 0xa2, 0xc1, 0x3f, 0xfe, 0xfb, 0x0a, 0xe8, 0x05, 0x11, 0x09, 0x98, 0x53, 0xa0, 0x58, 0x1f, 0x4b, 0x2b, 0xd2, 0xe1, 0x45, 0x41, 0x04, 0x79, 0x01, 0xe2, 0x2a, 0x94, 0x0a, 0x9c, 0x7e, 0x3a, 0xc0, 0xa8, 0x82, 0xd1, 0xa8, 0xaf, 0x6b, 0xfa, 0xea, 0x81, 0x3a, 0x6a, 0x6b, 0xe7, 0x21, 0xf9, 0x26, 0x22, 0x04, 0xaa, 0x9d, 0xa4, 0xe4, 0x77, 0x27, 0xd0},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.generateOperationFromMnemonicAndPath(ctx)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
// fmt.Printf("%v\n", test.command.signedOperations)
require.Equal(t, test.expected, test.command.signedOperations)
}
})
}
}
func TestGenerateOperationFromMnemonicAndValidator(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
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},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
tests := []struct {
name string
command *command
expected []*capella.SignedBLSToExecutionChange
err string
}{
{
name: "MnemonicInvalid",
command: &command{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
validator: "0",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "mnemonic is invalid",
},
{
name: "ValidatorMissing",
command: &command{
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",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
err: "no validator specified",
},
{
name: "Good",
command: &command{
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,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
expected: []*capella.SignedBLSToExecutionChange{
{
Message: &capella.BLSToExecutionChange{
ValidatorIndex: 0,
FromBLSPubkey: phase0.BLSPubKey{0x99, 0xb1, 0xf1, 0xd8, 0x4d, 0x76, 0x18, 0x54, 0x66, 0xd8, 0x6c, 0x34, 0xbd, 0xe1, 0x10, 0x13, 0x16, 0xaf, 0xdd, 0xae, 0x76, 0x21, 0x7a, 0xa8, 0x6c, 0xd0, 0x66, 0x97, 0x9b, 0x19, 0x85, 0x8c, 0x2c, 0x9d, 0x9e, 0x56, 0xee, 0xbc, 0x1e, 0x06, 0x7a, 0xc5, 0x42, 0x77, 0xa6, 0x17, 0x90, 0xdb},
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
},
Signature: phase0.BLSSignature{0xb7, 0x8a, 0x05, 0xba, 0xd9, 0x27, 0xfc, 0x89, 0x6f, 0x14, 0x06, 0xb3, 0x2d, 0x64, 0x4a, 0xe1, 0x69, 0xce, 0xcd, 0x89, 0x86, 0xc1, 0xef, 0x8c, 0x0d, 0x03, 0x7d, 0x70, 0x86, 0xf8, 0x5f, 0x13, 0xe1, 0xe1, 0x88, 0xb4, 0x30, 0x96, 0x43, 0xa2, 0xc1, 0x3f, 0xfe, 0xfb, 0x0a, 0xe8, 0x05, 0x11, 0x09, 0x98, 0x53, 0xa0, 0x58, 0x1f, 0x4b, 0x2b, 0xd2, 0xe1, 0x45, 0x41, 0x04, 0x79, 0x01, 0xe2, 0x2a, 0x94, 0x0a, 0x9c, 0x7e, 0x3a, 0xc0, 0xa8, 0x82, 0xd1, 0xa8, 0xaf, 0x6b, 0xfa, 0xea, 0x81, 0x3a, 0x6a, 0x6b, 0xe7, 0x21, 0xf9, 0x26, 0x22, 0x04, 0xaa, 0x9d, 0xa4, 0xe4, 0x77, 0x27, 0xd0},
},
},
},
{
name: "GoodPubkey",
command: &command{
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: "0xb384f767d964e100c8a9b21018d08c25ffebae268b3ab6d610353897541971726dbfc3c7463884c68a531515aab94c87",
chainInfo: chainInfo,
signedOperations: make([]*capella.SignedBLSToExecutionChange, 0),
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
expected: []*capella.SignedBLSToExecutionChange{
{
Message: &capella.BLSToExecutionChange{
ValidatorIndex: 0,
FromBLSPubkey: phase0.BLSPubKey{0x99, 0xb1, 0xf1, 0xd8, 0x4d, 0x76, 0x18, 0x54, 0x66, 0xd8, 0x6c, 0x34, 0xbd, 0xe1, 0x10, 0x13, 0x16, 0xaf, 0xdd, 0xae, 0x76, 0x21, 0x7a, 0xa8, 0x6c, 0xd0, 0x66, 0x97, 0x9b, 0x19, 0x85, 0x8c, 0x2c, 0x9d, 0x9e, 0x56, 0xee, 0xbc, 0x1e, 0x06, 0x7a, 0xc5, 0x42, 0x77, 0xa6, 0x17, 0x90, 0xdb},
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
},
Signature: phase0.BLSSignature{0xb7, 0x8a, 0x05, 0xba, 0xd9, 0x27, 0xfc, 0x89, 0x6f, 0x14, 0x06, 0xb3, 0x2d, 0x64, 0x4a, 0xe1, 0x69, 0xce, 0xcd, 0x89, 0x86, 0xc1, 0xef, 0x8c, 0x0d, 0x03, 0x7d, 0x70, 0x86, 0xf8, 0x5f, 0x13, 0xe1, 0xe1, 0x88, 0xb4, 0x30, 0x96, 0x43, 0xa2, 0xc1, 0x3f, 0xfe, 0xfb, 0x0a, 0xe8, 0x05, 0x11, 0x09, 0x98, 0x53, 0xa0, 0x58, 0x1f, 0x4b, 0x2b, 0xd2, 0xe1, 0x45, 0x41, 0x04, 0x79, 0x01, 0xe2, 0x2a, 0x94, 0x0a, 0x9c, 0x7e, 0x3a, 0xc0, 0xa8, 0x82, 0xd1, 0xa8, 0xaf, 0x6b, 0xfa, 0xea, 0x81, 0x3a, 0x6a, 0x6b, 0xe7, 0x21, 0xf9, 0x26, 0x22, 0x04, 0xaa, 0x9d, 0xa4, 0xe4, 0x77, 0x27, 0xd0},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.generateOperationFromMnemonicAndValidator(ctx)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, test.command.signedOperations)
}
})
}
}
func TestGenerateOperationFromSeedAndPath(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
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},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
validators := make(map[string]*beacon.ValidatorInfo, len(chainInfo.Validators))
for i := range chainInfo.Validators {
validators[fmt.Sprintf("%#x", chainInfo.Validators[i].Pubkey)] = chainInfo.Validators[i]
}
tests := []struct {
name string
command *command
seed []byte
path string
generated bool
err string
expected []*capella.SignedBLSToExecutionChange
}{
{
name: "PathInvalid",
command: &command{
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",
chainInfo: chainInfo,
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "invalid",
err: "failed to generate validator private key: not master at path component 0",
},
{
name: "ValidatorUnknown",
command: &command{
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",
chainInfo: chainInfo,
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/999/0/0",
},
{
name: "ValidatorCredentialsAlreadySet",
command: &command{
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",
chainInfo: chainInfo,
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/2/0/0",
},
{
name: "PrivateKeyInvalid",
command: &command{
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",
chainInfo: chainInfo,
privateKey: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/0/0/0",
err: "failed to create account from private key: invalid private key: err blsSecretKeyDeserialize ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
name: "Good",
command: &command{
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",
chainInfo: chainInfo,
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/0/0/0",
generated: true,
expected: []*capella.SignedBLSToExecutionChange{
{
Message: &capella.BLSToExecutionChange{
ValidatorIndex: 0,
FromBLSPubkey: phase0.BLSPubKey{0x99, 0xb1, 0xf1, 0xd8, 0x4d, 0x76, 0x18, 0x54, 0x66, 0xd8, 0x6c, 0x34, 0xbd, 0xe1, 0x10, 0x13, 0x16, 0xaf, 0xdd, 0xae, 0x76, 0x21, 0x7a, 0xa8, 0x6c, 0xd0, 0x66, 0x97, 0x9b, 0x19, 0x85, 0x8c, 0x2c, 0x9d, 0x9e, 0x56, 0xee, 0xbc, 0x1e, 0x06, 0x7a, 0xc5, 0x42, 0x77, 0xa6, 0x17, 0x90, 0xdb},
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
},
Signature: phase0.BLSSignature{0xb7, 0x8a, 0x05, 0xba, 0xd9, 0x27, 0xfc, 0x89, 0x6f, 0x14, 0x06, 0xb3, 0x2d, 0x64, 0x4a, 0xe1, 0x69, 0xce, 0xcd, 0x89, 0x86, 0xc1, 0xef, 0x8c, 0x0d, 0x03, 0x7d, 0x70, 0x86, 0xf8, 0x5f, 0x13, 0xe1, 0xe1, 0x88, 0xb4, 0x30, 0x96, 0x43, 0xa2, 0xc1, 0x3f, 0xfe, 0xfb, 0x0a, 0xe8, 0x05, 0x11, 0x09, 0x98, 0x53, 0xa0, 0x58, 0x1f, 0x4b, 0x2b, 0xd2, 0xe1, 0x45, 0x41, 0x04, 0x79, 0x01, 0xe2, 0x2a, 0x94, 0x0a, 0x9c, 0x7e, 0x3a, 0xc0, 0xa8, 0x82, 0xd1, 0xa8, 0xaf, 0x6b, 0xfa, 0xea, 0x81, 0x3a, 0x6a, 0x6b, 0xe7, 0x21, 0xf9, 0x26, 0x22, 0x04, 0xaa, 0x9d, 0xa4, 0xe4, 0x77, 0x27, 0xd0},
},
},
},
{
name: "GoodPrivateKey",
command: &command{
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",
chainInfo: chainInfo,
withdrawalAddressStr: "0x8c1Ff978036F2e9d7CC382Eff7B4c8c53C22ac15",
privateKey: "0x67775f030068b4610d6e1bd04948f547305b2502423fcece4c1091d065b44638",
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/3/0/0",
generated: true,
expected: []*capella.SignedBLSToExecutionChange{
{
Message: &capella.BLSToExecutionChange{
ValidatorIndex: 3,
FromBLSPubkey: phase0.BLSPubKey{0x86, 0x71, 0x0a, 0xbb, 0x44, 0xb6, 0xcd, 0xa6, 0x66, 0x57, 0x7b, 0xbb, 0x25, 0x5e, 0x16, 0xd9, 0x8b, 0xf2, 0x52, 0x51, 0x76, 0x22, 0x3f, 0x35, 0x35, 0xc7, 0xdf, 0xf8, 0xe7, 0x0b, 0x3b, 0xc8, 0x92, 0xbb, 0x36, 0x11, 0x33, 0x95, 0x2b, 0x03, 0xd2, 0xb0, 0x78, 0xcd, 0x07, 0x18, 0xca, 0xf3},
ToExecutionAddress: bellatrix.ExecutionAddress{0x8c, 0x1f, 0xf9, 0x78, 0x03, 0x6f, 0x2e, 0x9d, 0x7c, 0xc3, 0x82, 0xef, 0xf7, 0xb4, 0xc8, 0xc5, 0x3c, 0x22, 0xac, 0x15},
},
Signature: phase0.BLSSignature{0x8d, 0x92, 0xb9, 0x1c, 0x5d, 0xfd, 0x98, 0xc7, 0x98, 0xfc, 0x94, 0xe1, 0xe6, 0x69, 0xf3, 0xaa, 0xae, 0x72, 0xb2, 0x36, 0x47, 0xde, 0x88, 0x54, 0xea, 0x16, 0x74, 0x7f, 0xfe, 0xf0, 0x4d, 0x46, 0x5c, 0x07, 0x56, 0x34, 0x03, 0x30, 0x2f, 0xbc, 0x26, 0xa2, 0x6d, 0xec, 0x10, 0x20, 0xe7, 0x67, 0x10, 0xb0, 0x4a, 0x7e, 0x4e, 0x25, 0x89, 0x7e, 0x87, 0x88, 0xda, 0xaf, 0x2b, 0xb5, 0xb7, 0x73, 0x25, 0x64, 0x80, 0xc1, 0xba, 0xf3, 0x1d, 0x33, 0x8f, 0x17, 0xa5, 0x35, 0x74, 0x80, 0xf3, 0x37, 0x0e, 0xea, 0x19, 0x15, 0xd5, 0x69, 0x7e, 0xf6, 0x68, 0xaa, 0x9c, 0x3d, 0x47, 0x19, 0x75, 0xfc},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
generated, err := test.command.generateOperationFromSeedAndPath(ctx, validators, test.seed, test.path)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.generated, generated)
if generated {
require.Equal(t, test.expected, test.command.signedOperations)
}
}
})
}
}

View File

@@ -0,0 +1,105 @@
// Copyright © 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
//
// 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 validatorexit
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/beacon"
)
// obtainChainInfo obtains the chain information required to create an exit operation.
func (c *command) obtainChainInfo(ctx context.Context) error {
// Use the offline preparation file if present (and we haven't been asked to recreate it).
if !c.prepareOffline {
err := c.obtainChainInfoFromFile(ctx)
if err == nil {
return nil
}
}
if c.offline {
return fmt.Errorf("%s is unavailable or outdated; this is required to have been previously generated using --offline-preparation on an online machine and be readable in the directory in which this command is being run", offlinePreparationFilename)
}
if err := c.obtainChainInfoFromNode(ctx); err != nil {
return err
}
return nil
}
// obtainChainInfoFromFile obtains chain information from a pre-generated file.
func (c *command) obtainChainInfoFromFile(_ context.Context) error {
_, err := os.Stat(offlinePreparationFilename)
if err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "Failed to read offline preparation file: %v\n", err)
}
return errors.Wrap(err, fmt.Sprintf("cannot find %s", offlinePreparationFilename))
}
if c.debug {
fmt.Fprintf(os.Stderr, "%s found; loading chain state\n", offlinePreparationFilename)
}
data, err := os.ReadFile(offlinePreparationFilename)
if err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "failed to load chain state: %v\n", err)
}
return errors.Wrap(err, "failed to read offline preparation file")
}
c.chainInfo = &beacon.ChainInfo{}
if err := json.Unmarshal(data, c.chainInfo); err != nil {
if c.debug {
fmt.Fprintf(os.Stderr, "chain state invalid: %v\n", err)
}
return errors.Wrap(err, "failed to parse offline preparation file")
}
return nil
}
// obtainChainInfoFromNode obtains chain info from a beacon node.
func (c *command) obtainChainInfoFromNode(ctx context.Context) error {
if c.debug {
fmt.Fprintf(os.Stderr, "Populating chain info from beacon node\n")
}
var err error
c.chainInfo, err = beacon.ObtainChainInfoFromNode(ctx, c.consensusClient, c.chainTime)
if err != nil {
return err
}
return nil
}
// writeChainInfoToFile prepares for an offline run of this command by dumping
// the chain information to a file.
func (c *command) writeChainInfoToFile(_ context.Context) error {
data, err := json.Marshal(c.chainInfo)
if err != nil {
return err
}
if err := os.WriteFile(offlinePreparationFilename, data, 0600); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,97 @@
// Copyright © 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
//
// 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 validatorexit
import (
"context"
"time"
consensusclient "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/beacon"
"github.com/wealdtech/ethdo/services/chaintime"
"github.com/wealdtech/ethdo/util"
)
type command struct {
quiet bool
verbose bool
debug bool
offline bool
json bool
// Input.
passphrases []string
mnemonic string
path string
privateKey string
validator string
forkVersion string
genesisValidatorsRoot string
prepareOffline bool
signedOperationInput string
// Beacon node connection.
timeout time.Duration
connection string
allowInsecureConnections bool
// Information required to generate the operations.
chainInfo *beacon.ChainInfo
domain phase0.Domain
// Processing.
consensusClient consensusclient.Service
chainTime chaintime.Service
// Output.
signedOperation *phase0.SignedVoluntaryExit
}
func newCommand(_ context.Context) (*command, error) {
c := &command{
quiet: viper.GetBool("quiet"),
verbose: viper.GetBool("verbose"),
debug: viper.GetBool("debug"),
offline: viper.GetBool("offline"),
json: viper.GetBool("json"),
timeout: viper.GetDuration("timeout"),
connection: viper.GetString("connection"),
allowInsecureConnections: viper.GetBool("allow-insecure-connections"),
prepareOffline: viper.GetBool("prepare-offline"),
passphrases: util.GetPassphrases(),
mnemonic: viper.GetString("mnemonic"),
path: viper.GetString("path"),
privateKey: viper.GetString("private-key"),
signedOperationInput: viper.GetString("signed-operation"),
validator: viper.GetString("validator"),
forkVersion: viper.GetString("fork-version"),
genesisValidatorsRoot: viper.GetString("genesis-validators-root"),
}
// Timeout is required.
if c.timeout == 0 {
return nil, errors.New("timeout is required")
}
// We are generating information for offline use, we don't need any information
// related to the accounts or signing.
if c.prepareOffline {
return c, nil
}
return c, nil
}

View File

@@ -1,149 +0,0 @@
// 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 validatorexit
import (
"context"
"encoding/hex"
"encoding/json"
"strings"
"time"
eth2client "github.com/attestantio/go-eth2-client"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/spf13/viper"
"github.com/wealdtech/ethdo/util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
type dataIn struct {
// System.
timeout time.Duration
quiet bool
verbose bool
debug bool
// Operation.
eth2Client eth2client.Service
jsonOutput bool
// Chain information.
fork *spec.Fork
currentEpoch spec.Epoch
// Exit information.
account e2wtypes.Account
passphrases []string
epoch spec.Epoch
domain spec.Domain
signedVoluntaryExit *spec.SignedVoluntaryExit
}
func input(ctx context.Context) (*dataIn, error) {
data := &dataIn{}
if viper.GetDuration("timeout") == 0 {
return nil, errors.New("timeout is required")
}
data.timeout = viper.GetDuration("timeout")
data.quiet = viper.GetBool("quiet")
data.verbose = viper.GetBool("verbose")
data.debug = viper.GetBool("debug")
data.passphrases = util.GetPassphrases()
data.jsonOutput = viper.GetBool("json")
switch {
case viper.GetString("exit") != "":
return inputJSON(ctx, data)
case viper.GetString("account") != "":
return inputAccount(ctx, data)
case viper.GetString("key") != "":
return inputKey(ctx, data)
default:
return nil, errors.New("must supply account, key, or pre-constructed JSON")
}
}
func inputJSON(ctx context.Context, data *dataIn) (*dataIn, error) {
validatorData := &util.ValidatorExitData{}
err := json.Unmarshal([]byte(viper.GetString("exit")), validatorData)
if err != nil {
return nil, err
}
data.signedVoluntaryExit = validatorData.Exit
return inputChainData(ctx, data)
}
func inputAccount(ctx context.Context, data *dataIn) (*dataIn, error) {
var err error
_, data.account, err = util.WalletAndAccountFromInput(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain acount")
}
return inputChainData(ctx, data)
}
func inputKey(ctx context.Context, data *dataIn) (*dataIn, error) {
privKeyBytes, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode key")
}
data.account, err = util.NewScratchAccount(privKeyBytes, nil)
if err != nil {
return nil, errors.Wrap(err, "failed to create acount from key")
}
if err := data.account.(e2wtypes.AccountLocker).Unlock(ctx, nil); err != nil {
return nil, errors.Wrap(err, "failed to unlock account")
}
return inputChainData(ctx, data)
}
func inputChainData(ctx context.Context, data *dataIn) (*dataIn, error) {
var err error
data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections"))
if err != nil {
return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node")
}
// Current fork.
data.fork, err = data.eth2Client.(eth2client.ForkProvider).Fork(ctx, "head")
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain fork information")
}
// Calculate current epoch.
config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain configuration information")
}
genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to connect to obtain genesis information")
}
data.currentEpoch = spec.Epoch(uint64(time.Since(genesis.GenesisTime).Seconds()) / (uint64(config["SECONDS_PER_SLOT"].(time.Duration).Seconds()) * config["SLOTS_PER_EPOCH"].(uint64)))
// Epoch.
if viper.GetInt64("epoch") == -1 {
data.epoch = data.currentEpoch
} else {
data.epoch = spec.Epoch(viper.GetUint64("epoch"))
}
// Domain.
domain, err := data.eth2Client.(eth2client.DomainProvider).Domain(ctx, config["DOMAIN_VOLUNTARY_EXIT"].(spec.DomainType), data.epoch)
if err != nil {
return nil, errors.New("failed to calculate domain")
}
data.domain = domain
return data, nil
}

View File

@@ -1,193 +0,0 @@
// 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 validatorexit
import (
"context"
"os"
"testing"
"time"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestInput(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
require.NoError(t, e2types.InitBLS())
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
_, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
tests := []struct {
name string
vars map[string]interface{}
res *dataIn
err string
}{
{
name: "TimeoutMissing",
vars: map[string]interface{}{
"account": "Test wallet",
"wallet-passphrase": "ce%NohGhah4ye5ra",
"type": "nd",
},
err: "timeout is required",
},
{
name: "NoMethod",
vars: map[string]interface{}{
"timeout": "5s",
},
err: "must supply account, key, or pre-constructed JSON",
},
{
name: "KeyInvalid",
vars: map[string]interface{}{
"timeout": "5s",
"key": "0xinvalid",
},
err: "failed to decode key: encoding/hex: invalid byte: U+0069 'i'",
},
{
name: "KeyBad",
vars: map[string]interface{}{
"timeout": "5s",
"key": "0x00",
},
err: "failed to create acount from key: private key must be 32 bytes",
},
{
name: "KeyGood",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
{
name: "AccountUnknown",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"account": "Test wallet/unknown",
},
res: &dataIn{
timeout: 5 * time.Second,
},
err: "failed to obtain acount: failed to obtain account: no account with name \"unknown\"",
},
{
name: "AccountGood",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"account": "Test wallet/Interop 0",
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
{
name: "JSONInvalid",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"exit": `invalid`,
},
res: &dataIn{
timeout: 5 * time.Second,
},
err: "invalid character 'i' looking for beginning of value",
},
{
name: "JSONGood",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"exit": `{"exit":{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"},"fork_version":"0x00002009"}`,
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
{
name: "ClientBad",
vars: map[string]interface{}{
"connection": "localhost:1",
"allow-insecure-connections": true,
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
},
err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to confirm node connection: failed to fetch genesis: failed to request genesis: failed to call GET endpoint: Get \"http://localhost:1/eth/v1/beacon/genesis\": dial tcp 127.0.0.1:1: connect: connection refused",
},
{
name: "EpochProvided",
vars: map[string]interface{}{
"connection": os.Getenv("ETHDO_TEST_CONNECTION"),
"allow-insecure-connections": true,
"timeout": "5s",
"key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866",
"epoch": "123",
},
res: &dataIn{
timeout: 5 * time.Second,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
viper.Reset()
for k, v := range test.vars {
viper.Set(k, v)
}
res, err := input(context.Background())
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res.timeout, res.timeout)
}
})
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 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
@@ -16,42 +16,35 @@ package validatorexit
import (
"context"
"encoding/json"
"fmt"
"os"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/util"
)
type dataOut struct {
jsonOutput bool
forkVersion spec.Version
signedVoluntaryExit *spec.SignedVoluntaryExit
}
func output(ctx context.Context, data *dataOut) (string, error) {
if data == nil {
return "", errors.New("no data")
//nolint:unparam
func (c *command) output(_ context.Context) (string, error) {
if c.quiet {
return "", nil
}
if data.signedVoluntaryExit == nil {
return "", errors.New("no signed voluntary exit")
if c.prepareOffline {
return fmt.Sprintf("%s generated", offlinePreparationFilename), nil
}
if data.jsonOutput {
return outputJSON(ctx, data)
if c.json || c.offline {
data, err := json.Marshal(c.signedOperation)
if err != nil {
return "", errors.Wrap(err, "failed to marshal signed operation")
}
if c.json {
return string(data), nil
}
if err := os.WriteFile(exitOperationFilename, data, 0600); err != nil {
return "", errors.Wrap(err, fmt.Sprintf("failed to write %s", exitOperationFilename))
}
return "", nil
}
return "", nil
}
func outputJSON(ctx context.Context, data *dataOut) (string, error) {
validatorExitData := &util.ValidatorExitData{
Exit: data.signedVoluntaryExit,
ForkVersion: data.forkVersion,
}
bytes, err := json.Marshal(validatorExitData)
if err != nil {
return "", errors.Wrap(err, "failed to generate JSON")
}
return string(bytes), nil
}

View File

@@ -1,97 +0,0 @@
// 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 validatorexit
import (
"context"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"
)
func TestOutput(t *testing.T) {
tests := []struct {
name string
dataOut *dataOut
res string
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "SignedVoluntaryExitNil",
dataOut: &dataOut{
jsonOutput: true,
},
err: "no signed voluntary exit",
},
{
name: "Good",
dataOut: &dataOut{
forkVersion: spec.Version{0x01, 0x02, 0x03, 0x04},
signedVoluntaryExit: &spec.SignedVoluntaryExit{
Message: &spec.VoluntaryExit{
Epoch: spec.Epoch(123),
ValidatorIndex: spec.ValidatorIndex(456),
},
Signature: spec.BLSSignature{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
},
},
},
},
{
name: "JSON",
dataOut: &dataOut{
jsonOutput: true,
forkVersion: spec.Version{0x01, 0x02, 0x03, 0x04},
signedVoluntaryExit: &spec.SignedVoluntaryExit{
Message: &spec.VoluntaryExit{
Epoch: spec.Epoch(123),
ValidatorIndex: spec.ValidatorIndex(456),
},
Signature: spec.BLSSignature{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
},
},
},
res: `{"exit":{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"},"fork_version":"0x01020304"}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res, err := output(context.Background(), test.dataOut)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.res, res)
}
})
}
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 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
@@ -14,120 +14,526 @@
package validatorexit
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
eth2client "github.com/attestantio/go-eth2-client"
api "github.com/attestantio/go-eth2-client/api/v1"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
consensusclient "github.com/attestantio/go-eth2-client"
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-ssz"
"github.com/wealdtech/ethdo/beacon"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/signing"
"github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
ethutil "github.com/wealdtech/go-eth2-util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
// maxFutureEpochs is the farthest in the future for which an exit will be created.
var maxFutureEpochs = spec.Epoch(1024)
// validatorPath is the regular expression that matches a validator path.
var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$")
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
var offlinePreparationFilename = "offline-preparation.json"
var exitOperationFilename = "exit-operation.json"
func (c *command) process(ctx context.Context) error {
if err := c.setup(ctx); err != nil {
return err
}
if data.epoch > data.currentEpoch {
if data.epoch-data.currentEpoch > maxFutureEpochs {
return nil, errors.New("not generating exit for an epoch in the far future")
if err := c.obtainChainInfo(ctx); err != nil {
return err
}
if c.prepareOffline {
return c.writeChainInfoToFile(ctx)
}
if err := c.generateDomain(ctx); err != nil {
return err
}
if err := c.obtainOperation(ctx); err != nil {
return err
}
if validated, reason := c.validateOperation(ctx); !validated {
return fmt.Errorf("operation failed validation: %s", reason)
}
if c.json || c.offline {
if c.debug {
fmt.Fprintf(os.Stderr, "Not broadcasting credentials change operations\n")
}
// Want JSON output, or cannot broadcast.
return nil
}
return c.broadcastOperation(ctx)
}
func (c *command) obtainOperation(ctx context.Context) error {
if (c.mnemonic == "" || c.path == "") && c.privateKey == "" && c.validator == "" {
// No input information; fetch the operation from a file.
err := c.obtainOperationFromFileOrInput(ctx)
if err == nil {
// Success.
return nil
}
if c.signedOperationInput != "" {
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))
}
if c.mnemonic != "" {
switch {
case c.path != "":
// Have a mnemonic and path.
return c.generateOperationFromMnemonicAndPath(ctx)
case c.validator != "":
// Have a mnemonic and validator.
return c.generateOperationFromMnemonicAndValidator(ctx)
default:
return errors.New("mnemonic must be supplied with either a path or validator")
}
}
results := &dataOut{
forkVersion: data.fork.CurrentVersion,
jsonOutput: data.jsonOutput,
if c.privateKey != "" {
return c.generateOperationFromPrivateKey(ctx)
}
validator, err := fetchValidator(ctx, data)
if c.validator != "" {
return c.generateOperationFromValidator(ctx)
}
return errors.New("unsupported combination of inputs; see help for details of supported combinations")
}
func (c *command) generateOperationFromMnemonicAndPath(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
}
validatorKeyPath := c.path
match := validatorPath.Match([]byte(c.path))
if !match {
return fmt.Errorf("path %s does not match EIP-2334 format for a validator", c.path)
}
if err := c.generateOperationFromSeedAndPath(ctx, validators, seed, validatorKeyPath); err != nil {
return errors.Wrap(err, "failed to generate operation from seed and path")
}
return nil
}
func (c *command) generateOperationFromMnemonicAndValidator(ctx context.Context) error {
seed, err := util.SeedFromMnemonic(c.mnemonic)
if err != nil {
return err
}
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, c.validator)
if err != nil {
return err
}
// Scan the keys from the seed to find the path.
maxDistance := 1024
// Start scanning the validator keys.
for i := 0; ; i++ {
if i == maxDistance {
if c.debug {
fmt.Fprintf(os.Stderr, "Gone %d indices without finding the validator, not scanning any further\n", maxDistance)
}
break
}
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, validatorKeyPath)
if err != nil {
return errors.Wrap(err, "failed to generate validator private key")
}
validatorPubkey := validatorPrivkey.PublicKey().Marshal()
if bytes.Equal(validatorPubkey, validatorInfo.Pubkey[:]) {
validatorAccount, err := util.ParseAccount(ctx, c.mnemonic, []string{validatorKeyPath}, true)
if err != nil {
return errors.Wrap(err, "failed to create withdrawal account")
}
err = c.generateOperationFromAccount(ctx, validatorInfo, validatorAccount, c.chainInfo.Epoch)
if err != nil {
return err
}
break
}
}
return nil
}
func (c *command) generateOperationFromPrivateKey(ctx context.Context) error {
validatorAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
if err != nil {
return errors.Wrap(err, "failed to create validator account")
}
validatorPubkey, err := util.BestPublicKey(validatorAccount)
if err != nil {
return err
}
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%#x", validatorPubkey.Marshal()))
if err != nil {
return err
}
if c.verbose {
fmt.Fprintf(os.Stderr, "Validator %d found with public key %s\n", validatorInfo.Index, validatorPubkey)
}
if err = c.generateOperationFromAccount(ctx, validatorInfo, validatorAccount, c.chainInfo.Epoch); err != nil {
return err
}
return nil
}
func (c *command) generateOperationFromValidator(ctx context.Context) error {
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, c.validator)
if err != nil {
return err
}
validatorAccount, err := util.ParseAccount(ctx, c.validator, nil, true)
if err != nil {
return err
}
if err := c.generateOperationFromAccount(ctx, validatorInfo, validatorAccount, c.chainInfo.Epoch); err != nil {
return err
}
return nil
}
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)
}
// If not, read it from the file with the standard name.
return c.obtainOperationFromFile(ctx)
}
func (c *command) obtainOperationFromFile(ctx context.Context) error {
_, err := os.Stat(exitOperationFilename)
if err != nil {
return errors.Wrap(err, "failed to read exit operation file")
}
if c.debug {
fmt.Fprintf(os.Stderr, "%s found; loading operation\n", exitOperationFilename)
}
data, err := os.ReadFile(exitOperationFilename)
if err != nil {
return errors.Wrap(err, "failed to read exit operation file")
}
if err := json.Unmarshal(data, &c.signedOperation); err != nil {
return errors.Wrap(err, "failed to parse exit operation file")
}
if err := c.verifySignedOperation(ctx, c.signedOperation); err != nil {
return err
}
return nil
}
func (c *command) obtainOperationFromInput(ctx context.Context) error {
if !strings.HasPrefix(c.signedOperationInput, "{") {
// This looks like a file; read it in.
data, err := os.ReadFile(c.signedOperationInput)
if err != nil {
return errors.Wrap(err, "failed to read input file")
}
c.signedOperationInput = string(data)
}
if err := json.Unmarshal([]byte(c.signedOperationInput), &c.signedOperation); err != nil {
return errors.Wrap(err, "failed to parse exit operation input")
}
if err := c.verifySignedOperation(ctx, c.signedOperation); err != nil {
return err
}
return nil
}
func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
validators map[string]*beacon.ValidatorInfo,
seed []byte,
path string,
) error {
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, path)
if err != nil {
return errors.Wrap(err, "failed to generate validator private key")
}
c.privateKey = fmt.Sprintf("%#x", validatorPrivkey.Marshal())
return c.generateOperationFromPrivateKey(ctx)
}
func (c *command) generateOperationFromAccount(ctx context.Context,
validator *beacon.ValidatorInfo,
account e2wtypes.Account,
epoch phase0.Epoch,
) error {
var err error
c.signedOperation, err = c.createSignedOperation(ctx, validator, account, epoch)
return err
}
func (c *command) createSignedOperation(ctx context.Context,
validator *beacon.ValidatorInfo,
account e2wtypes.Account,
epoch phase0.Epoch,
) (
*phase0.SignedVoluntaryExit,
error,
) {
pubkey, err := util.BestPublicKey(account)
if err != nil {
return nil, err
}
exit, err := generateExit(ctx, data, validator)
if err != nil {
return nil, errors.Wrap(err, "failed to generate voluntary exit")
}
root, err := exit.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to generate root for voluntary exit")
if c.debug {
fmt.Fprintf(os.Stderr, "Using %#x as best public key for %s\n", pubkey.Marshal(), account.Name())
}
blsPubkey := phase0.BLSPubKey{}
copy(blsPubkey[:], pubkey.Marshal())
if data.account != nil {
signature, err := signing.SignRoot(ctx, data.account, data.passphrases, root, data.domain)
if err != nil {
return nil, errors.Wrap(err, "failed to sign voluntary exit")
}
results.signedVoluntaryExit = &spec.SignedVoluntaryExit{
Message: exit,
Signature: signature,
}
} else {
results.signedVoluntaryExit = data.signedVoluntaryExit
}
if !data.jsonOutput {
if err := broadcastExit(ctx, data, results); err != nil {
return nil, errors.Wrap(err, "failed to broadcast voluntary exit")
}
}
return results, nil
}
func generateExit(ctx context.Context, data *dataIn, validator *api.Validator) (*spec.VoluntaryExit, error) {
if data == nil {
return nil, errors.New("no data")
}
if data.signedVoluntaryExit != nil {
return data.signedVoluntaryExit.Message, nil
}
if validator == nil {
return nil, errors.New("no validator")
}
exit := &spec.VoluntaryExit{
Epoch: data.epoch,
operation := &phase0.VoluntaryExit{
Epoch: epoch,
ValidatorIndex: validator.Index,
}
return exit, nil
}
func broadcastExit(ctx context.Context, data *dataIn, results *dataOut) error {
return data.eth2Client.(eth2client.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, results.signedVoluntaryExit)
}
func fetchValidator(ctx context.Context, data *dataIn) (*api.Validator, error) {
// Validator.
if data.account == nil {
return nil, nil
}
var validator *api.Validator
validatorPubKeys := make([]spec.BLSPubKey, 1)
pubKey, err := util.BestPublicKey(data.account)
root, err := operation.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for account")
return nil, errors.Wrap(err, "failed to generate root for exit operation")
}
copy(validatorPubKeys[0][:], pubKey.Marshal())
validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, "head", validatorPubKeys)
// Sign the operation.
if c.debug {
fmt.Fprintf(os.Stderr, "Signing %#x with domain %#x by public key %#x\n", root, c.domain, account.PublicKey().Marshal())
}
signature, err := signing.SignRoot(ctx, account, nil, root, c.domain)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain validator from beacon node")
return nil, errors.Wrap(err, "failed to sign exit operation")
}
if len(validators) == 0 {
return nil, errors.New("validator not known by beacon node")
}
for _, v := range validators {
validator = v
}
if validator.Status != api.ValidatorStateActiveOngoing {
return nil, errors.New("validator is not active; cannot exit")
}
return validator, nil
return &phase0.SignedVoluntaryExit{
Message: operation,
Signature: signature,
}, nil
}
func (c *command) verifySignedOperation(ctx context.Context, op *phase0.SignedVoluntaryExit) error {
root, err := op.Message.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to generate message root")
}
sigBytes := make([]byte, len(op.Signature))
copy(sigBytes, op.Signature[:])
sig, err := e2types.BLSSignatureFromBytes(sigBytes)
if err != nil {
if c.verbose {
fmt.Fprintf(os.Stderr, "Invalid signature: %v\n", err.Error())
}
return errors.New("invalid signature")
}
container := &phase0.SigningData{
ObjectRoot: root,
Domain: c.domain,
}
signingRoot, err := ssz.HashTreeRoot(container)
if err != nil {
return errors.Wrap(err, "failed to generate signing root")
}
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%d", op.Message.ValidatorIndex))
if err != nil {
return err
}
pubkeyBytes := make([]byte, len(validatorInfo.Pubkey[:]))
copy(pubkeyBytes, validatorInfo.Pubkey[:])
pubkey, err := e2types.BLSPublicKeyFromBytes(pubkeyBytes)
if err != nil {
return errors.Wrap(err, "invalid public key")
}
if !sig.Verify(signingRoot[:], pubkey) {
return errors.New("signature does not verify")
}
return nil
}
func (c *command) validateOperation(_ context.Context,
) (
bool,
string,
) {
var validatorInfo *beacon.ValidatorInfo
for _, chainValidatorInfo := range c.chainInfo.Validators {
if chainValidatorInfo.Index == c.signedOperation.Message.ValidatorIndex {
validatorInfo = chainValidatorInfo
break
}
}
if validatorInfo == nil {
return false, "validator not known on chain"
}
if c.debug {
fmt.Fprintf(os.Stderr, "Validator exit operation: %v", c.signedOperation)
fmt.Fprintf(os.Stderr, "On-chain validator info: %v\n", validatorInfo)
}
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.Sprintf("validator is in state %v, not suitable to generate an exit", validatorInfo.State)
}
return true, ""
}
func (c *command) broadcastOperation(ctx context.Context) error {
return c.consensusClient.(consensusclient.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, c.signedOperation)
}
func (c *command) setup(ctx context.Context) error {
if c.offline {
return nil
}
// Connect to the consensus node.
var err error
c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections)
if err != nil {
return errors.Wrap(err, "failed to connect to consensus node")
}
// Set up chaintime.
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithGenesisTimeProvider(c.consensusClient.(consensusclient.GenesisTimeProvider)),
standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)),
)
if err != nil {
return errors.Wrap(err, "failed to create chaintime service")
}
return nil
}
func (c *command) generateDomain(ctx context.Context) error {
genesisValidatorsRoot, err := c.obtainGenesisValidatorsRoot(ctx)
if err != nil {
return err
}
forkVersion, err := c.obtainForkVersion(ctx)
if err != nil {
return err
}
root, err := (&phase0.ForkData{
CurrentVersion: forkVersion,
GenesisValidatorsRoot: genesisValidatorsRoot,
}).HashTreeRoot()
if err != nil {
return errors.Wrap(err, "failed to calculate signature domain")
}
copy(c.domain[:], c.chainInfo.BLSToExecutionChangeDomainType[:])
copy(c.domain[4:], root[:])
if c.debug {
fmt.Fprintf(os.Stderr, "Domain is %#x\n", c.domain)
}
return nil
}
func (c *command) obtainGenesisValidatorsRoot(ctx context.Context) (phase0.Root, error) {
genesisValidatorsRoot := phase0.Root{}
if c.genesisValidatorsRoot != "" {
if c.debug {
fmt.Fprintf(os.Stderr, "Genesis validators root supplied on the command line\n")
}
root, err := hex.DecodeString(strings.TrimPrefix(c.genesisValidatorsRoot, "0x"))
if err != nil {
return phase0.Root{}, errors.Wrap(err, "invalid genesis validators root supplied")
}
if len(root) != phase0.RootLength {
return phase0.Root{}, errors.New("invalid length for genesis validators root")
}
copy(genesisValidatorsRoot[:], root)
} else {
if c.debug {
fmt.Fprintf(os.Stderr, "Genesis validators root obtained from chain info\n")
}
copy(genesisValidatorsRoot[:], c.chainInfo.GenesisValidatorsRoot[:])
}
if c.debug {
fmt.Fprintf(os.Stderr, "Using genesis validators root %#x\n", genesisValidatorsRoot)
}
return genesisValidatorsRoot, nil
}
func (c *command) obtainForkVersion(ctx context.Context) (phase0.Version, error) {
forkVersion := phase0.Version{}
if c.forkVersion != "" {
if c.debug {
fmt.Fprintf(os.Stderr, "Fork version supplied on the command line\n")
}
version, err := hex.DecodeString(strings.TrimPrefix(c.forkVersion, "0x"))
if err != nil {
return phase0.Version{}, errors.Wrap(err, "invalid fork version supplied")
}
if len(version) != phase0.ForkVersionLength {
return phase0.Version{}, errors.New("invalid length for fork version")
}
copy(forkVersion[:], version)
} else {
if c.debug {
fmt.Fprintf(os.Stderr, "Fork version obtained from chain info\n")
}
// Use the current fork version for generating an exit as per the spec.
copy(forkVersion[:], c.chainInfo.CurrentForkVersion[:])
}
if c.debug {
fmt.Fprintf(os.Stderr, "Using fork version %#x\n", forkVersion)
}
return forkVersion, nil
}

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 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
@@ -15,215 +15,467 @@ package validatorexit
import (
"context"
"os"
"fmt"
"testing"
"time"
api "github.com/attestantio/go-eth2-client/api/v1"
"github.com/attestantio/go-eth2-client/auto"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/spf13/viper"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"
"github.com/wealdtech/ethdo/testutil"
"github.com/wealdtech/ethdo/beacon"
e2types "github.com/wealdtech/go-eth2-types/v2"
e2wallet "github.com/wealdtech/go-eth2-wallet"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
nd "github.com/wealdtech/go-eth2-wallet-nd/v2"
scratch "github.com/wealdtech/go-eth2-wallet-store-scratch"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func TestProcess(t *testing.T) {
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
}
func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
eth2Client, err := auto.New(context.Background(),
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
)
require.NoError(t, err)
store := scratch.New()
require.NoError(t, e2wallet.UseStore(store))
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
require.NoError(t, err)
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
viper.Set("passphrase", "pass")
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
"Interop 0",
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
[]byte("pass"),
)
require.NoError(t, err)
// activeValidator := &api.Validator{
// Index: 123,
// Balance: 32123456789,
// Status: api.ValidatorStateActiveOngoing,
// Validator: &spec.Validator{
// PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
// WithdrawalCredentials: nil,
// EffectiveBalance: 32000000000,
// Slashed: false,
// ActivationEligibilityEpoch: 0,
// ActivationEpoch: 0,
// ExitEpoch: 0,
// WithdrawableEpoch: 0,
// },
// }
epochFork := &spec.Fork{
PreviousVersion: spec.Version{0x00, 0x00, 0x00, 0x00},
CurrentVersion: spec.Version{0x00, 0x00, 0x00, 0x00},
Epoch: 0,
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},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
tests := []struct {
name string
dataIn *dataIn
err string
name string
command *command
expected *phase0.SignedVoluntaryExit
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "EpochTooLate",
dataIn: &dataIn{
timeout: 5 * time.Second,
eth2Client: eth2Client,
fork: epochFork,
currentEpoch: 10,
account: interop0,
passphrases: []string{"pass"},
epoch: 9999999,
domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
name: "MnemonicInvalid",
command: &command{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
path: "m/12381/3600/0/0/0",
chainInfo: chainInfo,
},
err: "not generating exit for an epoch in the far future",
err: "mnemonic is invalid",
},
{
name: "AccountUnknown",
dataIn: &dataIn{
timeout: 5 * time.Second,
eth2Client: eth2Client,
fork: epochFork,
currentEpoch: 10,
account: interop0,
passphrases: []string{"pass"},
epoch: 10,
domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
name: "PathInvalid",
command: &command{
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",
path: "m/12381/3600/0/0",
chainInfo: chainInfo,
},
err: "validator not known by beacon node",
},
// {
// name: "Good",
// dataIn: &dataIn{
// timeout: 5 * time.Second,
// eth2Client: eth2Client,
// fork: epochFork,
// currentEpoch: 10,
// account: interop0,
// passphrases: []string{"pass"},
// epoch: 10,
// domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f},
// },
// },
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := process(context.Background(), test.dataIn)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}
func TestGenerateExit(t *testing.T) {
activeValidator := &api.Validator{
Index: 123,
Balance: 32123456789,
Status: api.ValidatorStateActiveOngoing,
Validator: &spec.Validator{
PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"),
WithdrawalCredentials: nil,
EffectiveBalance: 32000000000,
Slashed: false,
ActivationEligibilityEpoch: 0,
ActivationEpoch: 0,
ExitEpoch: 0,
WithdrawableEpoch: 0,
},
}
tests := []struct {
name string
validator *api.Validator
dataIn *dataIn
err string
}{
{
name: "Nil",
err: "no data",
},
{
name: "SignedVoluntaryExitGood",
dataIn: &dataIn{
signedVoluntaryExit: &spec.SignedVoluntaryExit{
Message: &spec.VoluntaryExit{
Epoch: spec.Epoch(123),
ValidatorIndex: spec.ValidatorIndex(456),
},
Signature: spec.BLSSignature{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
},
},
},
},
{
name: "ValidatorMissing",
dataIn: &dataIn{},
err: "no validator",
},
{
name: "ValidatorGood",
dataIn: &dataIn{},
validator: activeValidator,
err: "path m/12381/3600/0/0 does not match EIP-2334 format for a validator",
},
{
name: "Good",
dataIn: &dataIn{
signedVoluntaryExit: &spec.SignedVoluntaryExit{
Message: &spec.VoluntaryExit{
Epoch: spec.Epoch(123),
ValidatorIndex: spec.ValidatorIndex(456),
},
Signature: spec.BLSSignature{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
},
},
command: &command{
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",
path: "m/12381/3600/0/0/0",
chainInfo: chainInfo,
},
expected: &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},
},
validator: activeValidator,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := generateExit(context.Background(), test.dataIn, test.validator)
err := test.command.generateOperationFromMnemonicAndPath(ctx)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, test.command.signedOperation)
}
})
}
}
func TestGenerateOperationFromMnemonicAndValidator(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
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},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
tests := []struct {
name string
command *command
expected *phase0.SignedVoluntaryExit
err string
}{
{
name: "MnemonicInvalid",
command: &command{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
validator: "0",
chainInfo: chainInfo,
},
err: "mnemonic is invalid",
},
{
name: "ValidatorMissing",
command: &command{
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",
chainInfo: chainInfo,
},
err: "no validator specified",
},
{
name: "Good",
command: &command{
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,
},
expected: &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},
},
},
{
name: "GoodPubkey",
command: &command{
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: "0xb384f767d964e100c8a9b21018d08c25ffebae268b3ab6d610353897541971726dbfc3c7463884c68a531515aab94c87",
chainInfo: chainInfo,
},
expected: &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},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.generateOperationFromMnemonicAndValidator(ctx)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, test.command.signedOperation)
}
})
}
}
func TestGenerateOperationFromSeedAndPath(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
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},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
validators := make(map[string]*beacon.ValidatorInfo, len(chainInfo.Validators))
for i := range chainInfo.Validators {
validators[fmt.Sprintf("%#x", chainInfo.Validators[i].Pubkey)] = chainInfo.Validators[i]
}
tests := []struct {
name string
command *command
seed []byte
path string
err string
expected *phase0.SignedVoluntaryExit
}{
{
name: "PathInvalid",
command: &command{
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",
chainInfo: chainInfo,
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "invalid",
err: "failed to generate validator private key: not master at path component 0",
},
{
name: "ValidatorUnknown",
command: &command{
chainInfo: chainInfo,
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/999/0/0",
err: "unknown validator",
},
// {
// name: "ValidatorAlreadyExited",
// command: &command{
// chainInfo: chainInfo,
// },
// seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
// path: "m/12381/3600/2/0/0",
// },
{
name: "GoodPath0",
command: &command{
chainInfo: chainInfo,
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/0/0/0",
expected: &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},
},
},
{
name: "GoodPath3",
command: &command{
chainInfo: chainInfo,
},
seed: []byte{0x40, 0x8b, 0x28, 0x5c, 0x12, 0x38, 0x36, 0x00, 0x4f, 0x4b, 0x88, 0x42, 0xc8, 0x93, 0x24, 0xc1, 0xf0, 0x13, 0x82, 0x45, 0x0c, 0x0d, 0x43, 0x9a, 0xf3, 0x45, 0xba, 0x7f, 0xc4, 0x9a, 0xcf, 0x70, 0x54, 0x89, 0xc6, 0xfc, 0x77, 0xdb, 0xd4, 0xe3, 0xdc, 0x1d, 0xd8, 0xcc, 0x6b, 0xc9, 0xf0, 0x43, 0xdb, 0x8a, 0xda, 0x1e, 0x24, 0x3c, 0x4a, 0x0e, 0xaf, 0xb2, 0x90, 0xd3, 0x99, 0x48, 0x08, 0x40},
path: "m/12381/3600/3/0/0",
expected: &phase0.SignedVoluntaryExit{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 3,
},
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},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.generateOperationFromSeedAndPath(ctx, validators, test.seed, test.path)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, test.command.signedOperation)
}
})
}
}
func TestVerifyOperation(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
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},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
tests := []struct {
name string
command *command
err string
}{
{
name: "SignatureMissing",
command: &command{
chainInfo: chainInfo,
signedOperation: &phase0.SignedVoluntaryExit{
Message: &phase0.VoluntaryExit{
Epoch: 1,
ValidatorIndex: 0,
},
},
},
err: "invalid signature",
},
{
name: "SignatureShort",
command: &command{
chainInfo: chainInfo,
signedOperation: &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},
},
},
err: "invalid signature",
},
{
name: "SignatureIncorrect",
command: &command{
chainInfo: chainInfo,
signedOperation: &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},
},
},
err: "signature does not verify",
},
{
name: "Good",
command: &command{
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,
},
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},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.command.verifySignedOperation(ctx, test.command.signedOperation)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}
func TestObtainOperationFromInput(t *testing.T) {
ctx := context.Background()
require.NoError(t, e2types.InitBLS())
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},
},
},
GenesisValidatorsRoot: phase0.Root{},
Epoch: 1,
CurrentForkVersion: phase0.Version{},
}
tests := []struct {
name string
command *command
err string
}{
{
name: "InvalidFilename",
command: &command{
signedOperationInput: `[]`,
chainInfo: chainInfo,
},
err: "failed to read input file: open []: no such file or directory",
},
{
name: "InvalidJSON",
command: &command{
signedOperationInput: `{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,
},
err: "signature does not verify",
},
{
name: "Good",
command: &command{
signedOperationInput: `{"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)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {

View File

@@ -1,4 +1,4 @@
// Copyright © 2019, 2020 Weald Technology Trading
// Copyright © 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
@@ -21,19 +21,19 @@ import (
"github.com/spf13/viper"
)
// Run runs the wallet create data command.
// Run runs the command.
func Run(cmd *cobra.Command) (string, error) {
ctx := context.Background()
dataIn, err := input(ctx)
c, err := newCommand(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain input")
return "", errors.Wrap(err, "failed to set up command")
}
// Further errors do not need a usage report.
cmd.SilenceUsage = true
dataOut, err := process(ctx, dataIn)
if err != nil {
if err := c.process(ctx); err != nil {
return "", errors.Wrap(err, "failed to process")
}
@@ -41,7 +41,7 @@ func Run(cmd *cobra.Command) (string, error) {
return "", nil
}
results, err := output(ctx, dataOut)
results, err := c.output(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to obtain output")
}

View File

@@ -130,7 +130,6 @@ func (c *command) setup(ctx context.Context) error {
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -384,7 +384,6 @@ func (c *command) setup(ctx context.Context) error {
c.chainTime, err = standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -119,7 +119,6 @@ func (c *command) setup(ctx context.Context) error {
if c.validators == "" {
chainTime, err := standardchaintime.New(ctx,
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
)
if err != nil {

View File

@@ -26,9 +26,16 @@ var validatorExitCmd = &cobra.Command{
Short: "Send an exit request for a validator",
Long: `Send an exit request for a validator. For example:
ethdo validator exit --account=primary/validator --passphrase=secret
ethdo validator exit --validator=12345
In quiet mode this will return 0 if the transaction has been generated, otherwise 1.`,
The validator and key can be specified in one of a number of ways:
- 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
- validator account using --validator
In quiet mode this will return 0 if the exit operation has been generated (and successfully broadcast if online), otherwise 1.`,
RunE: func(cmd *cobra.Command, args []string) error {
res, err := validatorexit.Run(cmd)
if err != nil {
@@ -48,22 +55,38 @@ func init() {
validatorCmd.AddCommand(validatorExitCmd)
validatorFlags(validatorExitCmd)
validatorExitCmd.Flags().Int64("epoch", -1, "Epoch at which to exit (defaults to current epoch)")
validatorExitCmd.Flags().String("key", "", "Private key if validator not known by ethdo")
validatorExitCmd.Flags().String("exit", "", "Use pre-defined JSON data as created by --json to exit")
validatorExitCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
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-operations.json if not present)")
validatorExitCmd.Flags().Bool("json", false, "Generate JSON data containing a signed operation rather than broadcast it to the network (implied when offline)")
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)")
}
func validatorExitBindings() {
if err := viper.BindPFlag("epoch", validatorExitCmd.Flags().Lookup("epoch")); err != nil {
panic(err)
}
if err := viper.BindPFlag("key", validatorExitCmd.Flags().Lookup("key")); err != nil {
if err := viper.BindPFlag("prepare-offline", validatorExitCmd.Flags().Lookup("prepare-offline")); err != nil {
panic(err)
}
if err := viper.BindPFlag("exit", validatorExitCmd.Flags().Lookup("exit")); err != nil {
if err := viper.BindPFlag("validator", validatorExitCmd.Flags().Lookup("validator")); err != nil {
panic(err)
}
if err := viper.BindPFlag("signed-operation", validatorExitCmd.Flags().Lookup("signed-operation")); err != nil {
panic(err)
}
if err := viper.BindPFlag("json", validatorExitCmd.Flags().Lookup("json")); err != nil {
panic(err)
}
if err := viper.BindPFlag("offline", validatorExitCmd.Flags().Lookup("offline")); err != nil {
panic(err)
}
if err := viper.BindPFlag("fork-version", validatorExitCmd.Flags().Lookup("fork-version")); err != nil {
panic(err)
}
if err := viper.BindPFlag("genesis-validators-root", validatorExitCmd.Flags().Lookup("genesis-validators-root")); err != nil {
panic(err)
}
}

7
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/wealdtech/ethdo
go 1.18
require (
github.com/attestantio/go-eth2-client v0.15.1
github.com/attestantio/go-eth2-client v0.15.2
github.com/ferranbt/fastssz v0.1.2
github.com/gofrs/uuid v4.2.0+incompatible
github.com/google/uuid v1.3.0
@@ -21,6 +21,7 @@ require (
github.com/spf13/viper v1.13.0
github.com/stretchr/testify v1.8.1
github.com/tyler-smith/go-bip39 v1.1.0
github.com/wealdtech/chaind v0.6.17
github.com/wealdtech/go-bytesutil v1.2.0
github.com/wealdtech/go-ecodec v1.1.2
github.com/wealdtech/go-eth2-types/v2 v2.8.0
@@ -50,7 +51,7 @@ require (
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-yaml v1.9.2 // indirect
github.com/goccy/go-yaml v1.9.5 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@@ -73,7 +74,7 @@ require (
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/protolambda/zssz v0.1.5 // indirect
github.com/r3labs/sse/v2 v2.7.4 // indirect
github.com/r3labs/sse/v2 v2.8.1 // indirect
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/cast v1.5.0 // indirect

12
go.sum
View File

@@ -67,8 +67,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/attestantio/go-eth2-client v0.15.1 h1:LbtRcjc1eXbtsi9TeCYYFV7VtvLYIOrXQkXjFW0B1SE=
github.com/attestantio/go-eth2-client v0.15.1/go.mod h1:5kLLzdlyPGboWr8tAwnG/4Kpi43BHd/HWp++WmmP6Ws=
github.com/attestantio/go-eth2-client v0.15.2 h1:4EYeA5IBSBypkUMhkkFALzMddaFDdb5PvCl7ORXEl6w=
github.com/attestantio/go-eth2-client v0.15.2/go.mod h1:/Oh6YTuHmHhgLN/ZnQRKHGc7HdIzGlDkI2vjNZvOsvA=
github.com/aws/aws-sdk-go v1.40.41/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.44.152 h1:L9aaepO8wHB67gwuGD8VgIYH/cmQDxieCt7FeLa0+fI=
github.com/aws/aws-sdk-go v1.44.152/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
@@ -162,8 +162,9 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-yaml v1.9.2 h1:2Njwzw+0+pjU2gb805ZC1B/uBuAs2VcZ3K+ZgHwDs7w=
github.com/goccy/go-yaml v1.9.2/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@@ -438,8 +439,9 @@ github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388 h1:4bD+ujqGfY
github.com/prysmaticlabs/go-ssz v0.0.0-20210121151755-f6208871c388/go.mod h1:VecIJZrewdAuhVckySLFt2wAAHRME934bSDurP8ftkc=
github.com/prysmaticlabs/gohashtree v0.0.1-alpha.0.20220714111606-acbb2962fb48 h1:cSo6/vk8YpvkLbk9v3FO97cakNmUoxwi2KMP8hd5WIw=
github.com/prysmaticlabs/gohashtree v0.0.1-alpha.0.20220714111606-acbb2962fb48/go.mod h1:4pWaT30XoEx1j8KNJf3TV+E3mQkaufn7mf+jRNb/Fuk=
github.com/r3labs/sse/v2 v2.7.4 h1:pvCMswPDlXd/ZUFx1dry0LbXJNHXwWPulLcUGYwClc0=
github.com/r3labs/sse/v2 v2.7.4/go.mod h1:hUrYMKfu9WquG9MyI0r6TKiNH+6Sw/QPKm2YbNbU5g8=
github.com/r3labs/sse/v2 v2.8.1 h1:lZH+W4XOLIq88U5MIHOsLec7+R62uhz3bIi2yn0Sg8o=
github.com/r3labs/sse/v2 v2.8.1/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
@@ -498,6 +500,8 @@ github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNG
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/wealdtech/chaind v0.6.17 h1:HBlmzKj9Egy9rnZKGGIwvM6mUHJ+64163hNhSwjg/FQ=
github.com/wealdtech/chaind v0.6.17/go.mod h1:g8XOXrrRtwjD6mlpn9TydRPJD+gy4iFZMlPkQrBxxQA=
github.com/wealdtech/eth2-signer-api v1.7.1 h1:XdwFuv3VWCwcPPPrfa77sUXL1GSvxDtsUZxlByz//b0=
github.com/wealdtech/eth2-signer-api v1.7.1/go.mod h1:fX8XtN9Svyjs+e7TgoOfOcwRTHeblR5SXftAVV3T1ZA=
github.com/wealdtech/go-bytesutil v1.0.1/go.mod h1:jENeMqeTEU8FNZyDFRVc7KqBdRKSnJ9CCh26TcuNb9s=

View File

@@ -20,10 +20,9 @@ import (
)
type parameters struct {
logLevel zerolog.Level
genesisTimeProvider eth2client.GenesisTimeProvider
specProvider eth2client.SpecProvider
forkScheduleProvider eth2client.ForkScheduleProvider
logLevel zerolog.Level
genesisTimeProvider eth2client.GenesisTimeProvider
specProvider eth2client.SpecProvider
}
// Parameter is the interface for service parameters.
@@ -58,13 +57,6 @@ func WithSpecProvider(provider eth2client.SpecProvider) Parameter {
})
}
// WithForkScheduleProvider sets the fork schedule provider.
func WithForkScheduleProvider(provider eth2client.ForkScheduleProvider) Parameter {
return parameterFunc(func(p *parameters) {
p.forkScheduleProvider = provider
})
}
// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct.
func parseAndCheckParameters(params ...Parameter) (*parameters, error) {
parameters := parameters{
@@ -82,9 +74,6 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) {
if parameters.genesisTimeProvider == nil {
return nil, errors.New("no genesis time provider specified")
}
if parameters.forkScheduleProvider == nil {
return nil, errors.New("no fork schedule provider specified")
}
return &parameters, nil
}

View File

@@ -14,7 +14,6 @@
package standard
import (
"bytes"
"context"
"time"
@@ -87,21 +86,21 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
epochsPerSyncCommitteePeriod = tmp2
}
altairForkEpoch, err := fetchAltairForkEpoch(ctx, parameters.forkScheduleProvider)
altairForkEpoch, err := fetchAltairForkEpoch(ctx, parameters.specProvider)
if err != nil {
// Set to far future epoch.
altairForkEpoch = 0xffffffffffffffff
}
log.Trace().Uint64("epoch", uint64(altairForkEpoch)).Msg("Obtained Altair fork epoch")
bellatrixForkEpoch, err := fetchBellatrixForkEpoch(ctx, parameters.forkScheduleProvider)
bellatrixForkEpoch, err := fetchBellatrixForkEpoch(ctx, parameters.specProvider)
if err != nil {
// Set to far future epoch.
bellatrixForkEpoch = 0xffffffffffffffff
}
log.Trace().Uint64("epoch", uint64(bellatrixForkEpoch)).Msg("Obtained Bellatrix fork epoch")
capellaForkEpoch, err := fetchCapellaForkEpoch(ctx, parameters.forkScheduleProvider)
capellaForkEpoch, err := fetchCapellaForkEpoch(ctx, parameters.specProvider)
if err != nil {
// Set to far future epoch.
capellaForkEpoch = 0xffffffffffffffff
@@ -217,19 +216,28 @@ func (s *Service) AltairInitialSyncCommitteePeriod() uint64 {
return uint64(s.altairForkEpoch) / s.epochsPerSyncCommitteePeriod
}
func fetchAltairForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
forkSchedule, err := provider.ForkSchedule(ctx)
func fetchAltairForkEpoch(ctx context.Context,
specProvider eth2client.SpecProvider,
) (
phase0.Epoch,
error,
) {
// Fetch the fork version.
spec, err := specProvider.Spec(ctx)
if err != nil {
return 0, err
return 0, errors.Wrap(err, "failed to obtain spec")
}
for i := range forkSchedule {
if bytes.Equal(forkSchedule[i].CurrentVersion[:], forkSchedule[i].PreviousVersion[:]) {
// This is the genesis fork; ignore it.
continue
}
return forkSchedule[i].Epoch, nil
tmp, exists := spec["ALTAIR_FORK_EPOCH"]
if !exists {
return 0, errors.New("altair fork version not known by chain")
}
return 0, errors.New("no altair fork obtained")
epoch, isEpoch := tmp.(uint64)
if !isEpoch {
//nolint:revive
return 0, errors.New("ALTAIR_FORK_EPOCH is not a uint64!")
}
return phase0.Epoch(epoch), nil
}
// BellatrixInitialEpoch provides the epoch at which the Bellatrix hard fork takes place.
@@ -237,24 +245,28 @@ func (s *Service) BellatrixInitialEpoch() phase0.Epoch {
return s.bellatrixForkEpoch
}
func fetchBellatrixForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
forkSchedule, err := provider.ForkSchedule(ctx)
func fetchBellatrixForkEpoch(ctx context.Context,
specProvider eth2client.SpecProvider,
) (
phase0.Epoch,
error,
) {
// Fetch the fork version.
spec, err := specProvider.Spec(ctx)
if err != nil {
return 0, err
return 0, errors.Wrap(err, "failed to obtain spec")
}
count := 0
for i := range forkSchedule {
count++
if bytes.Equal(forkSchedule[i].CurrentVersion[:], forkSchedule[i].PreviousVersion[:]) {
// This is the genesis fork; ignore it.
continue
}
if count == 1 {
return forkSchedule[i].Epoch, nil
}
count++
tmp, exists := spec["BELLATRIX_FORK_EPOCH"]
if !exists {
return 0, errors.New("bellatrix fork version not known by chain")
}
return 0, errors.New("no bellatrix fork obtained")
epoch, isEpoch := tmp.(uint64)
if !isEpoch {
//nolint:revive
return 0, errors.New("BELLATRIX_FORK_EPOCH is not a uint64!")
}
return phase0.Epoch(epoch), nil
}
// CapellaInitialEpoch provides the epoch at which the Capella hard fork takes place.
@@ -262,22 +274,26 @@ func (s *Service) CapellaInitialEpoch() phase0.Epoch {
return s.capellaForkEpoch
}
func fetchCapellaForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
forkSchedule, err := provider.ForkSchedule(ctx)
func fetchCapellaForkEpoch(ctx context.Context,
specProvider eth2client.SpecProvider,
) (
phase0.Epoch,
error,
) {
// Fetch the fork version.
spec, err := specProvider.Spec(ctx)
if err != nil {
return 0, err
return 0, errors.Wrap(err, "failed to obtain spec")
}
count := 0
for i := range forkSchedule {
count++
if bytes.Equal(forkSchedule[i].CurrentVersion[:], forkSchedule[i].PreviousVersion[:]) {
// This is the genesis fork; ignore it.
continue
}
if count == 2 {
return forkSchedule[i].Epoch, nil
}
count++
tmp, exists := spec["CAPELLAELLATRIX_FORK_EPOCH"]
if !exists {
return 0, errors.New("capella fork version not known by chain")
}
return 0, errors.New("no capella fork obtained")
epoch, isEpoch := tmp.(uint64)
if !isEpoch {
//nolint:revive
return 0, errors.New("CAPELLAELLATRIX_FORK_EPOCH is not a uint64!")
}
return phase0.Epoch(epoch), nil
}

View File

@@ -31,22 +31,9 @@ func TestService(t *testing.T) {
slotDuration := 12 * time.Second
slotsPerEpoch := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
forkSchedule := []*phase0.Fork{
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
Epoch: 0,
},
{
PreviousVersion: phase0.Version{0x01, 0x02, 0x03, 0x04},
CurrentVersion: phase0.Version{0x05, 0x06, 0x07, 0x08},
Epoch: 10,
},
}
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
mockForkScheduleProvider := mock.NewForkScheduleProvider(forkSchedule)
tests := []struct {
name string
@@ -58,7 +45,6 @@ func TestService(t *testing.T) {
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithSpecProvider(mockSpecProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
},
err: "problem with parameters: no genesis time provider specified",
},
@@ -67,26 +53,15 @@ func TestService(t *testing.T) {
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
},
err: "problem with parameters: no spec provider specified",
},
{
name: "ForkScheduleProviderMissing",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithSpecProvider(mockSpecProvider),
},
err: "problem with parameters: no fork schedule provider specified",
},
{
name: "Good",
params: []standard.Parameter{
standard.WithLogLevel(zerolog.Disabled),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithSpecProvider(mockSpecProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
},
},
}
@@ -123,11 +98,9 @@ func createService(genesisTime time.Time) (chaintime.Service, time.Duration, uin
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
mockForkScheduleProvider := mock.NewForkScheduleProvider(forkSchedule)
s, err := standard.New(context.Background(),
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
standard.WithSpecProvider(mockSpecProvider),
standard.WithForkScheduleProvider(mockForkScheduleProvider),
)
return s, slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod, forkSchedule, err
}

View File

@@ -61,7 +61,7 @@ func ParseAccount(ctx context.Context,
// Private key.
account, err = newScratchAccountFromPrivKey(data)
if err != nil {
return nil, errors.Wrap(err, "failed to create account from public key")
return nil, errors.Wrap(err, "failed to create account from private key")
}
if unlock {
_, err = UnlockAccount(ctx, account, nil)

View File

@@ -24,11 +24,15 @@ import (
// ParseEpoch parses input to calculate the desired epoch.
func ParseEpoch(ctx context.Context, chainTime chaintime.Service, epochStr string) (phase0.Epoch, error) {
currentEpoch := chainTime.CurrentEpoch()
switch epochStr {
case "", "current":
return chainTime.CurrentEpoch(), nil
case "", "current", "-0":
return currentEpoch, nil
case "last":
return chainTime.CurrentEpoch() - 1, nil
if currentEpoch > 0 {
currentEpoch--
}
return currentEpoch, nil
default:
val, err := strconv.ParseInt(epochStr, 10, 64)
if err != nil {
@@ -37,6 +41,9 @@ func ParseEpoch(ctx context.Context, chainTime chaintime.Service, epochStr strin
if val >= 0 {
return phase0.Epoch(val), nil
}
return chainTime.CurrentEpoch() + phase0.Epoch(val), nil
if phase0.Epoch(-val) > currentEpoch {
return 0, nil
}
return currentEpoch + phase0.Epoch(val), nil
}
}

105
util/epoch_test.go Normal file
View File

@@ -0,0 +1,105 @@
// Copyright © 2032 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 util_test
import (
"context"
"testing"
"time"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
"github.com/wealdtech/ethdo/testing/mock"
"github.com/wealdtech/ethdo/util"
)
func TestParseEpoch(t *testing.T) {
ctx := context.Background()
// genesis is 1 day ago.
genesisTime := time.Now().AddDate(0, 0, -1)
slotDuration := 12 * time.Second
slotsPerEpoch := uint64(32)
epochsPerSyncCommitteePeriod := uint64(256)
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
chainTime, err := standardchaintime.New(context.Background(),
standardchaintime.WithLogLevel(zerolog.Disabled),
standardchaintime.WithGenesisTimeProvider(mockGenesisTimeProvider),
standardchaintime.WithSpecProvider(mockSpecProvider),
)
require.NoError(t, err)
tests := []struct {
name string
input string
err string
expected phase0.Epoch
}{
{
name: "Genesis",
input: "0",
expected: 0,
},
{
name: "Invalid",
input: "invalid",
err: `failed to parse epoch: strconv.ParseInt: parsing "invalid": invalid syntax`,
},
{
name: "Absolute",
input: "15",
expected: 15,
},
{
name: "Current",
input: "current",
expected: 225,
},
{
name: "Last",
input: "last",
expected: 224,
},
{
name: "RelativeZero",
input: "-0",
expected: 225,
},
{
name: "Relative",
input: "-5",
expected: 220,
},
{
name: "RelativeFar",
input: "-500",
expected: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
epoch, err := util.ParseEpoch(ctx, chainTime, test.input)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
require.Equal(t, test.expected, epoch)
}
})
}
}