mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-07 21:24:01 -05:00
Update code for spec change.
This commit is contained in:
@@ -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
300
beacon/chaininfo.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
372
cmd/validator/credentials/set/process_internal_test.go
Normal file
372
cmd/validator/credentials/set/process_internal_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
105
cmd/validator/exit/chaininfo.go
Normal file
105
cmd/validator/exit/chaininfo.go
Normal 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
|
||||
}
|
||||
97
cmd/validator/exit/command.go
Normal file
97
cmd/validator/exit/command.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
7
go.mod
@@ -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
12
go.sum
@@ -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=
|
||||
|
||||
@@ -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 ¶meters, nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
105
util/epoch_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user