diff --git a/CHANGELOG.md b/CHANGELOG.md index ab81b5f..1f54af1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ -dev: +1.29.0: - allow use of keystores with validator credentials set + - tidy up various command options to provide more standard usage + - add mainnet fallback beacon node 1.28.4: - allow validator exit to use a keystore as its validator parameter diff --git a/cmd/attester/duties/input.go b/cmd/attester/duties/input.go index 37077d7..c3c0595 100644 --- a/cmd/attester/duties/input.go +++ b/cmd/attester/duties/input.go @@ -18,9 +18,11 @@ import ( "time" eth2client "github.com/attestantio/go-eth2-client" - spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" "github.com/spf13/viper" + "github.com/wealdtech/ethdo/services/chaintime" + standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard" "github.com/wealdtech/ethdo/util" ) @@ -31,13 +33,11 @@ type dataIn struct { verbose bool debug bool json bool - // Chain information. - slotsPerEpoch uint64 // Operation. - account string - pubKey string + validator string eth2Client eth2client.Service - epoch spec.Epoch + chainTime chaintime.Service + epoch phase0.Epoch } func input(ctx context.Context) (*dataIn, error) { @@ -52,38 +52,37 @@ func input(ctx context.Context) (*dataIn, error) { data.debug = viper.GetBool("debug") data.json = viper.GetBool("json") - // Account or pubkey. - if viper.GetString("account") == "" && viper.GetString("pubkey") == "" { - return nil, errors.New("account or pubkey is required") + // Validator. + data.validator = viper.GetString("validator") + if data.validator == "" { + return nil, errors.New("validator is required") } - data.account = viper.GetString("account") - data.pubKey = viper.GetString("pubkey") // Ethereum 2 client. var err error - data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !data.quiet, + }) if err != nil { return nil, err } - // Required data. - config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx) + data.chainTime, err = standardchaintime.New(ctx, + standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)), + standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)), + ) if err != nil { - return nil, errors.Wrap(err, "failed to obtain beacon chain configuration") + return nil, errors.Wrap(err, "failed to set up chaintime service") } - data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64) - // Epoch - epoch := viper.GetInt64("epoch") - if epoch == -1 { - slotDuration := config["SECONDS_PER_SLOT"].(time.Duration) - genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain genesis data") - } - epoch = int64(time.Since(genesis.GenesisTime).Seconds()) / (int64(slotDuration.Seconds()) * int64(data.slotsPerEpoch)) + // Epoch. + data.epoch, err = util.ParseEpoch(ctx, data.chainTime, viper.GetString("epoch")) + if err != nil { + return nil, err } - data.epoch = spec.Epoch(epoch) return data, nil } diff --git a/cmd/attester/duties/input_internal_test.go b/cmd/attester/duties/input_internal_test.go index 900b842..abf242c 100644 --- a/cmd/attester/duties/input_internal_test.go +++ b/cmd/attester/duties/input_internal_test.go @@ -17,6 +17,7 @@ import ( "context" "os" "testing" + "time" "github.com/spf13/viper" "github.com/stretchr/testify/require" @@ -61,19 +62,21 @@ func TestInput(t *testing.T) { err: "timeout is required", }, { - name: "AccountMissing", + name: "ValidatorMissing", vars: map[string]interface{}{ "timeout": "5s", }, - err: "account or pubkey is required", + err: "validator is required", }, { - name: "ConnectionMissing", + name: "Good", vars: map[string]interface{}{ - "timeout": "5s", - "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "timeout": "5s", + "validator": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + }, + res: &dataIn{ + timeout: 5 * time.Second, }, - err: "failed to connect to any beacon node", }, } diff --git a/cmd/attester/duties/process.go b/cmd/attester/duties/process.go index 1c7744a..1549cf0 100644 --- a/cmd/attester/duties/process.go +++ b/cmd/attester/duties/process.go @@ -15,16 +15,12 @@ package attesterduties import ( "context" - "encoding/hex" - "fmt" - "strings" eth2client "github.com/attestantio/go-eth2-client" api "github.com/attestantio/go-eth2-client/api/v1" spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" "github.com/wealdtech/ethdo/util" - e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) func process(ctx context.Context, data *dataIn) (*dataOut, error) { @@ -32,43 +28,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { return nil, errors.New("no data") } - var account e2wtypes.Account - var err error - if data.account != "" { - ctx, cancel := context.WithTimeout(ctx, data.timeout) - defer cancel() - _, account, err = util.WalletAndAccountFromPath(ctx, data.account) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain account") - } - } else { - pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.pubKey, "0x")) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", data.pubKey)) - } - account, err = util.NewScratchAccount(nil, pubKeyBytes) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", data.pubKey)) - } - } - - // Fetch validator - pubKeys := make([]spec.BLSPubKey, 1) - pubKey, err := util.BestPublicKey(account) + validator, err := util.ParseValidator(ctx, data.eth2Client.(eth2client.ValidatorsProvider), data.validator, "head") if err != nil { - return nil, errors.Wrap(err, "failed to obtain public key for account") - } - copy(pubKeys[0][:], pubKey.Marshal()) - validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), pubKeys) - if err != nil { - return nil, errors.New("failed to obtain validator information") - } - if len(validators) == 0 { - return nil, errors.New("validator is not known") - } - var validator *api.Validator - for _, v := range validators { - validator = v + return nil, err } results := &dataOut{ diff --git a/cmd/attester/duties/process_internal_test.go b/cmd/attester/duties/process_internal_test.go index b1a31b4..43866ba 100644 --- a/cmd/attester/duties/process_internal_test.go +++ b/cmd/attester/duties/process_internal_test.go @@ -18,9 +18,11 @@ import ( "os" "testing" + eth2client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/auto" "github.com/rs/zerolog" "github.com/stretchr/testify/require" + standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard" ) func TestProcess(t *testing.T) { @@ -33,6 +35,12 @@ func TestProcess(t *testing.T) { ) require.NoError(t, err) + chainTime, err := standardchaintime.New(context.Background(), + standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)), + standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)), + ) + require.NoError(t, err) + tests := []struct { name string dataIn *dataIn @@ -45,10 +53,9 @@ func TestProcess(t *testing.T) { { name: "Client", dataIn: &dataIn{ - eth2Client: eth2Client, - slotsPerEpoch: 32, - pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95", - epoch: 100, + eth2Client: eth2Client, + chainTime: chainTime, + validator: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95", }, }, } diff --git a/cmd/attester/inclusion/input.go b/cmd/attester/inclusion/input.go index fbafc06..ff9f055 100644 --- a/cmd/attester/inclusion/input.go +++ b/cmd/attester/inclusion/input.go @@ -15,7 +15,6 @@ package attesterinclusion import ( "context" - "fmt" "time" eth2client "github.com/attestantio/go-eth2-client" @@ -23,6 +22,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/viper" "github.com/wealdtech/ethdo/services/chaintime" + standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard" "github.com/wealdtech/ethdo/util" ) @@ -32,15 +32,11 @@ type dataIn struct { quiet bool verbose bool debug bool - // Chain information. - slotsPerEpoch uint64 // Operation. eth2Client eth2client.Service chainTime chaintime.Service epoch spec.Epoch - account string - pubKey string - index string + validator string } func input(ctx context.Context) (*dataIn, error) { @@ -54,48 +50,35 @@ func input(ctx context.Context) (*dataIn, error) { data.verbose = viper.GetBool("verbose") data.debug = viper.GetBool("debug") - // Account. - data.account = viper.GetString("account") - - // PubKey. - data.pubKey = viper.GetString("pubkey") - - // ID. - data.index = viper.GetString("index") - - if viper.GetString("account") == "" && viper.GetString("index") == "" && viper.GetString("pubkey") == "" { - return nil, errors.New("account, index or pubkey is required") + data.validator = viper.GetString("validator") + if data.validator == "" { + return nil, errors.New("validator is required") } // Ethereum 2 client. var err error - data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !data.quiet, + }) if err != nil { return nil, err } - config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx) + data.chainTime, err = standardchaintime.New(ctx, + standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)), + standardchaintime.WithGenesisTimeProvider(data.eth2Client.(eth2client.GenesisTimeProvider)), + ) if err != nil { - return nil, errors.Wrap(err, "failed to obtain beacon chain configuration") + return nil, errors.Wrap(err, "failed to set up chaintime service") } - data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64) // Epoch. - epoch := viper.GetInt64("epoch") - if epoch == -1 { - slotDuration := config["SECONDS_PER_SLOT"].(time.Duration) - genesis, err := data.eth2Client.(eth2client.GenesisProvider).Genesis(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain genesis data") - } - epoch = int64(time.Since(genesis.GenesisTime).Seconds()) / (int64(slotDuration.Seconds()) * int64(data.slotsPerEpoch)) - if epoch > 0 { - epoch-- - } - } - data.epoch = spec.Epoch(epoch) - if data.debug { - fmt.Printf("Epoch is %d\n", data.epoch) + data.epoch, err = util.ParseEpoch(ctx, data.chainTime, viper.GetString("epoch")) + if err != nil { + return nil, err } return data, nil diff --git a/cmd/attester/inclusion/input_internal_test.go b/cmd/attester/inclusion/input_internal_test.go index 44ec4f4..4ad5357 100644 --- a/cmd/attester/inclusion/input_internal_test.go +++ b/cmd/attester/inclusion/input_internal_test.go @@ -17,6 +17,7 @@ import ( "context" "os" "testing" + "time" "github.com/spf13/viper" "github.com/stretchr/testify/require" @@ -61,19 +62,21 @@ func TestInput(t *testing.T) { err: "timeout is required", }, { - name: "IndexMissing", + name: "ValidatorMissing", vars: map[string]interface{}{ "timeout": "5s", }, - err: "account, index or pubkey is required", + err: "validator is required", }, { - name: "ConnectionMissing", + name: "Good", vars: map[string]interface{}{ - "timeout": "5s", - "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "timeout": "5s", + "validator": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + }, + res: &dataIn{ + timeout: 5 * time.Second, }, - err: "failed to connect to any beacon node", }, } diff --git a/cmd/attester/inclusion/process.go b/cmd/attester/inclusion/process.go index a16cae8..3ddc0de 100644 --- a/cmd/attester/inclusion/process.go +++ b/cmd/attester/inclusion/process.go @@ -31,7 +31,10 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { return nil, errors.New("no data") } - var err error + validator, err := util.ParseValidator(ctx, data.eth2Client.(eth2client.ValidatorsProvider), data.validator, "head") + if err != nil { + return nil, err + } data.chainTime, err = standardchaintime.New(ctx, standardchaintime.WithSpecProvider(data.eth2Client.(eth2client.SpecProvider)), @@ -41,23 +44,6 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { return nil, errors.Wrap(err, "failed to set up chaintime service") } - validatorIndex, err := util.ValidatorIndex(ctx, data.eth2Client, data.account, data.pubKey, data.index) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain validator index") - } - - validators, err := data.eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, fmt.Sprintf("%d", uint64(data.epoch)*data.slotsPerEpoch), []phase0.ValidatorIndex{validatorIndex}) - if err != nil { - return nil, errors.New("failed to obtain validator information") - } - if len(validators) == 0 { - return nil, errors.New("validator is not known") - } - var validator *api.Validator - for _, v := range validators { - validator = v - } - results := &dataOut{ debug: data.debug, quiet: data.quiet, diff --git a/cmd/attester/inclusion/process_internal_test.go b/cmd/attester/inclusion/process_internal_test.go index 6e2c944..0a7c9d9 100644 --- a/cmd/attester/inclusion/process_internal_test.go +++ b/cmd/attester/inclusion/process_internal_test.go @@ -18,18 +18,26 @@ import ( "os" "testing" - "github.com/attestantio/go-eth2-client/auto" + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/http" "github.com/rs/zerolog" "github.com/stretchr/testify/require" + standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard" ) func TestProcess(t *testing.T) { if os.Getenv("ETHDO_TEST_CONNECTION") == "" { t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests") } - eth2Client, err := auto.New(context.Background(), - auto.WithLogLevel(zerolog.Disabled), - auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")), + eth2Client, err := http.New(context.Background(), + http.WithLogLevel(zerolog.Disabled), + http.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")), + ) + require.NoError(t, err) + + chainTime, err := standardchaintime.New(context.Background(), + standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)), + standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)), ) require.NoError(t, err) @@ -45,10 +53,9 @@ func TestProcess(t *testing.T) { { name: "Client", dataIn: &dataIn{ - eth2Client: eth2Client, - slotsPerEpoch: 32, - pubKey: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95", - epoch: 100, + eth2Client: eth2Client, + chainTime: chainTime, + validator: "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95", }, }, } diff --git a/cmd/attesterduties.go b/cmd/attesterduties.go index 38b24b9..b024cfc 100644 --- a/cmd/attesterduties.go +++ b/cmd/attesterduties.go @@ -26,7 +26,7 @@ var attesterDutiesCmd = &cobra.Command{ Short: "Obtain information about duties of an attester", Long: `Obtain information about dutes of an attester. For example: - ethdo attester duties --account=Validators/00001 --epoch=12345 + ethdo attester duties --validator=Validators/00001 --epoch=12345 In quiet mode this will return 0 if a duty from the attester is found, otherwise 1.`, RunE: func(cmd *cobra.Command, args []string) error { @@ -47,8 +47,8 @@ In quiet mode this will return 0 if a duty from the attester is found, otherwise func init() { attesterCmd.AddCommand(attesterDutiesCmd) attesterFlags(attesterDutiesCmd) - attesterDutiesCmd.Flags().Int64("epoch", -1, "the last complete epoch") - attesterDutiesCmd.Flags().String("pubkey", "", "the public key of the attester") + attesterDutiesCmd.Flags().String("epoch", "head", "the epoch for which to obtain the duties") + attesterDutiesCmd.Flags().String("validator", "", "the index, public key, or acount of the validator") attesterDutiesCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network") } @@ -56,7 +56,7 @@ func attesterDutiesBindings() { if err := viper.BindPFlag("epoch", attesterDutiesCmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("pubkey", attesterDutiesCmd.Flags().Lookup("pubkey")); err != nil { + if err := viper.BindPFlag("validator", attesterDutiesCmd.Flags().Lookup("validator")); err != nil { panic(err) } if err := viper.BindPFlag("json", attesterDutiesCmd.Flags().Lookup("json")); err != nil { diff --git a/cmd/attesterinclusion.go b/cmd/attesterinclusion.go index e2c4994..ab5e2da 100644 --- a/cmd/attesterinclusion.go +++ b/cmd/attesterinclusion.go @@ -26,7 +26,7 @@ var attesterInclusionCmd = &cobra.Command{ Short: "Obtain information about attester inclusion", Long: `Obtain information about attester inclusion. For example: - ethdo attester inclusion --account=Validators/00001 --epoch=12345 + ethdo attester inclusion --validator=Validators/00001 --epoch=12345 In quiet mode this will return 0 if an attestation from the attester is found on the block of the given epoch, otherwise 1.`, RunE: func(cmd *cobra.Command, args []string) error { @@ -47,8 +47,8 @@ In quiet mode this will return 0 if an attestation from the attester is found on func init() { attesterCmd.AddCommand(attesterInclusionCmd) attesterFlags(attesterInclusionCmd) - attesterInclusionCmd.Flags().Int64("epoch", -1, "the last complete epoch") - attesterInclusionCmd.Flags().String("pubkey", "", "the public key of the attester") + attesterInclusionCmd.Flags().String("epoch", "-1", "the epoch for which to obtain the inclusion") + attesterInclusionCmd.Flags().String("validator", "", "the index, public key, or account of the validator") attesterInclusionCmd.Flags().String("index", "", "the index of the attester") } @@ -56,7 +56,7 @@ func attesterInclusionBindings() { if err := viper.BindPFlag("epoch", attesterInclusionCmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("pubkey", attesterInclusionCmd.Flags().Lookup("pubkey")); err != nil { + if err := viper.BindPFlag("validator", attesterInclusionCmd.Flags().Lookup("validator")); err != nil { panic(err) } if err := viper.BindPFlag("index", attesterInclusionCmd.Flags().Lookup("index")); err != nil { diff --git a/cmd/block/analyze/process.go b/cmd/block/analyze/process.go index f7106ba..9003ac0 100644 --- a/cmd/block/analyze/process.go +++ b/cmd/block/analyze/process.go @@ -255,7 +255,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to beacon node") } diff --git a/cmd/block/analyze/process_internal_test.go b/cmd/block/analyze/process_internal_test.go index 6c4a451..9644df4 100644 --- a/cmd/block/analyze/process_internal_test.go +++ b/cmd/block/analyze/process_internal_test.go @@ -39,7 +39,7 @@ func TestProcess(t *testing.T) { "connection": os.Getenv("ETHDO_TEST_CONNECTION"), "blockid": "invalid", }, - err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"Invalid block: invalid\"}", + err: "failed to obtain beacon block: failed to request signed beacon block: GET failed with status 400: {\"code\":400,\"message\":\"BAD_REQUEST: Unsupported endpoint version: v2\",\"stacktraces\":[]}", }, } diff --git a/cmd/block/info/input.go b/cmd/block/info/input.go index ff207bd..1e2b075 100644 --- a/cmd/block/info/input.go +++ b/cmd/block/info/input.go @@ -54,7 +54,12 @@ func input(ctx context.Context) (*dataIn, error) { data.stream = viper.GetBool("stream") var err error - data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !data.quiet, + }) if err != nil { return nil, err } diff --git a/cmd/block/info/input_internal_test.go b/cmd/block/info/input_internal_test.go index 1e5d2c6..c090fc5 100644 --- a/cmd/block/info/input_internal_test.go +++ b/cmd/block/info/input_internal_test.go @@ -61,13 +61,6 @@ func TestInput(t *testing.T) { vars: map[string]interface{}{}, err: "timeout is required", }, - { - name: "ConnectionMissing", - vars: map[string]interface{}{ - "timeout": "5s", - }, - err: "failed to connect to any beacon node", - }, { name: "ConnectionBad", vars: map[string]interface{}{ diff --git a/cmd/block/info/process.go b/cmd/block/info/process.go index a842da9..0e0a802 100644 --- a/cmd/block/info/process.go +++ b/cmd/block/info/process.go @@ -1,4 +1,4 @@ -// Copyright © 2019, 2020 Weald Technology Trading +// Copyright © 2019 - 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 @@ -39,6 +39,9 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { if data == nil { return nil, errors.New("no data") } + if data.blockID == "" { + return nil, errors.New("no block ID") + } results = &dataOut{ debug: data.debug, diff --git a/cmd/block/info/process_internal_test.go b/cmd/block/info/process_internal_test.go index 6679a26..baba0b0 100644 --- a/cmd/block/info/process_internal_test.go +++ b/cmd/block/info/process_internal_test.go @@ -1,4 +1,4 @@ -// Copyright © 2019, 2020 Weald Technology Trading +// Copyright © 2019 - 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 @@ -43,11 +43,11 @@ func TestProcess(t *testing.T) { err: "no data", }, { - name: "Client", + name: "NoBlockID", dataIn: &dataIn{ eth2Client: eth2Client, }, - err: "empty beacon block", + err: "no block ID", }, } diff --git a/cmd/chain/eth1votes/process.go b/cmd/chain/eth1votes/process.go index b4ebbf3..5400d05 100644 --- a/cmd/chain/eth1votes/process.go +++ b/cmd/chain/eth1votes/process.go @@ -114,7 +114,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to beacon node") } diff --git a/cmd/chain/queues/process.go b/cmd/chain/queues/process.go index 0bf2a22..30b22a3 100644 --- a/cmd/chain/queues/process.go +++ b/cmd/chain/queues/process.go @@ -58,7 +58,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to beacon node") } diff --git a/cmd/chain/time/output.go b/cmd/chain/time/output.go index b389a95..2753eb0 100644 --- a/cmd/chain/time/output.go +++ b/cmd/chain/time/output.go @@ -34,6 +34,7 @@ type dataOut struct { slot spec.Slot slotStart time.Time slotEnd time.Time + hasSyncCommittees bool syncCommitteePeriod uint64 syncCommitteePeriodStart time.Time syncCommitteePeriodEpochStart spec.Epoch @@ -56,27 +57,59 @@ func output(_ context.Context, data *dataOut) (string, error) { builder.WriteString(fmt.Sprintf("%d", data.epoch)) builder.WriteString("\n Epoch start ") builder.WriteString(data.epochStart.Format("2006-01-02 15:04:05")) + if data.verbose { + builder.WriteString(" (") + builder.WriteString(fmt.Sprintf("%d", data.epochStart.Unix())) + builder.WriteString(")") + } builder.WriteString("\n Epoch end ") builder.WriteString(data.epochEnd.Format("2006-01-02 15:04:05")) + if data.verbose { + builder.WriteString(" (") + builder.WriteString(fmt.Sprintf("%d", data.epochEnd.Unix())) + builder.WriteString(")") + } builder.WriteString("\nSlot ") builder.WriteString(fmt.Sprintf("%d", data.slot)) builder.WriteString("\n Slot start ") builder.WriteString(data.slotStart.Format("2006-01-02 15:04:05")) + if data.verbose { + builder.WriteString(" (") + builder.WriteString(fmt.Sprintf("%d", data.slotStart.Unix())) + builder.WriteString(")") + } builder.WriteString("\n Slot end ") builder.WriteString(data.slotEnd.Format("2006-01-02 15:04:05")) + if data.verbose { + builder.WriteString(" (") + builder.WriteString(fmt.Sprintf("%d", data.slotEnd.Unix())) + builder.WriteString(")") + } - builder.WriteString("\nSync committee period ") - builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod)) - builder.WriteString("\n Sync committee period start ") - builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05")) - builder.WriteString(" (epoch ") - builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart)) - builder.WriteString(")\n Sync committee period end ") - builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05")) - builder.WriteString(" (epoch ") - builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd)) - builder.WriteString(")\n") + if data.hasSyncCommittees { + builder.WriteString("\nSync committee period ") + builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriod)) + builder.WriteString("\n Sync committee period start ") + builder.WriteString(data.syncCommitteePeriodStart.Format("2006-01-02 15:04:05")) + builder.WriteString(" (epoch ") + builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochStart)) + if data.verbose { + builder.WriteString(", ") + builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodStart.Unix())) + } + builder.WriteString(")\n Sync committee period end ") + builder.WriteString(data.syncCommitteePeriodEnd.Format("2006-01-02 15:04:05")) + builder.WriteString(" (epoch ") + builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEpochEnd)) + if data.verbose { + builder.WriteString(", ") + builder.WriteString(fmt.Sprintf("%d", data.syncCommitteePeriodEnd.Unix())) + } + builder.WriteString(")") + } + + builder.WriteString("\n") return builder.String(), nil } diff --git a/cmd/chain/time/process.go b/cmd/chain/time/process.go index 08d23bd..2cae55e 100644 --- a/cmd/chain/time/process.go +++ b/cmd/chain/time/process.go @@ -21,6 +21,7 @@ import ( eth2client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" + standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard" "github.com/wealdtech/ethdo/util" ) @@ -29,22 +30,22 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { return nil, errors.New("no data") } - eth2Client, err := util.ConnectToBeaconNode(ctx, data.connection, data.timeout, data.allowInsecureConnections) + eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: data.connection, + Timeout: data.timeout, + AllowInsecure: data.allowInsecureConnections, + LogFallback: !data.quiet, + }) if err != nil { return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node") } - config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx) + chainTime, err := standardchaintime.New(ctx, + standardchaintime.WithSpecProvider(eth2Client.(eth2client.SpecProvider)), + standardchaintime.WithGenesisTimeProvider(eth2Client.(eth2client.GenesisTimeProvider)), + ) if err != nil { - return nil, errors.Wrap(err, "failed to obtain beacon chain configuration") - } - - slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64) - slotDuration := config["SECONDS_PER_SLOT"].(time.Duration) - epochsPerSyncCommitteePeriod := config["EPOCHS_PER_SYNC_COMMITTEE_PERIOD"].(uint64) - genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain genesis data") + return nil, errors.Wrap(err, "failed to set up chaintime service") } results := &dataOut{ @@ -62,34 +63,33 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { } results.slot = phase0.Slot(slot) case data.epoch != "": - epoch, err := strconv.ParseUint(data.epoch, 10, 64) + epoch, err := util.ParseEpoch(ctx, chainTime, data.epoch) if err != nil { return nil, errors.Wrap(err, "failed to parse epoch") } - results.slot = phase0.Slot(epoch * slotsPerEpoch) + results.slot = chainTime.FirstSlotOfEpoch(epoch) case data.timestamp != "": timestamp, err := time.Parse("2006-01-02T15:04:05-0700", data.timestamp) if err != nil { return nil, errors.Wrap(err, "failed to parse timestamp") } - secs := timestamp.Sub(genesis.GenesisTime) - if secs < 0 { - return nil, errors.New("timestamp prior to genesis") - } - results.slot = phase0.Slot(secs / slotDuration) + results.slot = chainTime.TimestampToSlot(timestamp) } // Fill in the info given the slot. - results.slotStart = genesis.GenesisTime.Add(time.Duration(results.slot) * slotDuration) - results.slotEnd = genesis.GenesisTime.Add(time.Duration(results.slot+1) * slotDuration) - results.epoch = phase0.Epoch(uint64(results.slot) / slotsPerEpoch) - results.epochStart = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch)*slotsPerEpoch) * slotDuration) - results.epochEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.epoch+1)*slotsPerEpoch) * slotDuration) - results.syncCommitteePeriod = uint64(results.epoch) / epochsPerSyncCommitteePeriod - results.syncCommitteePeriodEpochStart = phase0.Epoch(results.syncCommitteePeriod * epochsPerSyncCommitteePeriod) - results.syncCommitteePeriodEpochEnd = phase0.Epoch((results.syncCommitteePeriod+1)*epochsPerSyncCommitteePeriod) - 1 - results.syncCommitteePeriodStart = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochStart)*slotsPerEpoch) * slotDuration) - results.syncCommitteePeriodEnd = genesis.GenesisTime.Add(time.Duration(uint64(results.syncCommitteePeriodEpochEnd)*slotsPerEpoch) * slotDuration) + results.slotStart = chainTime.StartOfSlot(results.slot) + results.slotEnd = chainTime.StartOfSlot(results.slot + 1) + results.epoch = chainTime.SlotToEpoch(results.slot) + results.epochStart = chainTime.StartOfEpoch(results.epoch) + results.epochEnd = chainTime.StartOfEpoch(results.epoch + 1) + if results.epoch >= chainTime.FirstEpochOfSyncPeriod(chainTime.AltairInitialSyncCommitteePeriod()) { + results.hasSyncCommittees = true + results.syncCommitteePeriod = chainTime.SlotToSyncCommitteePeriod(results.slot) + results.syncCommitteePeriodEpochStart = chainTime.FirstEpochOfSyncPeriod(results.syncCommitteePeriod) + results.syncCommitteePeriodEpochEnd = chainTime.FirstEpochOfSyncPeriod(results.syncCommitteePeriod + 1) + results.syncCommitteePeriodStart = chainTime.StartOfEpoch(results.syncCommitteePeriodEpochStart) + results.syncCommitteePeriodEnd = chainTime.StartOfEpoch(results.syncCommitteePeriodEpochEnd) + } return results, nil } diff --git a/cmd/chain/time/process_internal_test.go b/cmd/chain/time/process_internal_test.go index a88c00c..920fa73 100644 --- a/cmd/chain/time/process_internal_test.go +++ b/cmd/chain/time/process_internal_test.go @@ -1,4 +1,4 @@ -// Copyright © 2021 Weald Technology Trading +// Copyright © 2021 - 2023 Weald Technology Trading. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -15,7 +15,6 @@ package chaintime import ( "context" - "fmt" "os" "testing" "time" @@ -47,16 +46,11 @@ func TestProcess(t *testing.T) { slot: "1", }, expected: &dataOut{ - epochStart: time.Unix(1606824023, 0), - epochEnd: time.Unix(1606824407, 0), - slot: 1, - slotStart: time.Unix(1606824035, 0), - slotEnd: time.Unix(1606824047, 0), - syncCommitteePeriod: 0, - syncCommitteePeriodStart: time.Unix(1606824023, 0), - syncCommitteePeriodEnd: time.Unix(1606921943, 0), - syncCommitteePeriodEpochStart: 0, - syncCommitteePeriodEpochEnd: 255, + epochStart: time.Unix(1606824023, 0), + epochEnd: time.Unix(1606824407, 0), + slot: 1, + slotStart: time.Unix(1606824035, 0), + slotEnd: time.Unix(1606824047, 0), }, }, { @@ -68,17 +62,12 @@ func TestProcess(t *testing.T) { epoch: "2", }, expected: &dataOut{ - epoch: 2, - epochStart: time.Unix(1606824791, 0), - epochEnd: time.Unix(1606825175, 0), - slot: 64, - slotStart: time.Unix(1606824791, 0), - slotEnd: time.Unix(1606824803, 0), - syncCommitteePeriod: 0, - syncCommitteePeriodStart: time.Unix(1606824023, 0), - syncCommitteePeriodEnd: time.Unix(1606921943, 0), - syncCommitteePeriodEpochStart: 0, - syncCommitteePeriodEpochEnd: 255, + epoch: 2, + epochStart: time.Unix(1606824791, 0), + epochEnd: time.Unix(1606825175, 0), + slot: 64, + slotStart: time.Unix(1606824791, 0), + slotEnd: time.Unix(1606824803, 0), }, }, { @@ -87,20 +76,21 @@ func TestProcess(t *testing.T) { connection: os.Getenv("ETHDO_TEST_CONNECTION"), timeout: 10 * time.Second, allowInsecureConnections: true, - timestamp: "2021-01-01T00:00:00+0000", + timestamp: "2023-01-01T00:00:00+0000", }, expected: &dataOut{ - epoch: 6862, - epochStart: time.Unix(1609459031, 0), - epochEnd: time.Unix(1609459415, 0), - slot: 219598, - slotStart: time.Unix(1609459199, 0), - slotEnd: time.Unix(1609459211, 0), - syncCommitteePeriod: 26, - syncCommitteePeriodStart: time.Unix(1609379927, 0), - syncCommitteePeriodEnd: time.Unix(1609477847, 0), - syncCommitteePeriodEpochStart: 6656, - syncCommitteePeriodEpochEnd: 6911, + epoch: 171112, + epochStart: time.Unix(1672531031, 0), + epochEnd: time.Unix(1672531415, 0), + slot: 5475598, + slotStart: time.Unix(1672531199, 0), + slotEnd: time.Unix(1672531211, 0), + hasSyncCommittees: true, + syncCommitteePeriod: 668, + syncCommitteePeriodStart: time.Unix(1672491095, 0), + syncCommitteePeriodEnd: time.Unix(1672589399, 0), + syncCommitteePeriodEpochStart: 171008, + syncCommitteePeriodEpochEnd: 171264, }, }, } @@ -112,7 +102,6 @@ func TestProcess(t *testing.T) { require.EqualError(t, err, test.err) } else { require.NoError(t, err) - fmt.Printf("****** %d %d\n", res.syncCommitteePeriodStart.Unix(), res.syncCommitteePeriodEnd.Unix()) require.Equal(t, test.expected, res) } }) diff --git a/cmd/chain/verify/signedcontributionandproof/process.go b/cmd/chain/verify/signedcontributionandproof/process.go index 5fcb146..c6b9cae 100644 --- a/cmd/chain/verify/signedcontributionandproof/process.go +++ b/cmd/chain/verify/signedcontributionandproof/process.go @@ -85,7 +85,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to beacon node") } diff --git a/cmd/chaininfo.go b/cmd/chaininfo.go index b93d540..26a140f 100644 --- a/cmd/chaininfo.go +++ b/cmd/chaininfo.go @@ -37,7 +37,12 @@ In quiet mode this will return 0 if the chain information can be obtained, other Run: func(cmd *cobra.Command, args []string) { ctx := context.Background() - eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !viper.GetBool("quiet"), + }) errCheck(err, "Failed to connect to Ethereum 2 beacon node") config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx) diff --git a/cmd/chainstatus.go b/cmd/chainstatus.go index c4e6e47..ce41c3c 100644 --- a/cmd/chainstatus.go +++ b/cmd/chainstatus.go @@ -41,7 +41,12 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise Run: func(cmd *cobra.Command, args []string) { ctx := context.Background() - eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !viper.GetBool("quiet"), + }) errCheck(err, "Failed to connect to Ethereum 2 beacon node") chainTime, err := standardchaintime.New(ctx, diff --git a/cmd/epoch/summary/process.go b/cmd/epoch/summary/process.go index 91bd427..c488dee 100644 --- a/cmd/epoch/summary/process.go +++ b/cmd/epoch/summary/process.go @@ -324,7 +324,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to beacon node") } diff --git a/cmd/exitverify.go b/cmd/exitverify.go index 237deb1..28578ad 100644 --- a/cmd/exitverify.go +++ b/cmd/exitverify.go @@ -14,80 +14,97 @@ package cmd import ( - "bytes" "context" - "encoding/hex" "encoding/json" "fmt" "os" "strings" - eth2client "github.com/attestantio/go-eth2-client" - spec "github.com/attestantio/go-eth2-client/spec/phase0" + consensusclient "github.com/attestantio/go-eth2-client" + v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/wealdtech/ethdo/util" e2types "github.com/wealdtech/go-eth2-types/v2" - e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) -var exitVerifyPubKey string - var exitVerifyCmd = &cobra.Command{ Use: "verify", Short: "Verify exit data is valid", Long: `Verify that exit data generated by "ethdo validator exit" is correct for a given account. For example: - ethdo exit verify --data=exitdata.json --account=primary/current + ethdo exit verify --signed-operation=exitdata.json --validator=primary/current In quiet mode this will return 0 if the exit is verified correctly, otherwise 1.`, Run: func(cmd *cobra.Command, args []string) { ctx := context.Background() - assert(viper.GetString("account") != "" || exitVerifyPubKey != "", "account or public key is required") - account, err := exitVerifyAccount(ctx) - errCheck(err, "Failed to obtain account") + assert(viper.GetString("signed-operation") != "", "signed-operation is required") + signedOp, err := obtainSignedOperation(viper.GetString("signed-operation")) + errCheck(err, "Failed to obtain signed operation") - assert(viper.GetString("exit") != "", "exit is required") - data, err := obtainExitData(viper.GetString("exit")) - errCheck(err, "Failed to obtain exit data") - - // Confirm signature is good. - eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !viper.GetBool("quiet"), + }) errCheck(err, "Failed to connect to Ethereum 2 beacon node") - genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx) + validator, err := util.ParseValidator(ctx, eth2Client.(consensusclient.ValidatorsProvider), fmt.Sprintf("%d", signedOp.Message.ValidatorIndex), "head") + errCheck(err, "failed to obtain validator") + pubkey, err := validator.PubKey(ctx) + errCheck(err, "failed to obtain validator public key") + account, err := util.ParseAccount(ctx, pubkey.String(), nil, false) + errCheck(err, "failed to obtain account") + + // Ensure the validator is in a suitable state. + assert(validator.Status == v1.ValidatorStateActiveOngoing, "validator not in a suitable state to exit") + + // Obtain the hash tree root of the message to check the signature. + opRoot, err := signedOp.Message.HashTreeRoot() + errCheck(err, "Failed to obtain exit hash tree root") + + genesis, err := eth2Client.(consensusclient.GenesisProvider).Genesis(ctx) errCheck(err, "Failed to obtain beacon chain genesis") - domain := e2types.Domain(e2types.DomainVoluntaryExit, data.ForkVersion[:], genesis.GenesisValidatorsRoot[:]) - var exitDomain spec.Domain - copy(exitDomain[:], domain) - exit := &spec.VoluntaryExit{ - Epoch: data.Exit.Message.Epoch, - ValidatorIndex: data.Exit.Message.ValidatorIndex, - } - exitRoot, err := exit.HashTreeRoot() - errCheck(err, "Failed to obtain exit hash tree root") + fork, err := eth2Client.(consensusclient.ForkProvider).Fork(ctx, "head") + errCheck(err, "Failed to obtain fork information") + + // Check against current and prior fork versions. signatureBytes := make([]byte, 96) - copy(signatureBytes, data.Exit.Signature[:]) + copy(signatureBytes, signedOp.Signature[:]) sig, err := e2types.BLSSignatureFromBytes(signatureBytes) errCheck(err, "Invalid signature") - verified, err := util.VerifyRoot(account, exitRoot, exitDomain, sig) - errCheck(err, "Failed to verify voluntary exit") - assert(verified, "Voluntary exit failed to verify") - fork, err := eth2Client.(eth2client.ForkProvider).Fork(ctx, "head") - errCheck(err, "Failed to obtain current fork") - assert(bytes.Equal(data.ForkVersion[:], fork.CurrentVersion[:]) || bytes.Equal(data.ForkVersion[:], fork.PreviousVersion[:]), "Exit is for an old fork version and is no longer valid") + verified := false + + // Try with the current fork. + domain := phase0.Domain{} + currentExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.CurrentVersion[:], genesis.GenesisValidatorsRoot[:]) + errCheck(err, "Failed to compute domain") + copy(domain[:], currentExitDomain) + verified, err = util.VerifyRoot(account, opRoot, domain, sig) + errCheck(err, "Failed to verify voluntary exit") + if !verified { + // Try again with the previous fork. + previousExitDomain, err := e2types.ComputeDomain(e2types.DomainVoluntaryExit, fork.PreviousVersion[:], genesis.GenesisValidatorsRoot[:]) + copy(domain[:], previousExitDomain) + errCheck(err, "Failed to compute domain") + verified, err = util.VerifyRoot(account, opRoot, domain, sig) + errCheck(err, "Failed to verify voluntary exit") + } + assert(verified, "Voluntary exit failed to verify against current and previous fork versions") outputIf(verbose, "Verified") os.Exit(_exitSuccess) }, } -// obtainExitData obtains exit data from an input, could be JSON itself or a path to JSON. -func obtainExitData(input string) (*util.ValidatorExitData, error) { +// obtainSignedOperation obtains exit data from an input, could be JSON itself or a path to JSON. +func obtainSignedOperation(input string) (*phase0.SignedVoluntaryExit, error) { var err error var data []byte // Input could be JSON or a path to JSON @@ -101,46 +118,23 @@ func obtainExitData(input string) (*util.ValidatorExitData, error) { return nil, errors.Wrap(err, "failed to find deposit data file") } } - exitData := &util.ValidatorExitData{} - err = json.Unmarshal(data, exitData) + signedOp := &phase0.SignedVoluntaryExit{} + err = json.Unmarshal(data, signedOp) if err != nil { return nil, errors.Wrap(err, "data is not valid JSON") } - return exitData, nil -} - -// exitVerifyAccount obtains the account for the exitVerify command. -func exitVerifyAccount(ctx context.Context) (e2wtypes.Account, error) { - var account e2wtypes.Account - var err error - if viper.GetString("account") != "" { - _, 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(exitVerifyPubKey, "0x")) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", exitVerifyPubKey)) - } - account, err = util.NewScratchAccount(nil, pubKeyBytes) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", exitVerifyPubKey)) - } - } - return account, nil + return signedOp, nil } func init() { exitCmd.AddCommand(exitVerifyCmd) exitFlags(exitVerifyCmd) - exitVerifyCmd.Flags().String("exit", "", "JSON data, or path to JSON data") - exitVerifyCmd.Flags().StringVar(&exitVerifyPubKey, "pubkey", "", "Public key for which to verify exit") + exitVerifyCmd.Flags().String("signed-operation", "", "JSON data, or path to JSON data") } func exitVerifyBindings() { - if err := viper.BindPFlag("exit", exitVerifyCmd.Flags().Lookup("exit")); err != nil { + if err := viper.BindPFlag("signed-operation", exitVerifyCmd.Flags().Lookup("signed-operation")); err != nil { panic(err) } } diff --git a/cmd/node/events/input.go b/cmd/node/events/input.go index 8bf80f9..7af4163 100644 --- a/cmd/node/events/input.go +++ b/cmd/node/events/input.go @@ -50,7 +50,12 @@ func input(ctx context.Context) (*dataIn, error) { data.topics = viper.GetStringSlice("topics") var err error - data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !data.quiet, + }) if err != nil { return nil, err } diff --git a/cmd/node/events/input_internal_test.go b/cmd/node/events/input_internal_test.go index 8c90a61..46b360c 100644 --- a/cmd/node/events/input_internal_test.go +++ b/cmd/node/events/input_internal_test.go @@ -61,13 +61,6 @@ func TestInput(t *testing.T) { vars: map[string]interface{}{}, err: "timeout is required", }, - { - name: "ConnectionMissing", - vars: map[string]interface{}{ - "timeout": "5s", - }, - err: "failed to connect to any beacon node", - }, { name: "ConnectionBad", vars: map[string]interface{}{ diff --git a/cmd/nodeinfo.go b/cmd/nodeinfo.go index 2b682c6..303d2ff 100644 --- a/cmd/nodeinfo.go +++ b/cmd/nodeinfo.go @@ -24,9 +24,6 @@ import ( "github.com/wealdtech/ethdo/util" ) -// defaultBeaconNode is used if no other connection is supplied. -var defaultBeaconNode = "http://mainnet-consensus.attestant.io/" - var nodeInfoCmd = &cobra.Command{ Use: "info", Short: "Obtain information about a node", @@ -38,27 +35,13 @@ In quiet mode this will return 0 if the node information can be obtained, otherw Run: func(cmd *cobra.Command, args []string) { ctx := context.Background() - eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) - if err != nil { - if viper.GetString("connection") != "" { - // The user provided a connection, so don't second-guess them by using a different node. - fmt.Fprintln(os.Stderr, err.Error()) - return - } - - // The user did not provide a connection, so attempt to use the default node. - if viper.GetBool("debug") { - fmt.Fprintf(os.Stderr, "No node connection, attempting to use %s\n", defaultBeaconNode) - } - eth2Client, err = util.ConnectToBeaconNode(ctx, defaultBeaconNode, viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) - if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - return - } - if !viper.GetBool("quiet") { - fmt.Fprintf(os.Stderr, "No connection supplied; using mainnet public access endpoint\n") - } - } + eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !viper.GetBool("quiet"), + }) + errCheck(err, "Failed to connect to Ethereum 2 beacon node") if quiet { os.Exit(_exitSuccess) diff --git a/cmd/proposer/duties/process.go b/cmd/proposer/duties/process.go index 12887e3..d152ab1 100644 --- a/cmd/proposer/duties/process.go +++ b/cmd/proposer/duties/process.go @@ -46,7 +46,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to beacon node") } diff --git a/cmd/slot/time/input.go b/cmd/slot/time/input.go index d4eb157..af28c54 100644 --- a/cmd/slot/time/input.go +++ b/cmd/slot/time/input.go @@ -52,7 +52,12 @@ func input(ctx context.Context) (*dataIn, error) { // Ethereum 2 client. var err error - data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !data.quiet, + }) if err != nil { return nil, err } diff --git a/cmd/slot/time/input_internal_test.go b/cmd/slot/time/input_internal_test.go index 1d607ea..0130c98 100644 --- a/cmd/slot/time/input_internal_test.go +++ b/cmd/slot/time/input_internal_test.go @@ -67,14 +67,6 @@ func TestInput(t *testing.T) { }, err: "slot is required", }, - { - name: "ConnectionMissing", - vars: map[string]interface{}{ - "timeout": "5s", - "slot": "1", - }, - err: "failed to connect to any beacon node", - }, } for _, test := range tests { diff --git a/cmd/synccommittee/inclusion/command.go b/cmd/synccommittee/inclusion/command.go index dd7c8a2..65c0fae 100644 --- a/cmd/synccommittee/inclusion/command.go +++ b/cmd/synccommittee/inclusion/command.go @@ -18,6 +18,7 @@ import ( "time" eth2client "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/services/chaintime" @@ -34,16 +35,15 @@ type command struct { allowInsecureConnections bool // Input. - account string - pubKey string - index string - epoch int64 + validator string + epochStr string // Data access. eth2Client eth2client.Service chainTime chaintime.Service // Output. + epoch phase0.Epoch inCommittee bool committeeIndex uint64 inclusions []int @@ -67,15 +67,13 @@ func newCommand(_ context.Context) (*command, error) { c.allowInsecureConnections = viper.GetBool("allow-insecure-connections") // Validator. - c.account = viper.GetString("account") - c.pubKey = viper.GetString("pubkey") - c.index = viper.GetString("index") - if c.account == "" && c.pubKey == "" && c.index == "" { - return nil, errors.New("account, pubkey or index required") + c.validator = viper.GetString("validator") + if c.validator == "" { + return nil, errors.New("validator is required") } // Epoch. - c.epoch = viper.GetInt64("epoch") + c.epochStr = viper.GetString("epoch") return c, nil } diff --git a/cmd/synccommittee/inclusion/command_internal_test.go b/cmd/synccommittee/inclusion/command_internal_test.go index 7537c28..919bc5f 100644 --- a/cmd/synccommittee/inclusion/command_internal_test.go +++ b/cmd/synccommittee/inclusion/command_internal_test.go @@ -43,14 +43,14 @@ func TestInput(t *testing.T) { "timeout": "5s", "connection": os.Getenv("ETHDO_TEST_CONNECTION"), }, - err: "account, pubkey or index required", + err: "validator is required", }, { name: "Good", vars: map[string]interface{}{ "validators": "1", "timeout": "5s", - "index": "1", + "validator": "1", "connection": os.Getenv("ETHDO_TEST_CONNECTION"), }, }, diff --git a/cmd/synccommittee/inclusion/process.go b/cmd/synccommittee/inclusion/process.go index 606cc82..a63b93d 100644 --- a/cmd/synccommittee/inclusion/process.go +++ b/cmd/synccommittee/inclusion/process.go @@ -20,7 +20,6 @@ import ( eth2client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" - "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" standardchaintime "github.com/wealdtech/ethdo/services/chaintime/standard" "github.com/wealdtech/ethdo/util" @@ -32,14 +31,17 @@ func (c *command) process(ctx context.Context) error { return err } - firstSlot, lastSlot := c.calculateSlots(ctx) - - validatorIndex, err := util.ValidatorIndex(ctx, c.eth2Client, c.account, c.pubKey, c.index) + validator, err := util.ParseValidator(ctx, c.eth2Client.(eth2client.ValidatorsProvider), c.validator, "head") if err != nil { return err } - syncCommittee, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", phase0.Epoch(c.epoch)) + c.epoch, err = util.ParseEpoch(ctx, c.chainTime, c.epochStr) + if err != nil { + return err + } + + syncCommittee, err := c.eth2Client.(eth2client.SyncCommitteesProvider).SyncCommitteeAtEpoch(ctx, "head", c.epoch) if err != nil { return errors.Wrap(err, "failed to obtain sync committee information") } @@ -49,7 +51,7 @@ func (c *command) process(ctx context.Context) error { } for i := range syncCommittee.Validators { - if syncCommittee.Validators[i] == validatorIndex { + if syncCommittee.Validators[i] == validator.Index { c.inCommittee = true c.committeeIndex = uint64(i) break @@ -57,6 +59,8 @@ func (c *command) process(ctx context.Context) error { } if c.inCommittee { + firstSlot := c.chainTime.FirstSlotOfEpoch(c.epoch) + lastSlot := c.chainTime.LastSlotOfEpoch(c.epoch) // This validator is in the sync committee. Check blocks to see where it has been included. c.inclusions = make([]int, 0) if lastSlot > c.chainTime.CurrentSlot() { @@ -107,7 +111,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return err } @@ -122,15 +131,3 @@ func (c *command) setup(ctx context.Context) error { return nil } - -func (c *command) calculateSlots(_ context.Context) (phase0.Slot, phase0.Slot) { - var firstSlot phase0.Slot - var lastSlot phase0.Slot - if c.epoch == -1 { - c.epoch = int64(c.chainTime.CurrentEpoch()) - 1 - } - firstSlot = c.chainTime.FirstSlotOfEpoch(phase0.Epoch(c.epoch)) - lastSlot = c.chainTime.FirstSlotOfEpoch(phase0.Epoch(c.epoch) + 1) - - return firstSlot, lastSlot -} diff --git a/cmd/synccommittee/inclusion/process_internal_test.go b/cmd/synccommittee/inclusion/process_internal_test.go index f9846d9..b0c56f7 100644 --- a/cmd/synccommittee/inclusion/process_internal_test.go +++ b/cmd/synccommittee/inclusion/process_internal_test.go @@ -32,28 +32,20 @@ func TestProcess(t *testing.T) { vars map[string]interface{} err string }{ - { - name: "MissingConnection", - vars: map[string]interface{}{ - "timeout": "5s", - "index": "1", - }, - err: "failed to connect to any beacon node", - }, { name: "InvalidConnection", vars: map[string]interface{}{ "timeout": "5s", - "index": "1", + "validator": "1", "connection": "invalid", }, - err: "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://invalid/eth/v1/beacon/genesis\": dial tcp: lookup invalid: no such host", + err: "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://invalid/eth/v1/beacon/genesis\": dial tcp: lookup invalid on 127.0.0.53:53: no such host", }, { name: "Good", vars: map[string]interface{}{ "timeout": "5s", - "index": "1", + "validator": "1", "epoch": "-1", "connection": os.Getenv("ETHDO_TEST_CONNECTION"), }, diff --git a/cmd/synccommittee/members/input.go b/cmd/synccommittee/members/input.go index b83a4ab..5ec39b7 100644 --- a/cmd/synccommittee/members/input.go +++ b/cmd/synccommittee/members/input.go @@ -34,7 +34,7 @@ type dataIn struct { // Operation. eth2Client eth2client.Service chainTime chaintime.Service - epoch int64 + epoch string period string } @@ -51,7 +51,12 @@ func input(ctx context.Context) (*dataIn, error) { // Ethereum 2 client. var err error - data.eth2Client, err = util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + data.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !data.quiet, + }) if err != nil { return nil, err } @@ -66,7 +71,7 @@ func input(ctx context.Context) (*dataIn, error) { } // Epoch - data.epoch = viper.GetInt64("epoch") + data.epoch = viper.GetString("epoch") data.period = viper.GetString("period") return data, nil diff --git a/cmd/synccommittee/members/input_internal_test.go b/cmd/synccommittee/members/input_internal_test.go index a78b2d8..19e6072 100644 --- a/cmd/synccommittee/members/input_internal_test.go +++ b/cmd/synccommittee/members/input_internal_test.go @@ -60,13 +60,6 @@ func TestInput(t *testing.T) { vars: map[string]interface{}{}, err: "timeout is required", }, - { - name: "ConnectionMissing", - vars: map[string]interface{}{ - "timeout": "5s", - }, - err: "failed to connect to any beacon node", - }, { name: "ConnectionInvalid", vars: map[string]interface{}{ diff --git a/cmd/synccommittee/members/process.go b/cmd/synccommittee/members/process.go index 07947a1..7d942d8 100644 --- a/cmd/synccommittee/members/process.go +++ b/cmd/synccommittee/members/process.go @@ -21,6 +21,7 @@ import ( eth2client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" + "github.com/wealdtech/ethdo/util" ) func process(ctx context.Context, data *dataIn) (*dataOut, error) { @@ -52,10 +53,11 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { return results, nil } -func calculateEpoch(_ context.Context, data *dataIn) (phase0.Epoch, error) { +func calculateEpoch(ctx context.Context, data *dataIn) (phase0.Epoch, error) { var epoch phase0.Epoch - if data.epoch != -1 { - epoch = phase0.Epoch(data.epoch) + var err error + if data.epoch != "" { + epoch, err = util.ParseEpoch(ctx, data.chainTime, data.epoch) } else { switch strings.ToLower(data.period) { case "", "current": @@ -68,6 +70,9 @@ func calculateEpoch(_ context.Context, data *dataIn) (phase0.Epoch, error) { return 0, fmt.Errorf("period %s not known", data.period) } } + if err != nil { + return 0, err + } if data.debug { fmt.Printf("epoch is %d\n", epoch) diff --git a/cmd/synccommittee/members/process_internal_test.go b/cmd/synccommittee/members/process_internal_test.go index 0053ac4..cd9fd0b 100644 --- a/cmd/synccommittee/members/process_internal_test.go +++ b/cmd/synccommittee/members/process_internal_test.go @@ -55,7 +55,7 @@ func TestProcess(t *testing.T) { dataIn: &dataIn{ eth2Client: eth2Client, chainTime: chainTime, - epoch: -1, + epoch: "-1", }, }, } diff --git a/cmd/synccommitteeinclusion.go b/cmd/synccommitteeinclusion.go index 7547969..8dc692a 100644 --- a/cmd/synccommitteeinclusion.go +++ b/cmd/synccommitteeinclusion.go @@ -49,19 +49,15 @@ epoch can be a specific epoch; If not supplied all slots for the current sync co func init() { synccommitteeCmd.AddCommand(synccommitteeInclusionCmd) synccommitteeFlags(synccommitteeInclusionCmd) - synccommitteeInclusionCmd.Flags().Int64("epoch", -1, "the epoch for which to fetch sync committee inclusion") - synccommitteeInclusionCmd.Flags().String("pubkey", "", "validator public key for sync committee") - synccommitteeInclusionCmd.Flags().String("index", "", "validator index for sync committee") + synccommitteeInclusionCmd.Flags().String("epoch", "", "the epoch for which to fetch sync committee inclusion") + synccommitteeInclusionCmd.Flags().String("validator", "", "the index, public key, or acount of the validator") } func synccommitteeInclusionBindings() { if err := viper.BindPFlag("epoch", synccommitteeInclusionCmd.Flags().Lookup("epoch")); err != nil { panic(err) } - if err := viper.BindPFlag("pubkey", synccommitteeInclusionCmd.Flags().Lookup("pubkey")); err != nil { - panic(err) - } - if err := viper.BindPFlag("index", synccommitteeInclusionCmd.Flags().Lookup("index")); err != nil { + if err := viper.BindPFlag("validator", synccommitteeInclusionCmd.Flags().Lookup("validator")); err != nil { panic(err) } } diff --git a/cmd/validator/credentials/get/command_internal_test.go b/cmd/validator/credentials/get/command_internal_test.go index c87743c..d33d7ee 100644 --- a/cmd/validator/credentials/get/command_internal_test.go +++ b/cmd/validator/credentials/get/command_internal_test.go @@ -43,24 +43,14 @@ func TestInput(t *testing.T) { "timeout": "5s", "connection": os.Getenv("ETHDO_TEST_CONNECTION"), }, - err: "one of account, index or pubkey required", - }, - { - name: "MultipleValidatorInfo", - vars: map[string]interface{}{ - "timeout": "5s", - "connection": os.Getenv("ETHDO_TEST_CONNECTION"), - "index": "1", - "pubkey": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - }, - err: "only one of account, index and pubkey allowed", + err: "validator is required", }, { name: "Good", vars: map[string]interface{}{ "timeout": "5s", "connection": os.Getenv("ETHDO_TEST_CONNECTION"), - "index": "1", + "validator": "1", }, }, } diff --git a/cmd/validator/credentials/get/process.go b/cmd/validator/credentials/get/process.go index 4706484..437efac 100644 --- a/cmd/validator/credentials/get/process.go +++ b/cmd/validator/credentials/get/process.go @@ -48,7 +48,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the consensus node. - c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.consensusClient, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to consensus node") } @@ -57,7 +62,7 @@ func (c *command) setup(ctx context.Context) error { var isProvider bool c.validatorsProvider, isProvider = c.consensusClient.(eth2client.ValidatorsProvider) if !isProvider { - return errors.New("consensu node does not provide validator information") + return errors.New("consensus node does not provide validator information") } return nil diff --git a/cmd/validator/credentials/set/command_internal_test.go b/cmd/validator/credentials/set/command_internal_test.go index 0beba2a..2c0c399 100644 --- a/cmd/validator/credentials/set/command_internal_test.go +++ b/cmd/validator/credentials/set/command_internal_test.go @@ -37,30 +37,12 @@ func TestInput(t *testing.T) { vars: map[string]interface{}{}, err: "timeout is required", }, - { - name: "NoValidatorInfo", - vars: map[string]interface{}{ - "timeout": "5s", - "connection": os.Getenv("ETHDO_TEST_CONNECTION"), - }, - err: "one of account, index or pubkey required", - }, - { - name: "MultipleValidatorInfo", - vars: map[string]interface{}{ - "timeout": "5s", - "connection": os.Getenv("ETHDO_TEST_CONNECTION"), - "index": "1", - "pubkey": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - }, - err: "only one of account, index and pubkey allowed", - }, { name: "Good", vars: map[string]interface{}{ "timeout": "5s", "connection": os.Getenv("ETHDO_TEST_CONNECTION"), - "index": "1", + "validator": "1", }, }, } diff --git a/cmd/validator/credentials/set/process.go b/cmd/validator/credentials/set/process.go index aeb1698..4808375 100644 --- a/cmd/validator/credentials/set/process.go +++ b/cmd/validator/credentials/set/process.go @@ -44,9 +44,6 @@ import ( // a lot of data for an unsophisticated audience so it's easier to set a higher timeout.. var minTimeout = 5 * time.Minute -// defaultBeaconNode is used if no other connection is supplied. -var defaultBeaconNode = "http://mainnet-consensus.attestant.io/" - // validatorPath is the regular expression that matches a validator path. var validatorPath = regexp.MustCompile("^m/12381/3600/[0-9]+/0/0$") @@ -707,24 +704,14 @@ func (c *command) setup(ctx context.Context) error { // Connect to the consensus node. var err error - c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.consensusClient, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { - if c.connection != "" { - // The user provided a connection, so don't second-guess them by using a different node. - return err - } - - // The user did not provide a connection, so attempt to use the default node. - if c.debug { - fmt.Fprintf(os.Stderr, "No node connection, attempting to use %s\n", defaultBeaconNode) - } - c.consensusClient, err = util.ConnectToBeaconNode(ctx, defaultBeaconNode, c.timeout, true) - if err != nil { - return err - } - if !c.quiet { - fmt.Fprintf(os.Stderr, "No connection supplied; using mainnet public access endpoint\n") - } + return err } // Set up chaintime. diff --git a/cmd/validator/duties/process.go b/cmd/validator/duties/process.go index 3e49c91..ecbed80 100644 --- a/cmd/validator/duties/process.go +++ b/cmd/validator/duties/process.go @@ -30,7 +30,12 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { } // Ethereum 2 client. - eth2Client, err := util.ConnectToBeaconNode(ctx, data.eth2Client, data.timeout, data.allowInsecure) + eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: data.eth2Client, + Timeout: data.timeout, + AllowInsecure: data.allowInsecure, + LogFallback: !data.quiet, + }) if err != nil { return nil, err } diff --git a/cmd/validator/exit/process.go b/cmd/validator/exit/process.go index 0c5da88..3663b03 100644 --- a/cmd/validator/exit/process.go +++ b/cmd/validator/exit/process.go @@ -51,9 +51,6 @@ var ( exitOperationFilename = "exit-operation.json" ) -// defaultBeaconNode is used if no other connection is supplied. -var defaultBeaconNode = "http://mainnet-consensus.attestant.io/" - func (c *command) process(ctx context.Context) error { if err := c.setup(ctx); err != nil { return err @@ -420,24 +417,14 @@ func (c *command) setup(ctx context.Context) error { // Connect to the consensus node. var err error - c.consensusClient, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.consensusClient, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { - if c.connection != "" { - // The user provided a connection, so don't second-guess them by using a different node. - return err - } - - // The user did not provide a connection, so attempt to use the default node. - if c.debug { - fmt.Fprintf(os.Stderr, "No node connection, attempting to use %s\n", defaultBeaconNode) - } - c.consensusClient, err = util.ConnectToBeaconNode(ctx, defaultBeaconNode, c.timeout, true) - if err != nil { - return err - } - if !c.quiet { - fmt.Fprintf(os.Stderr, "No connection supplied; using mainnet public access endpoint\n") - } + return err } // Set up chaintime. diff --git a/cmd/validator/expectation/process.go b/cmd/validator/expectation/process.go index 9ab7c4b..2faad08 100644 --- a/cmd/validator/expectation/process.go +++ b/cmd/validator/expectation/process.go @@ -123,7 +123,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to beacon node") } diff --git a/cmd/validator/summary/process.go b/cmd/validator/summary/process.go index 3387acf..1383dc9 100644 --- a/cmd/validator/summary/process.go +++ b/cmd/validator/summary/process.go @@ -377,7 +377,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to beacon node") } diff --git a/cmd/validator/yield/process.go b/cmd/validator/yield/process.go index 89531de..ac57153 100644 --- a/cmd/validator/yield/process.go +++ b/cmd/validator/yield/process.go @@ -112,7 +112,12 @@ func (c *command) setup(ctx context.Context) error { var err error // Connect to the client. - c.eth2Client, err = util.ConnectToBeaconNode(ctx, c.connection, c.timeout, c.allowInsecureConnections) + c.eth2Client, err = util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: c.connection, + Timeout: c.timeout, + AllowInsecure: c.allowInsecureConnections, + LogFallback: !c.quiet, + }) if err != nil { return errors.Wrap(err, "failed to connect to beacon node") } diff --git a/cmd/validatorinfo.go b/cmd/validatorinfo.go index 2c27781..e0bff5a 100644 --- a/cmd/validatorinfo.go +++ b/cmd/validatorinfo.go @@ -45,11 +45,12 @@ In quiet mode this will return 0 if the validator information can be obtained, o Run: func(cmd *cobra.Command, args []string) { ctx := context.Background() - eth2Client, err := util.ConnectToBeaconNode(ctx, - viper.GetString("connection"), - viper.GetDuration("timeout"), - viper.GetBool("allow-insecure-connections"), - ) + eth2Client, err := util.ConnectToBeaconNode(ctx, &util.ConnectOpts{ + Address: viper.GetString("connection"), + Timeout: viper.GetDuration("timeout"), + AllowInsecure: viper.GetBool("allow-insecure-connections"), + LogFallback: !viper.GetBool("quiet"), + }) errCheck(err, "Failed to connect to Ethereum 2 beacon node") if viper.GetString("validator") == "" { diff --git a/cmd/version.go b/cmd/version.go index 43aab1f..dee434f 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -24,7 +24,7 @@ import ( // ReleaseVersion is the release version of the codebase. // Usually overridden by tag names when building binaries. -var ReleaseVersion = "local build (latest release 1.28.5)" +var ReleaseVersion = "local build (latest release 1.29.0)" // versionCmd represents the version command. var versionCmd = &cobra.Command{ diff --git a/docs/usage.md b/docs/usage.md index 5d38773..9d4a3bb 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -481,12 +481,10 @@ Exit commands focus on information about validator exits generated by the `ethdo #### `verify` `ethdo exit verify` verifies the validator exit information in a JSON file generated by the `ethdo validator exit` command. Options include: - - `exit`: either a path to the JSON file or the JSON itself - - `account`: the account that generated the exit transaction (if available as an account, in format "wallet/account") - - `pubkey`: the public key of the account that generated the exit transaction + - `signed-operation`: either a path to the JSON file or the JSON itself ```sh -$ ethdo exit verify --exit=${HOME}/exit.json --pubkey=0xa951530887ae2494a8cc4f11cf186963b0051ac4f7942375585b9cf98324db1e532a67e521d0fcaab510edad1352394c +$ ethdo exit verify --signed-operation=${HOME}/exit.json ``` ### `node` commands @@ -547,9 +545,7 @@ Sync committee commands focus on information about sync committees. #### `inclusion` `ethdo synccommittee inclusion` provides information about the inclusion, or not, of a validator's sync committee messages. Options include: - - `account` the account of the validator for which to print sync committee contributions - - `index` the index of the validator for which to print sync committee contributions - - `pubkey` the public key of the validator for which to print sync committee contributions + - `validator`: the index, public key or account of the validator in format "wallet/account" - `epoch` the specific epoch for which to print sync committee contributions. Defaults to the last complete epoch @@ -686,23 +682,21 @@ Attester commands focus on Ethereum 2 validators' actions as attesters. `ethdo attester duties` provides information on the duties that a given validator has in a given epoch. Options include: - `epoch` the epoch in which to obtain the duties (defaults to current epoch) - - `account` the account for which to fetch the duties (in format "wallet/account") - - `pubkey` the public key for which to fetch the duties + - `validator` the validator for which to fetch the duties, as an index, publi key or account in the format "wallet/account" ```sh -$ ethdo attester duties --account=Validators/0 --epoch=5 +$ ethdo attester duties --validator=Validators/0 --epoch=5 Validator attesting in slot 186 committee 3 ``` #### `inclusion` `ethdo attester inclusion` finds the block with wihch an attestation is included on the chain. Options include: - - `epoch` the epoch in which to obtain the inclusion information (defaults to current epoch) - - `account` the account for which to fetch the inclusion information (in format "wallet/account") - - `pubkey` the public key for which to fetch the inclusion information + - `epoch` the epoch in which to obtain the inclusion information (defaults to previous epoch) + - `validator` the validator for which to fetch the duties, as an index, publi key or account in the format "wallet/account" ```sh -$ ethdo attester inclusion --account=Validators/1 --epoch=6484 +$ ethdo attester inclusion --validator=Validators/1 --epoch=6484 Attestation included in block 207492 (inclusion delay 1) ``` diff --git a/services/chaintime/service.go b/services/chaintime/service.go index 9fd3944..1b2399d 100644 --- a/services/chaintime/service.go +++ b/services/chaintime/service.go @@ -46,6 +46,8 @@ type Service interface { SlotToSyncCommitteePeriod(slot phase0.Slot) uint64 // FirstSlotOfEpoch provides the first slot of the given epoch. FirstSlotOfEpoch(epoch phase0.Epoch) phase0.Slot + // LastSlotOfEpoch provides the last slot of the given epoch. + LastSlotOfEpoch(epoch phase0.Epoch) phase0.Slot // TimestampToSlot provides the slot of the given timestamp. TimestampToSlot(timestamp time.Time) phase0.Slot // TimestampToEpoch provides the epoch of the given timestamp. diff --git a/services/chaintime/standard/service.go b/services/chaintime/standard/service.go index 729f417..d1e93aa 100644 --- a/services/chaintime/standard/service.go +++ b/services/chaintime/standard/service.go @@ -178,6 +178,11 @@ func (s *Service) FirstSlotOfEpoch(epoch phase0.Epoch) phase0.Slot { return phase0.Slot(uint64(epoch) * s.slotsPerEpoch) } +// LastSlotOfEpoch provides the last slot of the given epoch. +func (s *Service) LastSlotOfEpoch(epoch phase0.Epoch) phase0.Slot { + return phase0.Slot(uint64(epoch)*s.slotsPerEpoch + s.slotsPerEpoch - 1) +} + // TimestampToSlot provides the slot of the given timestamp. func (s *Service) TimestampToSlot(timestamp time.Time) phase0.Slot { if timestamp.Before(s.genesisTime) { diff --git a/util/beaconnode.go b/util/beaconnode.go index b50968c..54af7b6 100644 --- a/util/beaconnode.go +++ b/util/beaconnode.go @@ -1,4 +1,4 @@ -// Copyright © 2020 Weald Technology Trading +// Copyright © 2020, 2023 Weald Technology Trading // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -17,6 +17,7 @@ import ( "context" "fmt" "net/url" + "os" "strings" "time" @@ -33,25 +34,48 @@ var defaultBeaconNodeAddresses = []string{ "localhost:3500", // Prysm } +// fallbackBeaconNode is used if no other connection is supplied. +var fallbackBeaconNode = "http://mainnet-consensus.attestant.io/" + +type ConnectOpts struct { + Address string + Timeout time.Duration + AllowInsecure bool + LogFallback bool +} + // ConnectToBeaconNode connects to a beacon node at the given address. -func ConnectToBeaconNode(ctx context.Context, address string, timeout time.Duration, allowInsecure bool) (eth2client.Service, error) { - if timeout == 0 { +func ConnectToBeaconNode(ctx context.Context, opts *ConnectOpts) (eth2client.Service, error) { + if opts == nil { + return nil, errors.New("no options specified") + } + + if opts.Timeout == 0 { return nil, errors.New("no timeout specified") } - if address != "" { + if opts.Address != "" { // We have an explicit address; use it. - return connectToBeaconNode(ctx, address, timeout, allowInsecure) + return connectToBeaconNode(ctx, opts.Address, opts.Timeout, opts.AllowInsecure) } // Try the defaults. for _, address := range defaultBeaconNodeAddresses { - client, err := connectToBeaconNode(ctx, address, timeout, allowInsecure) + client, err := connectToBeaconNode(ctx, address, opts.Timeout, opts.AllowInsecure) if err == nil { return client, nil } } + // The user did not provide a connection, so attempt to use the fallback node. + if opts.LogFallback { + fmt.Fprintf(os.Stderr, "No connection supplied with --connection parameter and no local beacon node found, attempting to use mainnet fallback\n") + } + client, err := connectToBeaconNode(ctx, fallbackBeaconNode, opts.Timeout, true) + if err == nil { + return client, nil + } + return nil, errors.New("failed to connect to any beacon node") } diff --git a/util/epoch.go b/util/epoch.go index e19faf4..529aa71 100644 --- a/util/epoch.go +++ b/util/epoch.go @@ -26,7 +26,7 @@ import ( func ParseEpoch(_ context.Context, chainTime chaintime.Service, epochStr string) (phase0.Epoch, error) { currentEpoch := chainTime.CurrentEpoch() switch epochStr { - case "", "current", "-0": + case "", "current", "head", "-0": return currentEpoch, nil case "last": if currentEpoch > 0 {