mirror of
https://github.com/wealdtech/ethdo.git
synced 2026-01-10 14:37:57 -05:00
Update code for spec change.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
dev:
|
1.27.0:
|
||||||
- use new build system
|
- use new build system
|
||||||
- support S3 credentials
|
- support S3 credentials
|
||||||
|
- update operation of validator exit to match validator credentials set
|
||||||
|
|
||||||
1.26.5:
|
1.26.5:
|
||||||
- provide validator information in "chain status" verbose output
|
- 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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package validatorcredentialsset
|
package beacon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -20,33 +20,37 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type validatorInfo struct {
|
type ValidatorInfo struct {
|
||||||
Index phase0.ValidatorIndex
|
Index phase0.ValidatorIndex
|
||||||
Pubkey phase0.BLSPubKey
|
Pubkey phase0.BLSPubKey
|
||||||
|
State apiv1.ValidatorState
|
||||||
WithdrawalCredentials []byte
|
WithdrawalCredentials []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type validatorInfoJSON struct {
|
type validatorInfoJSON struct {
|
||||||
Index string `json:"index"`
|
Index string `json:"index"`
|
||||||
Pubkey string `json:"pubkey"`
|
Pubkey string `json:"pubkey"`
|
||||||
WithdrawalCredentials string `json:"withdrawal_credentials"`
|
State apiv1.ValidatorState `json:"state"`
|
||||||
|
WithdrawalCredentials string `json:"withdrawal_credentials"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements json.Marshaler.
|
// MarshalJSON implements json.Marshaler.
|
||||||
func (v *validatorInfo) MarshalJSON() ([]byte, error) {
|
func (v *ValidatorInfo) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(&validatorInfoJSON{
|
return json.Marshal(&validatorInfoJSON{
|
||||||
Index: fmt.Sprintf("%d", v.Index),
|
Index: fmt.Sprintf("%d", v.Index),
|
||||||
Pubkey: fmt.Sprintf("%#x", v.Pubkey),
|
Pubkey: fmt.Sprintf("%#x", v.Pubkey),
|
||||||
|
State: v.State,
|
||||||
WithdrawalCredentials: fmt.Sprintf("%#x", v.WithdrawalCredentials),
|
WithdrawalCredentials: fmt.Sprintf("%#x", v.WithdrawalCredentials),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON implements json.Unmarshaler.
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
func (v *validatorInfo) UnmarshalJSON(input []byte) error {
|
func (v *ValidatorInfo) UnmarshalJSON(input []byte) error {
|
||||||
var data validatorInfoJSON
|
var data validatorInfoJSON
|
||||||
if err := json.Unmarshal(input, &data); err != nil {
|
if err := json.Unmarshal(input, &data); err != nil {
|
||||||
return errors.Wrap(err, "invalid JSON")
|
return errors.Wrap(err, "invalid JSON")
|
||||||
@@ -73,6 +77,11 @@ func (v *validatorInfo) UnmarshalJSON(input []byte) error {
|
|||||||
}
|
}
|
||||||
copy(v.Pubkey[:], pubkey)
|
copy(v.Pubkey[:], pubkey)
|
||||||
|
|
||||||
|
if data.State == apiv1.ValidatorStateUnknown {
|
||||||
|
return errors.New("state unknown")
|
||||||
|
}
|
||||||
|
v.State = data.State
|
||||||
|
|
||||||
if data.WithdrawalCredentials == "" {
|
if data.WithdrawalCredentials == "" {
|
||||||
return errors.New("withdrawal credentials missing")
|
return errors.New("withdrawal credentials missing")
|
||||||
}
|
}
|
||||||
@@ -88,7 +97,7 @@ func (v *validatorInfo) UnmarshalJSON(input []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// String implements the Stringer interface.
|
// String implements the Stringer interface.
|
||||||
func (v *validatorInfo) String() string {
|
func (v *ValidatorInfo) String() string {
|
||||||
data, err := json.Marshal(v)
|
data, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("Err: %v\n", err)
|
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,
|
data.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(data.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -266,7 +266,6 @@ func (c *command) setup(ctx context.Context) error {
|
|||||||
|
|
||||||
c.chainTime, err = standardchaintime.New(ctx,
|
c.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ func (c *command) setup(ctx context.Context) error {
|
|||||||
|
|
||||||
c.chainTime, err = standardchaintime.New(ctx,
|
c.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ func (c *command) setup(ctx context.Context) error {
|
|||||||
|
|
||||||
c.chainTime, err = standardchaintime.New(ctx,
|
c.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
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,
|
chainTime, err := standardchaintime.New(ctx,
|
||||||
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||||
)
|
)
|
||||||
errCheck(err, "Failed to configure chaintime service")
|
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,
|
c.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ func (c *command) setup(ctx context.Context) error {
|
|||||||
|
|
||||||
c.chainTime, err = standardchaintime.New(ctx,
|
c.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/wealdtech/ethdo/util"
|
"github.com/wealdtech/ethdo/util"
|
||||||
"github.com/wealdtech/go-bytesutil"
|
"github.com/wealdtech/go-bytesutil"
|
||||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
e2types "github.com/wealdtech/go-eth2-types/v2"
|
||||||
|
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// signatureSignCmd represents the signature sign command
|
// 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))
|
outputIf(debug, fmt.Sprintf("Domain is %#x", domain))
|
||||||
|
|
||||||
assert(viper.GetString("account") != "", "--account is required")
|
var account e2wtypes.Account
|
||||||
_, account, err := walletAndAccountFromInput(ctx)
|
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")
|
errCheck(err, "Failed to obtain account")
|
||||||
|
|
||||||
var specDomain spec.Domain
|
var specDomain spec.Domain
|
||||||
copy(specDomain[:], domain)
|
copy(specDomain[:], domain)
|
||||||
var fixedSizeData [32]byte
|
var fixedSizeData [32]byte
|
||||||
copy(fixedSizeData[:], data)
|
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)
|
signature, err := util.SignRoot(account, fixedSizeData, specDomain)
|
||||||
errCheck(err, "Failed to sign")
|
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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
@@ -15,13 +15,10 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/wealdtech/ethdo/util"
|
"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.`,
|
In quiet mode this will return 0 if the data can be signed, otherwise 1.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
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")
|
assert(viper.GetString("signature-data") != "", "--data is required")
|
||||||
data, err := bytesutil.FromHexString(viper.GetString("signature-data"))
|
data, err := bytesutil.FromHexString(viper.GetString("signature-data"))
|
||||||
errCheck(err, "Failed to parse 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")
|
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")
|
errCheck(err, "Failed to obtain account")
|
||||||
outputIf(debug, fmt.Sprintf("Public key is %#x", account.PublicKey().Marshal()))
|
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() {
|
func init() {
|
||||||
signatureCmd.AddCommand(signatureVerifyCmd)
|
signatureCmd.AddCommand(signatureVerifyCmd)
|
||||||
signatureFlags(signatureVerifyCmd)
|
signatureFlags(signatureVerifyCmd)
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ func (c *command) setup(ctx context.Context) error {
|
|||||||
|
|
||||||
c.chainTime, err = standardchaintime.New(ctx,
|
c.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ func input(ctx context.Context) (*dataIn, error) {
|
|||||||
// Chain time.
|
// Chain time.
|
||||||
data.chainTime, err = standardchaintime.New(ctx,
|
data.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(data.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ func TestProcess(t *testing.T) {
|
|||||||
|
|
||||||
chainTime, err := standardchaintime.New(context.Background(),
|
chainTime, err := standardchaintime.New(context.Background(),
|
||||||
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)),
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
@@ -14,114 +14,92 @@
|
|||||||
package validatorcredentialsset
|
package validatorcredentialsset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/wealdtech/ethdo/beacon"
|
||||||
)
|
)
|
||||||
|
|
||||||
type chainInfo struct {
|
// obtainChainInfo obtains the chain information required to create a withdrawal credentials change operation.
|
||||||
Version uint64
|
func (c *command) obtainChainInfo(ctx context.Context) error {
|
||||||
Validators []*validatorInfo
|
// Use the offline preparation file if present (and we haven't been asked to recreate it).
|
||||||
GenesisValidatorsRoot phase0.Root
|
if !c.prepareOffline {
|
||||||
Epoch phase0.Epoch
|
err := c.obtainChainInfoFromFile(ctx)
|
||||||
ForkVersion phase0.Version
|
if err == nil {
|
||||||
Domain phase0.Domain
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
v.Version = version
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data.Validators) == 0 {
|
if c.offline {
|
||||||
return errors.New("validators missing")
|
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)
|
||||||
}
|
|
||||||
v.Validators = data.Validators
|
|
||||||
|
|
||||||
if data.GenesisValidatorsRoot == "" {
|
|
||||||
return errors.New("genesis validators root missing")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
genesisValidatorsRootBytes, err := hex.DecodeString(strings.TrimPrefix(data.GenesisValidatorsRoot, "0x"))
|
if err := c.obtainChainInfoFromNode(ctx); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return errors.Wrap(err, "genesis validators root invalid")
|
}
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ import (
|
|||||||
consensusclient "github.com/attestantio/go-eth2-client"
|
consensusclient "github.com/attestantio/go-eth2-client"
|
||||||
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
"github.com/attestantio/go-eth2-client/spec/bellatrix"
|
||||||
capella "github.com/attestantio/go-eth2-client/spec/capella"
|
capella "github.com/attestantio/go-eth2-client/spec/capella"
|
||||||
|
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"github.com/wealdtech/ethdo/beacon"
|
||||||
"github.com/wealdtech/ethdo/services/chaintime"
|
"github.com/wealdtech/ethdo/services/chaintime"
|
||||||
"github.com/wealdtech/ethdo/util"
|
"github.com/wealdtech/ethdo/util"
|
||||||
)
|
)
|
||||||
@@ -54,7 +56,8 @@ type command struct {
|
|||||||
|
|
||||||
// Information required to generate the operations.
|
// Information required to generate the operations.
|
||||||
withdrawalAddress bellatrix.ExecutionAddress
|
withdrawalAddress bellatrix.ExecutionAddress
|
||||||
chainInfo *chainInfo
|
chainInfo *beacon.ChainInfo
|
||||||
|
domain phase0.Domain
|
||||||
|
|
||||||
// Processing.
|
// Processing.
|
||||||
consensusClient consensusclient.Service
|
consensusClient consensusclient.Service
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ func (c *command) output(_ context.Context) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.prepareOffline {
|
||||||
|
return fmt.Sprintf("%s generated", offlinePreparationFilename), nil
|
||||||
|
}
|
||||||
|
|
||||||
if c.json || c.offline {
|
if c.json || c.offline {
|
||||||
data, err := json.Marshal(c.signedOperations)
|
data, err := json.Marshal(c.signedOperations)
|
||||||
if err != nil {
|
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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
consensusclient "github.com/attestantio/go-eth2-client"
|
consensusclient "github.com/attestantio/go-eth2-client"
|
||||||
@@ -30,6 +29,7 @@ import (
|
|||||||
"github.com/attestantio/go-eth2-client/spec/phase0"
|
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prysmaticlabs/go-ssz"
|
"github.com/prysmaticlabs/go-ssz"
|
||||||
|
"github.com/wealdtech/ethdo/beacon"
|
||||||
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard"
|
||||||
"github.com/wealdtech/ethdo/signing"
|
"github.com/wealdtech/ethdo/signing"
|
||||||
"github.com/wealdtech/ethdo/util"
|
"github.com/wealdtech/ethdo/util"
|
||||||
@@ -49,15 +49,19 @@ func (c *command) process(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.obtainRequiredInformation(ctx); err != nil {
|
if err := c.obtainChainInfo(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.prepareOffline {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,204 +80,28 @@ func (c *command) process(ctx context.Context) error {
|
|||||||
return c.broadcastOperations(ctx)
|
return c.broadcastOperations(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// obtainRequiredInformation obtains the information required to create a
|
func (c *command) obtainOperations(ctx context.Context) error {
|
||||||
// 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 {
|
|
||||||
if c.account == "" && c.mnemonic == "" && c.privateKey == "" && c.validator == "" {
|
if c.account == "" && c.mnemonic == "" && c.privateKey == "" && c.validator == "" {
|
||||||
// No input information; fetch the operations from a file.
|
// No input information; fetch the operations from a file.
|
||||||
err := c.loadOperations(ctx)
|
err := c.obtainOperationsFromFileOrInput(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Success.
|
// Success.
|
||||||
return nil
|
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 != "" {
|
if c.mnemonic != "" {
|
||||||
switch {
|
switch {
|
||||||
case c.path != "":
|
case c.path != "":
|
||||||
// Have a mnemonic and path.
|
// Have a mnemonic and path.
|
||||||
return c.generateOperationsFromMnemonicAndPath(ctx)
|
return c.generateOperationFromMnemonicAndPath(ctx)
|
||||||
case c.validator != "":
|
case c.validator != "":
|
||||||
// Have a mnemonic and validator.
|
// Have a mnemonic and validator.
|
||||||
return c.generateOperationsFromMnemonicAndValidator(ctx)
|
return c.generateOperationFromMnemonicAndValidator(ctx)
|
||||||
case c.privateKey != "":
|
case c.privateKey != "":
|
||||||
// Have a mnemonic and a private key for the withdrawal address.
|
// Have a mnemonic and a private key for the withdrawal address.
|
||||||
return c.generateOperationsFromMnemonicAndPrivateKey(ctx)
|
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")
|
return errors.New("unsupported combination of inputs; see help for details of supported combinations")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *command) loadChainInfo(_ context.Context) error {
|
func (c *command) generateOperationFromMnemonicAndPath(ctx context.Context) error {
|
||||||
_, err := os.Stat(offlinePreparationFilename)
|
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if c.debug {
|
return err
|
||||||
fmt.Fprintf(os.Stderr, "Failed to read offline preparation file: %v\n", 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))
|
validatorKeyPath := fmt.Sprintf("m/12381/3600/%d/0/0", i)
|
||||||
}
|
validatorPrivkey, err := ethutil.PrivateKeyFromSeedAndPath(seed, validatorKeyPath)
|
||||||
|
|
||||||
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)
|
|
||||||
if err != nil {
|
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 {
|
err = c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount)
|
||||||
return errors.Wrap(err, "failed to parse change operations input")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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 {
|
func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
|
||||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
||||||
if err != nil {
|
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.
|
// 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 {
|
for _, validator := range c.chainInfo.Validators {
|
||||||
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
|
validators[fmt.Sprintf("%#x", validator.Pubkey)] = validator
|
||||||
}
|
}
|
||||||
@@ -448,70 +241,141 @@ func (c *command) generateOperationsFromMnemonic(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *command) generateOperationsFromMnemonicAndPrivateKey(ctx context.Context) error {
|
func (c *command) generateOperationsFromAccountAndWithdrawalAccount(ctx context.Context) error {
|
||||||
// Functionally identical to a simple scan, so use that.
|
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
|
||||||
return c.generateOperationsFromMnemonic(ctx)
|
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 {
|
func (c *command) generateOperationsFromAccountAndPrivateKey(ctx context.Context) error {
|
||||||
seed, err := util.SeedFromMnemonic(c.mnemonic)
|
validatorAccount, err := util.ParseAccount(ctx, c.account, nil, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
validator, err := c.fetchValidatorInfo(ctx)
|
withdrawalAccount, err := util.ParseAccount(ctx, c.privateKey, nil, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan the keys from the seed to find the path.
|
validatorPubkey, err := util.BestPublicKey(validatorAccount)
|
||||||
maxDistance := 1024
|
if err != nil {
|
||||||
// Start scanning the validator keys.
|
return err
|
||||||
for i := 0; ; i++ {
|
}
|
||||||
if i == maxDistance {
|
validatorInfo, err := c.chainInfo.FetchValidatorInfo(ctx, fmt.Sprintf("%#x", validatorPubkey.Marshal()))
|
||||||
if c.debug {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Gone %d indices without finding the validator, not scanning any further\n", maxDistance)
|
return errors.Wrap(err, "failed to obtain validator info")
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
withdrawalAccount, err := util.ParseAccount(ctx, c.mnemonic, []string{withdrawalKeyPath}, true)
|
if err := c.generateOperationFromAccount(ctx, validatorInfo, withdrawalAccount); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return errors.Wrap(err, "failed to create withdrawal account")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
err = c.generateOperationFromAccount(ctx, validator, withdrawalAccount)
|
return nil
|
||||||
if err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
func (c *command) generateOperationsFromValidatorAndPrivateKey(ctx context.Context) error {
|
||||||
break
|
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
|
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,
|
func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
|
||||||
validators map[string]*validatorInfo,
|
validators map[string]*beacon.ValidatorInfo,
|
||||||
seed []byte,
|
seed []byte,
|
||||||
path string,
|
path string,
|
||||||
) (
|
) (
|
||||||
@@ -587,7 +451,7 @@ func (c *command) generateOperationFromSeedAndPath(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *command) generateOperationFromAccount(ctx context.Context,
|
func (c *command) generateOperationFromAccount(ctx context.Context,
|
||||||
validator *validatorInfo,
|
validator *beacon.ValidatorInfo,
|
||||||
withdrawalAccount e2wtypes.Account,
|
withdrawalAccount e2wtypes.Account,
|
||||||
) error {
|
) error {
|
||||||
signedOperation, err := c.createSignedOperation(ctx, validator, withdrawalAccount)
|
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,
|
func (c *command) createSignedOperation(ctx context.Context,
|
||||||
validator *validatorInfo,
|
validator *beacon.ValidatorInfo,
|
||||||
withdrawalAccount e2wtypes.Account,
|
withdrawalAccount e2wtypes.Account,
|
||||||
) (
|
) (
|
||||||
*capella.SignedBLSToExecutionChange,
|
*capella.SignedBLSToExecutionChange,
|
||||||
@@ -630,7 +494,10 @@ func (c *command) createSignedOperation(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sign the operation.
|
// 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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to sign credentials change operation")
|
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) {
|
func (c *command) validateOperations(ctx context.Context) (bool, string) {
|
||||||
// Turn the validators in to a map for easy lookup.
|
// 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 {
|
for _, validator := range c.chainInfo.Validators {
|
||||||
validators[validator.Index] = validator
|
validators[validator.Index] = validator
|
||||||
}
|
}
|
||||||
@@ -674,8 +541,43 @@ func (c *command) validateOperations(ctx context.Context) (bool, string) {
|
|||||||
return true, ""
|
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,
|
func (c *command) validateOperation(_ context.Context,
|
||||||
validators map[phase0.ValidatorIndex]*validatorInfo,
|
validators map[phase0.ValidatorIndex]*beacon.ValidatorInfo,
|
||||||
signedOperation *capella.SignedBLSToExecutionChange,
|
signedOperation *capella.SignedBLSToExecutionChange,
|
||||||
) (
|
) (
|
||||||
bool,
|
bool,
|
||||||
@@ -725,7 +627,6 @@ func (c *command) setup(ctx context.Context) error {
|
|||||||
// Set up chaintime.
|
// Set up chaintime.
|
||||||
c.chainTime, err = standardchaintime.New(ctx,
|
c.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithGenesisTimeProvider(c.consensusClient.(consensusclient.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.consensusClient.(consensusclient.GenesisTimeProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.consensusClient.(consensusclient.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.consensusClient.(consensusclient.SpecProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -735,56 +636,88 @@ func (c *command) setup(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *command) fetchValidatorInfo(ctx context.Context) (*validatorInfo, error) {
|
func (c *command) generateDomain(ctx context.Context) error {
|
||||||
var validatorInfo *validatorInfo
|
genesisValidatorsRoot, err := c.obtainGenesisValidatorsRoot(ctx)
|
||||||
switch {
|
if err != nil {
|
||||||
case c.validator == "":
|
return err
|
||||||
return nil, errors.New("no validator specified")
|
}
|
||||||
case strings.HasPrefix(c.validator, "0x"):
|
forkVersion, err := c.obtainForkVersion(ctx)
|
||||||
// A public key
|
if err != nil {
|
||||||
for _, validator := range c.chainInfo.Validators {
|
return err
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if validatorInfo == nil {
|
root, err := (&phase0.ForkData{
|
||||||
return nil, errors.New("unknown validator")
|
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.
|
// 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))
|
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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
@@ -16,42 +16,35 @@ package validatorexit
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/wealdtech/ethdo/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type dataOut struct {
|
//nolint:unparam
|
||||||
jsonOutput bool
|
func (c *command) output(_ context.Context) (string, error) {
|
||||||
forkVersion spec.Version
|
if c.quiet {
|
||||||
signedVoluntaryExit *spec.SignedVoluntaryExit
|
return "", nil
|
||||||
}
|
|
||||||
|
|
||||||
func output(ctx context.Context, data *dataOut) (string, error) {
|
|
||||||
if data == nil {
|
|
||||||
return "", errors.New("no data")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.signedVoluntaryExit == nil {
|
if c.prepareOffline {
|
||||||
return "", errors.New("no signed voluntary exit")
|
return fmt.Sprintf("%s generated", offlinePreparationFilename), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.jsonOutput {
|
if c.json || c.offline {
|
||||||
return outputJSON(ctx, data)
|
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
|
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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
@@ -14,120 +14,526 @@
|
|||||||
package validatorexit
|
package validatorexit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
eth2client "github.com/attestantio/go-eth2-client"
|
consensusclient "github.com/attestantio/go-eth2-client"
|
||||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
apiv1 "github.com/attestantio/go-eth2-client/api/v1"
|
||||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||||
"github.com/pkg/errors"
|
"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/signing"
|
||||||
"github.com/wealdtech/ethdo/util"
|
"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.
|
// validatorPath is the regular expression that matches a validator path.
|
||||||
var maxFutureEpochs = spec.Epoch(1024)
|
var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$")
|
||||||
|
|
||||||
func process(ctx context.Context, data *dataIn) (*dataOut, error) {
|
var offlinePreparationFilename = "offline-preparation.json"
|
||||||
if data == nil {
|
var exitOperationFilename = "exit-operation.json"
|
||||||
return nil, errors.New("no data")
|
|
||||||
|
func (c *command) process(ctx context.Context) error {
|
||||||
|
if err := c.setup(ctx); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.epoch > data.currentEpoch {
|
if err := c.obtainChainInfo(ctx); err != nil {
|
||||||
if data.epoch-data.currentEpoch > maxFutureEpochs {
|
return err
|
||||||
return nil, errors.New("not generating exit for an epoch in the far future")
|
}
|
||||||
|
|
||||||
|
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,
|
if c.privateKey != "" {
|
||||||
jsonOutput: data.jsonOutput,
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if c.debug {
|
||||||
exit, err := generateExit(ctx, data, validator)
|
fmt.Fprintf(os.Stderr, "Using %#x as best public key for %s\n", pubkey.Marshal(), account.Name())
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
blsPubkey := phase0.BLSPubKey{}
|
||||||
|
copy(blsPubkey[:], pubkey.Marshal())
|
||||||
|
|
||||||
if data.account != nil {
|
operation := &phase0.VoluntaryExit{
|
||||||
signature, err := signing.SignRoot(ctx, data.account, data.passphrases, root, data.domain)
|
Epoch: epoch,
|
||||||
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,
|
|
||||||
ValidatorIndex: validator.Index,
|
ValidatorIndex: validator.Index,
|
||||||
}
|
}
|
||||||
return exit, nil
|
root, err := operation.HashTreeRoot()
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
if err != nil {
|
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 {
|
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")
|
return &phase0.SignedVoluntaryExit{
|
||||||
}
|
Message: operation,
|
||||||
for _, v := range validators {
|
Signature: signature,
|
||||||
validator = v
|
}, nil
|
||||||
}
|
}
|
||||||
if validator.Status != api.ValidatorStateActiveOngoing {
|
|
||||||
return nil, errors.New("validator is not active; cannot exit")
|
func (c *command) verifySignedOperation(ctx context.Context, op *phase0.SignedVoluntaryExit) error {
|
||||||
}
|
root, err := op.Message.HashTreeRoot()
|
||||||
return validator, nil
|
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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
@@ -15,215 +15,467 @@ package validatorexit
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
api "github.com/attestantio/go-eth2-client/api/v1"
|
"github.com/attestantio/go-eth2-client/spec/phase0"
|
||||||
"github.com/attestantio/go-eth2-client/auto"
|
|
||||||
spec "github.com/attestantio/go-eth2-client/spec/phase0"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/wealdtech/ethdo/testutil"
|
"github.com/wealdtech/ethdo/beacon"
|
||||||
e2types "github.com/wealdtech/go-eth2-types/v2"
|
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) {
|
func TestGenerateOperationFromMnemonicAndPath(t *testing.T) {
|
||||||
if os.Getenv("ETHDO_TEST_CONNECTION") == "" {
|
ctx := context.Background()
|
||||||
t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests")
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, e2types.InitBLS())
|
require.NoError(t, e2types.InitBLS())
|
||||||
|
|
||||||
eth2Client, err := auto.New(context.Background(),
|
chainInfo := &beacon.ChainInfo{
|
||||||
auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")),
|
Version: 1,
|
||||||
)
|
Validators: []*beacon.ValidatorInfo{
|
||||||
require.NoError(t, err)
|
{
|
||||||
store := scratch.New()
|
Index: 0,
|
||||||
require.NoError(t, e2wallet.UseStore(store))
|
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},
|
||||||
testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New())
|
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},
|
||||||
require.NoError(t, err)
|
},
|
||||||
require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil))
|
{
|
||||||
viper.Set("passphrase", "pass")
|
Index: 1,
|
||||||
interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(),
|
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},
|
||||||
"Interop 0",
|
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},
|
||||||
testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"),
|
},
|
||||||
[]byte("pass"),
|
},
|
||||||
)
|
GenesisValidatorsRoot: phase0.Root{},
|
||||||
require.NoError(t, err)
|
Epoch: 1,
|
||||||
|
CurrentForkVersion: phase0.Version{},
|
||||||
// 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
dataIn *dataIn
|
command *command
|
||||||
err string
|
expected *phase0.SignedVoluntaryExit
|
||||||
|
err string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Nil",
|
name: "MnemonicInvalid",
|
||||||
err: "no data",
|
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",
|
||||||
name: "EpochTooLate",
|
chainInfo: chainInfo,
|
||||||
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},
|
|
||||||
},
|
},
|
||||||
err: "not generating exit for an epoch in the far future",
|
err: "mnemonic is invalid",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "AccountUnknown",
|
name: "PathInvalid",
|
||||||
dataIn: &dataIn{
|
command: &command{
|
||||||
timeout: 5 * time.Second,
|
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",
|
||||||
eth2Client: eth2Client,
|
path: "m/12381/3600/0/0",
|
||||||
fork: epochFork,
|
chainInfo: chainInfo,
|
||||||
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},
|
|
||||||
},
|
},
|
||||||
err: "validator not known by beacon node",
|
err: "path m/12381/3600/0/0 does not match EIP-2334 format for a validator",
|
||||||
},
|
|
||||||
// {
|
|
||||||
// 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,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Good",
|
name: "Good",
|
||||||
dataIn: &dataIn{
|
command: &command{
|
||||||
signedVoluntaryExit: &spec.SignedVoluntaryExit{
|
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",
|
||||||
Message: &spec.VoluntaryExit{
|
path: "m/12381/3600/0/0/0",
|
||||||
Epoch: spec.Epoch(123),
|
chainInfo: chainInfo,
|
||||||
ValidatorIndex: spec.ValidatorIndex(456),
|
},
|
||||||
},
|
expected: &phase0.SignedVoluntaryExit{
|
||||||
Signature: spec.BLSSignature{
|
Message: &phase0.VoluntaryExit{
|
||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
Epoch: 1,
|
||||||
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
ValidatorIndex: 0,
|
||||||
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,
|
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},
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
validator: activeValidator,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
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 != "" {
|
if test.err != "" {
|
||||||
require.EqualError(t, err, test.err)
|
require.EqualError(t, err, test.err)
|
||||||
} else {
|
} 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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
@@ -21,19 +21,19 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run runs the wallet create data command.
|
// Run runs the command.
|
||||||
func Run(cmd *cobra.Command) (string, error) {
|
func Run(cmd *cobra.Command) (string, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
dataIn, err := input(ctx)
|
|
||||||
|
c, err := newCommand(ctx)
|
||||||
if err != nil {
|
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.
|
// Further errors do not need a usage report.
|
||||||
cmd.SilenceUsage = true
|
cmd.SilenceUsage = true
|
||||||
|
|
||||||
dataOut, err := process(ctx, dataIn)
|
if err := c.process(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrap(err, "failed to process")
|
return "", errors.Wrap(err, "failed to process")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ func Run(cmd *cobra.Command) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := output(ctx, dataOut)
|
results, err := c.output(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "failed to obtain output")
|
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,
|
chainTime, err := standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -384,7 +384,6 @@ func (c *command) setup(ctx context.Context) error {
|
|||||||
|
|
||||||
c.chainTime, err = standardchaintime.New(ctx,
|
c.chainTime, err = standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ func (c *command) setup(ctx context.Context) error {
|
|||||||
if c.validators == "" {
|
if c.validators == "" {
|
||||||
chainTime, err := standardchaintime.New(ctx,
|
chainTime, err := standardchaintime.New(ctx,
|
||||||
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
standardchaintime.WithSpecProvider(c.eth2Client.(eth2client.SpecProvider)),
|
||||||
standardchaintime.WithForkScheduleProvider(c.eth2Client.(eth2client.ForkScheduleProvider)),
|
|
||||||
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
standardchaintime.WithGenesisTimeProvider(c.eth2Client.(eth2client.GenesisTimeProvider)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -26,9 +26,16 @@ var validatorExitCmd = &cobra.Command{
|
|||||||
Short: "Send an exit request for a validator",
|
Short: "Send an exit request for a validator",
|
||||||
Long: `Send an exit request for a validator. For example:
|
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 {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
res, err := validatorexit.Run(cmd)
|
res, err := validatorexit.Run(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -48,22 +55,38 @@ func init() {
|
|||||||
validatorCmd.AddCommand(validatorExitCmd)
|
validatorCmd.AddCommand(validatorExitCmd)
|
||||||
validatorFlags(validatorExitCmd)
|
validatorFlags(validatorExitCmd)
|
||||||
validatorExitCmd.Flags().Int64("epoch", -1, "Epoch at which to exit (defaults to current epoch)")
|
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().Bool("prepare-offline", false, "Create files for offline use")
|
||||||
validatorExitCmd.Flags().String("exit", "", "Use pre-defined JSON data as created by --json to exit")
|
validatorExitCmd.Flags().String("validator", "", "Validator to exit")
|
||||||
validatorExitCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network")
|
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() {
|
func validatorExitBindings() {
|
||||||
if err := viper.BindPFlag("epoch", validatorExitCmd.Flags().Lookup("epoch")); err != nil {
|
if err := viper.BindPFlag("epoch", validatorExitCmd.Flags().Lookup("epoch")); err != nil {
|
||||||
panic(err)
|
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)
|
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)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := viper.BindPFlag("json", validatorExitCmd.Flags().Lookup("json")); err != nil {
|
if err := viper.BindPFlag("json", validatorExitCmd.Flags().Lookup("json")); err != nil {
|
||||||
panic(err)
|
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
|
go 1.18
|
||||||
|
|
||||||
require (
|
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/ferranbt/fastssz v0.1.2
|
||||||
github.com/gofrs/uuid v4.2.0+incompatible
|
github.com/gofrs/uuid v4.2.0+incompatible
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
@@ -21,6 +21,7 @@ require (
|
|||||||
github.com/spf13/viper v1.13.0
|
github.com/spf13/viper v1.13.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0
|
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-bytesutil v1.2.0
|
||||||
github.com/wealdtech/go-ecodec v1.1.2
|
github.com/wealdtech/go-ecodec v1.1.2
|
||||||
github.com/wealdtech/go-eth2-types/v2 v2.8.0
|
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/fsnotify/fsnotify v1.5.4 // indirect
|
||||||
github.com/go-logr/logr v1.2.3 // indirect
|
github.com/go-logr/logr v1.2.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // 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/glog v1.0.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // 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/common v0.37.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
github.com/protolambda/zssz v0.1.5 // 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/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 // indirect
|
||||||
github.com/spf13/afero v1.9.2 // indirect
|
github.com/spf13/afero v1.9.2 // indirect
|
||||||
github.com/spf13/cast v1.5.0 // 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-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 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/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.2 h1:4EYeA5IBSBypkUMhkkFALzMddaFDdb5PvCl7ORXEl6w=
|
||||||
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/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.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 h1:L9aaepO8wHB67gwuGD8VgIYH/cmQDxieCt7FeLa0+fI=
|
||||||
github.com/aws/aws-sdk-go v1.44.152/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
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 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
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/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.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/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 h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
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/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 h1:cSo6/vk8YpvkLbk9v3FO97cakNmUoxwi2KMP8hd5WIw=
|
||||||
github.com/prysmaticlabs/gohashtree v0.0.1-alpha.0.20220714111606-acbb2962fb48/go.mod h1:4pWaT30XoEx1j8KNJf3TV+E3mQkaufn7mf+jRNb/Fuk=
|
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.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/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.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
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/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 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||||
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
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 h1:XdwFuv3VWCwcPPPrfa77sUXL1GSvxDtsUZxlByz//b0=
|
||||||
github.com/wealdtech/eth2-signer-api v1.7.1/go.mod h1:fX8XtN9Svyjs+e7TgoOfOcwRTHeblR5SXftAVV3T1ZA=
|
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=
|
github.com/wealdtech/go-bytesutil v1.0.1/go.mod h1:jENeMqeTEU8FNZyDFRVc7KqBdRKSnJ9CCh26TcuNb9s=
|
||||||
|
|||||||
@@ -20,10 +20,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type parameters struct {
|
type parameters struct {
|
||||||
logLevel zerolog.Level
|
logLevel zerolog.Level
|
||||||
genesisTimeProvider eth2client.GenesisTimeProvider
|
genesisTimeProvider eth2client.GenesisTimeProvider
|
||||||
specProvider eth2client.SpecProvider
|
specProvider eth2client.SpecProvider
|
||||||
forkScheduleProvider eth2client.ForkScheduleProvider
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameter is the interface for service parameters.
|
// 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.
|
// parseAndCheckParameters parses and checks parameters to ensure that mandatory parameters are present and correct.
|
||||||
func parseAndCheckParameters(params ...Parameter) (*parameters, error) {
|
func parseAndCheckParameters(params ...Parameter) (*parameters, error) {
|
||||||
parameters := parameters{
|
parameters := parameters{
|
||||||
@@ -82,9 +74,6 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) {
|
|||||||
if parameters.genesisTimeProvider == nil {
|
if parameters.genesisTimeProvider == nil {
|
||||||
return nil, errors.New("no genesis time provider specified")
|
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
|
return ¶meters, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
package standard
|
package standard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -87,21 +86,21 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
|
|||||||
epochsPerSyncCommitteePeriod = tmp2
|
epochsPerSyncCommitteePeriod = tmp2
|
||||||
}
|
}
|
||||||
|
|
||||||
altairForkEpoch, err := fetchAltairForkEpoch(ctx, parameters.forkScheduleProvider)
|
altairForkEpoch, err := fetchAltairForkEpoch(ctx, parameters.specProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Set to far future epoch.
|
// Set to far future epoch.
|
||||||
altairForkEpoch = 0xffffffffffffffff
|
altairForkEpoch = 0xffffffffffffffff
|
||||||
}
|
}
|
||||||
log.Trace().Uint64("epoch", uint64(altairForkEpoch)).Msg("Obtained Altair fork epoch")
|
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 {
|
if err != nil {
|
||||||
// Set to far future epoch.
|
// Set to far future epoch.
|
||||||
bellatrixForkEpoch = 0xffffffffffffffff
|
bellatrixForkEpoch = 0xffffffffffffffff
|
||||||
}
|
}
|
||||||
log.Trace().Uint64("epoch", uint64(bellatrixForkEpoch)).Msg("Obtained Bellatrix fork epoch")
|
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 {
|
if err != nil {
|
||||||
// Set to far future epoch.
|
// Set to far future epoch.
|
||||||
capellaForkEpoch = 0xffffffffffffffff
|
capellaForkEpoch = 0xffffffffffffffff
|
||||||
@@ -217,19 +216,28 @@ func (s *Service) AltairInitialSyncCommitteePeriod() uint64 {
|
|||||||
return uint64(s.altairForkEpoch) / s.epochsPerSyncCommitteePeriod
|
return uint64(s.altairForkEpoch) / s.epochsPerSyncCommitteePeriod
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchAltairForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
|
func fetchAltairForkEpoch(ctx context.Context,
|
||||||
forkSchedule, err := provider.ForkSchedule(ctx)
|
specProvider eth2client.SpecProvider,
|
||||||
|
) (
|
||||||
|
phase0.Epoch,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Fetch the fork version.
|
||||||
|
spec, err := specProvider.Spec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, errors.Wrap(err, "failed to obtain spec")
|
||||||
}
|
}
|
||||||
for i := range forkSchedule {
|
tmp, exists := spec["ALTAIR_FORK_EPOCH"]
|
||||||
if bytes.Equal(forkSchedule[i].CurrentVersion[:], forkSchedule[i].PreviousVersion[:]) {
|
if !exists {
|
||||||
// This is the genesis fork; ignore it.
|
return 0, errors.New("altair fork version not known by chain")
|
||||||
continue
|
|
||||||
}
|
|
||||||
return forkSchedule[i].Epoch, nil
|
|
||||||
}
|
}
|
||||||
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.
|
// 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
|
return s.bellatrixForkEpoch
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchBellatrixForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
|
func fetchBellatrixForkEpoch(ctx context.Context,
|
||||||
forkSchedule, err := provider.ForkSchedule(ctx)
|
specProvider eth2client.SpecProvider,
|
||||||
|
) (
|
||||||
|
phase0.Epoch,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Fetch the fork version.
|
||||||
|
spec, err := specProvider.Spec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, errors.Wrap(err, "failed to obtain spec")
|
||||||
}
|
}
|
||||||
count := 0
|
tmp, exists := spec["BELLATRIX_FORK_EPOCH"]
|
||||||
for i := range forkSchedule {
|
if !exists {
|
||||||
count++
|
return 0, errors.New("bellatrix fork version not known by chain")
|
||||||
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++
|
|
||||||
}
|
}
|
||||||
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.
|
// 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
|
return s.capellaForkEpoch
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchCapellaForkEpoch(ctx context.Context, provider eth2client.ForkScheduleProvider) (phase0.Epoch, error) {
|
func fetchCapellaForkEpoch(ctx context.Context,
|
||||||
forkSchedule, err := provider.ForkSchedule(ctx)
|
specProvider eth2client.SpecProvider,
|
||||||
|
) (
|
||||||
|
phase0.Epoch,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// Fetch the fork version.
|
||||||
|
spec, err := specProvider.Spec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, errors.Wrap(err, "failed to obtain spec")
|
||||||
}
|
}
|
||||||
count := 0
|
tmp, exists := spec["CAPELLAELLATRIX_FORK_EPOCH"]
|
||||||
for i := range forkSchedule {
|
if !exists {
|
||||||
count++
|
return 0, errors.New("capella fork version not known by chain")
|
||||||
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++
|
|
||||||
}
|
}
|
||||||
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
|
slotDuration := 12 * time.Second
|
||||||
slotsPerEpoch := uint64(32)
|
slotsPerEpoch := uint64(32)
|
||||||
epochsPerSyncCommitteePeriod := uint64(256)
|
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)
|
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
|
||||||
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
|
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
|
||||||
mockForkScheduleProvider := mock.NewForkScheduleProvider(forkSchedule)
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -58,7 +45,6 @@ func TestService(t *testing.T) {
|
|||||||
params: []standard.Parameter{
|
params: []standard.Parameter{
|
||||||
standard.WithLogLevel(zerolog.Disabled),
|
standard.WithLogLevel(zerolog.Disabled),
|
||||||
standard.WithSpecProvider(mockSpecProvider),
|
standard.WithSpecProvider(mockSpecProvider),
|
||||||
standard.WithForkScheduleProvider(mockForkScheduleProvider),
|
|
||||||
},
|
},
|
||||||
err: "problem with parameters: no genesis time provider specified",
|
err: "problem with parameters: no genesis time provider specified",
|
||||||
},
|
},
|
||||||
@@ -67,26 +53,15 @@ func TestService(t *testing.T) {
|
|||||||
params: []standard.Parameter{
|
params: []standard.Parameter{
|
||||||
standard.WithLogLevel(zerolog.Disabled),
|
standard.WithLogLevel(zerolog.Disabled),
|
||||||
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
|
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
|
||||||
standard.WithForkScheduleProvider(mockForkScheduleProvider),
|
|
||||||
},
|
},
|
||||||
err: "problem with parameters: no spec provider specified",
|
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",
|
name: "Good",
|
||||||
params: []standard.Parameter{
|
params: []standard.Parameter{
|
||||||
standard.WithLogLevel(zerolog.Disabled),
|
standard.WithLogLevel(zerolog.Disabled),
|
||||||
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
|
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
|
||||||
standard.WithSpecProvider(mockSpecProvider),
|
standard.WithSpecProvider(mockSpecProvider),
|
||||||
standard.WithForkScheduleProvider(mockForkScheduleProvider),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -123,11 +98,9 @@ func createService(genesisTime time.Time) (chaintime.Service, time.Duration, uin
|
|||||||
|
|
||||||
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
|
mockGenesisTimeProvider := mock.NewGenesisTimeProvider(genesisTime)
|
||||||
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
|
mockSpecProvider := mock.NewSpecProvider(slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod)
|
||||||
mockForkScheduleProvider := mock.NewForkScheduleProvider(forkSchedule)
|
|
||||||
s, err := standard.New(context.Background(),
|
s, err := standard.New(context.Background(),
|
||||||
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
|
standard.WithGenesisTimeProvider(mockGenesisTimeProvider),
|
||||||
standard.WithSpecProvider(mockSpecProvider),
|
standard.WithSpecProvider(mockSpecProvider),
|
||||||
standard.WithForkScheduleProvider(mockForkScheduleProvider),
|
|
||||||
)
|
)
|
||||||
return s, slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod, forkSchedule, err
|
return s, slotDuration, slotsPerEpoch, epochsPerSyncCommitteePeriod, forkSchedule, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func ParseAccount(ctx context.Context,
|
|||||||
// Private key.
|
// Private key.
|
||||||
account, err = newScratchAccountFromPrivKey(data)
|
account, err = newScratchAccountFromPrivKey(data)
|
||||||
if err != nil {
|
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 {
|
if unlock {
|
||||||
_, err = UnlockAccount(ctx, account, nil)
|
_, err = UnlockAccount(ctx, account, nil)
|
||||||
|
|||||||
@@ -24,11 +24,15 @@ import (
|
|||||||
|
|
||||||
// ParseEpoch parses input to calculate the desired epoch.
|
// ParseEpoch parses input to calculate the desired epoch.
|
||||||
func ParseEpoch(ctx context.Context, chainTime chaintime.Service, epochStr string) (phase0.Epoch, error) {
|
func ParseEpoch(ctx context.Context, chainTime chaintime.Service, epochStr string) (phase0.Epoch, error) {
|
||||||
|
currentEpoch := chainTime.CurrentEpoch()
|
||||||
switch epochStr {
|
switch epochStr {
|
||||||
case "", "current":
|
case "", "current", "-0":
|
||||||
return chainTime.CurrentEpoch(), nil
|
return currentEpoch, nil
|
||||||
case "last":
|
case "last":
|
||||||
return chainTime.CurrentEpoch() - 1, nil
|
if currentEpoch > 0 {
|
||||||
|
currentEpoch--
|
||||||
|
}
|
||||||
|
return currentEpoch, nil
|
||||||
default:
|
default:
|
||||||
val, err := strconv.ParseInt(epochStr, 10, 64)
|
val, err := strconv.ParseInt(epochStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -37,6 +41,9 @@ func ParseEpoch(ctx context.Context, chainTime chaintime.Service, epochStr strin
|
|||||||
if val >= 0 {
|
if val >= 0 {
|
||||||
return phase0.Epoch(val), nil
|
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