From 93e632972af966e32bcea6c87a467aba63d9dfa2 Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Tue, 10 Nov 2020 23:47:21 +0000 Subject: [PATCH] Move to eth2client --- cmd/account/key/input_internal_test.go | 1 - cmd/attester/inclusion/input.go | 130 +++++ cmd/attester/inclusion/input_internal_test.go | 96 ++++ cmd/attester/inclusion/output.go | 46 ++ .../inclusion/output_internal_test.go | 62 +++ cmd/attester/inclusion/process.go | 85 ++++ .../inclusion/process_internal_test.go | 69 +++ cmd/attester/inclusion/run.go | 50 ++ cmd/attesterinclusion.go | 118 +---- cmd/block/info/input.go | 68 +++ cmd/block/info/input_internal_test.go | 126 +++++ cmd/block/info/output.go | 325 ++++++++++++ cmd/block/info/output_internal_test.go | 177 +++++++ cmd/block/info/process.go | 107 ++++ cmd/block/info/process_internal_test.go | 63 +++ cmd/block/info/run.go | 50 ++ cmd/blockinfo.go | 267 +--------- cmd/chaininfo.go | 50 +- cmd/chainstatus.go | 100 ++-- cmd/exitverify.go | 37 +- cmd/networks.go | 46 -- cmd/nodeinfo.go | 36 +- cmd/root.go | 42 +- cmd/signing.go | 11 - cmd/validator/depositdata/input.go | 76 +-- .../depositdata/input_internal_test.go | 41 +- cmd/validator/depositdata/output.go | 78 ++- .../depositdata/output_internal_test.go | 475 +++++++++++------- cmd/validator/depositdata/process.go | 27 +- .../depositdata/process_internal_test.go | 110 +++- cmd/validator/exit/input.go | 147 ++++++ cmd/validator/exit/input_internal_test.go | 186 +++++++ cmd/validator/exit/output.go | 57 +++ cmd/validator/exit/output_internal_test.go | 97 ++++ cmd/validator/exit/process.go | 133 +++++ cmd/validator/exit/process_internal_test.go | 234 +++++++++ cmd/validator/exit/run.go | 50 ++ cmd/validatorexit.go | 254 ++-------- cmd/validatorinfo.go | 146 +++--- docs/usage.md | 5 +- go.mod | 2 +- go.sum | 4 + grpc/beaconchain.go | 318 ------------ grpc/beaconnode.go | 85 ---- grpc/connect.go | 41 -- grpc/node.go | 101 ---- signing/misc.go | 18 +- signing/signroot.go | 46 +- testutil/bytes.go | 56 +++ util/account.go | 62 +++ util/beaconnode.go | 52 ++ util/networks.go | 55 ++ util/scratchaccount.go | 26 +- util/scratchaccount_test.go | 42 +- util/validatorexitdata.go | 63 +++ util/validatorexitdata_test.go | 57 +++ 56 files changed, 3533 insertions(+), 1773 deletions(-) create mode 100644 cmd/attester/inclusion/input.go create mode 100644 cmd/attester/inclusion/input_internal_test.go create mode 100644 cmd/attester/inclusion/output.go create mode 100644 cmd/attester/inclusion/output_internal_test.go create mode 100644 cmd/attester/inclusion/process.go create mode 100644 cmd/attester/inclusion/process_internal_test.go create mode 100644 cmd/attester/inclusion/run.go create mode 100644 cmd/block/info/input.go create mode 100644 cmd/block/info/input_internal_test.go create mode 100644 cmd/block/info/output.go create mode 100644 cmd/block/info/output_internal_test.go create mode 100644 cmd/block/info/process.go create mode 100644 cmd/block/info/process_internal_test.go create mode 100644 cmd/block/info/run.go delete mode 100644 cmd/networks.go create mode 100644 cmd/validator/exit/input.go create mode 100644 cmd/validator/exit/input_internal_test.go create mode 100644 cmd/validator/exit/output.go create mode 100644 cmd/validator/exit/output_internal_test.go create mode 100644 cmd/validator/exit/process.go create mode 100644 cmd/validator/exit/process_internal_test.go create mode 100644 cmd/validator/exit/run.go delete mode 100644 grpc/beaconchain.go delete mode 100644 grpc/beaconnode.go delete mode 100644 grpc/connect.go delete mode 100644 grpc/node.go create mode 100644 util/account.go create mode 100644 util/beaconnode.go create mode 100644 util/networks.go create mode 100644 util/validatorexitdata.go create mode 100644 util/validatorexitdata_test.go diff --git a/cmd/account/key/input_internal_test.go b/cmd/account/key/input_internal_test.go index c86d66b..0d07b05 100644 --- a/cmd/account/key/input_internal_test.go +++ b/cmd/account/key/input_internal_test.go @@ -36,7 +36,6 @@ func TestInput(t *testing.T) { 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)) - require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil)) viper.Set("passphrase", "pass") _, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(), "Interop 0", diff --git a/cmd/attester/inclusion/input.go b/cmd/attester/inclusion/input.go new file mode 100644 index 0000000..6095185 --- /dev/null +++ b/cmd/attester/inclusion/input.go @@ -0,0 +1,130 @@ +// 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 attesterinclusion + +import ( + "context" + "encoding/hex" + "fmt" + "strings" + "time" + + 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/spf13/viper" + "github.com/wealdtech/ethdo/core" + "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 + // Chain information. + slotsPerEpoch uint64 + // Operation. + validator *api.Validator + eth2Client eth2client.Service + epoch spec.Epoch + account e2wtypes.Account +} + +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") + + // Account. + var err error + data.account, err = attesterInclusionAccount() + if err != nil { + return nil, errors.Wrap(err, "failed to obtain account") + } + + // Ethereum 2 client. + 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") + } + + // Epoch + epoch := viper.GetInt64("epoch") + if epoch == -1 { + config, err := data.eth2Client.(eth2client.SpecProvider).Spec(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain beacon chain configuration") + } + data.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64) + 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) + + pubKeys := make([]spec.BLSPubKey, 1) + pubKey, err := core.BestPublicKey(data.account) + 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") + } + data.validator = validators[0] + + return data, nil +} + +// attesterInclusionAccount obtains the account for the attester inclusion command. +func attesterInclusionAccount() (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 = core.WalletAndAccountFromPath(ctx, viper.GetString("account")) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain account") + } + } else { + pubKey := viper.GetString("pubkey") + pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubKey, "0x")) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", pubKey)) + } + account, err = util.NewScratchAccount(nil, pubKeyBytes) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", pubKey)) + } + } + return account, nil +} diff --git a/cmd/attester/inclusion/input_internal_test.go b/cmd/attester/inclusion/input_internal_test.go new file mode 100644 index 0000000..00b33e0 --- /dev/null +++ b/cmd/attester/inclusion/input_internal_test.go @@ -0,0 +1,96 @@ +// 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 attesterinclusion + +import ( + "context" + "os" + "testing" + + "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{}{}, + err: "timeout is required", + }, + { + name: "AccountMissing", + vars: map[string]interface{}{ + "timeout": "5s", + }, + err: "failed to obtain account: invalid public key : public key must be 48 bytes", + }, + { + name: "ConnectionMissing", + vars: map[string]interface{}{ + "timeout": "5s", + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + }, + err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified", + }, + } + + 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) + } + }) + } +} diff --git a/cmd/attester/inclusion/output.go b/cmd/attester/inclusion/output.go new file mode 100644 index 0000000..29668a9 --- /dev/null +++ b/cmd/attester/inclusion/output.go @@ -0,0 +1,46 @@ +// 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 attesterinclusion + +import ( + "context" + "fmt" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +type dataOut struct { + debug bool + quiet bool + verbose bool + slot spec.Slot + attestationIndex uint64 + inclusionDelay spec.Slot + found bool +} + +func output(ctx context.Context, data *dataOut) (string, error) { + if data == nil { + return "", errors.New("no data") + } + + if !data.quiet { + if data.found { + return fmt.Sprintf("Attestation included in block %d, attestation %d (inclusion delay %d)", data.slot, data.attestationIndex, data.inclusionDelay), nil + } + return "Attestation not found", nil + } + return "", nil +} diff --git a/cmd/attester/inclusion/output_internal_test.go b/cmd/attester/inclusion/output_internal_test.go new file mode 100644 index 0000000..65691cc --- /dev/null +++ b/cmd/attester/inclusion/output_internal_test.go @@ -0,0 +1,62 @@ +// 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 attesterinclusion + +import ( + "context" + "testing" + + "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: "Empty", + dataOut: &dataOut{}, + res: "Attestation not found", + }, + { + name: "Found", + dataOut: &dataOut{ + found: true, + slot: 123, + attestationIndex: 456, + inclusionDelay: 7, + }, + res: "Attestation included in block 123, attestation 456 (inclusion delay 7)", + }, + } + + 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) + } + }) + } +} diff --git a/cmd/attester/inclusion/process.go b/cmd/attester/inclusion/process.go new file mode 100644 index 0000000..906061f --- /dev/null +++ b/cmd/attester/inclusion/process.go @@ -0,0 +1,85 @@ +// 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 attesterinclusion + +import ( + "context" + "fmt" + + 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" +) + +func process(ctx context.Context, data *dataIn) (*dataOut, error) { + if data == nil { + return nil, errors.New("no data") + } + + results := &dataOut{ + debug: data.debug, + quiet: data.quiet, + verbose: data.verbose, + } + + duty, err := duty(ctx, data.eth2Client, data.validator, data.epoch, data.slotsPerEpoch) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain duty for validator") + } + + startSlot := duty.Slot + 1 + endSlot := startSlot + 32 + for slot := startSlot; slot < endSlot; slot++ { + signedBlock, err := data.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, fmt.Sprintf("%d", slot)) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain block") + } + if signedBlock == nil { + continue + } + if signedBlock.Message.Slot != slot { + continue + } + if data.debug { + fmt.Printf("Fetched block for slot %d\n", slot) + } + for i, attestation := range signedBlock.Message.Body.Attestations { + if attestation.Data.Slot == duty.Slot && + attestation.Data.Index == duty.CommitteeIndex && + attestation.AggregationBits.BitAt(duty.ValidatorCommitteeIndex) { + results.slot = slot + results.attestationIndex = uint64(i) + results.inclusionDelay = slot - duty.Slot + results.found = true + return results, nil + } + } + } + return nil, errors.New("not found") +} + +func duty(ctx context.Context, eth2Client eth2client.Service, validator *api.Validator, epoch spec.Epoch, slotsPerEpoch uint64) (*api.AttesterDuty, error) { + // Find the attesting slot for the given epoch. + duties, err := eth2Client.(eth2client.AttesterDutiesProvider).AttesterDuties(ctx, epoch, []spec.ValidatorIndex{validator.Index}) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain attester duties") + } + + if len(duties) == 0 { + return nil, errors.New("validator does not have duty for that epoch") + } + + return duties[0], nil +} diff --git a/cmd/attester/inclusion/process_internal_test.go b/cmd/attester/inclusion/process_internal_test.go new file mode 100644 index 0000000..9374c3e --- /dev/null +++ b/cmd/attester/inclusion/process_internal_test.go @@ -0,0 +1,69 @@ +// 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 attesterinclusion + +import ( + "context" + "os" + "testing" + + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/auto" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +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")), + ) + require.NoError(t, err) + + tests := []struct { + name string + dataIn *dataIn + err string + }{ + { + name: "Nil", + err: "no data", + }, + { + name: "Client", + dataIn: &dataIn{ + eth2Client: eth2Client, + slotsPerEpoch: 32, + validator: &api.Validator{ + Index: 0, + }, + epoch: 100, + }, + }, + } + + 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) + } + }) + } +} diff --git a/cmd/attester/inclusion/run.go b/cmd/attester/inclusion/run.go new file mode 100644 index 0000000..77d5a1b --- /dev/null +++ b/cmd/attester/inclusion/run.go @@ -0,0 +1,50 @@ +// 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 attesterinclusion + +import ( + "context" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Run runs the wallet create data command. +func Run(cmd *cobra.Command) (string, error) { + ctx := context.Background() + dataIn, err := input(ctx) + if err != nil { + return "", errors.Wrap(err, "failed to obtain input") + } + + // Further errors do not need a usage report. + cmd.SilenceUsage = true + + dataOut, err := process(ctx, dataIn) + if err != nil { + return "", errors.Wrap(err, "failed to process") + } + + if viper.GetBool("quiet") { + return "", nil + } + + results, err := output(ctx, dataOut) + if err != nil { + return "", errors.Wrap(err, "failed to obtain output") + } + + return results, nil +} diff --git a/cmd/attesterinclusion.go b/cmd/attesterinclusion.go index 6c2f7ee..e33ad20 100644 --- a/cmd/attesterinclusion.go +++ b/cmd/attesterinclusion.go @@ -14,19 +14,11 @@ package cmd import ( - "context" - "encoding/hex" "fmt" - "os" - "strings" - "time" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/wealdtech/ethdo/grpc" - "github.com/wealdtech/ethdo/util" - e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" + attesterinclusion "github.com/wealdtech/ethdo/cmd/attester/inclusion" ) var attesterInclusionCmd = &cobra.Command{ @@ -37,111 +29,21 @@ var attesterInclusionCmd = &cobra.Command{ ethdo attester inclusion --account=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.`, - Run: func(cmd *cobra.Command, args []string) { - err := connect() - errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain block") - - // Obtain the epoch. - epoch := viper.GetInt64("epoch") - if epoch == -1 { - outputIf(debug, "No epoch supplied; fetching current epoch") - config, err := grpc.FetchChainConfig(eth2GRPCConn) - errCheck(err, "Failed to obtain beacon chain configuration") - slotsPerEpoch := config["SlotsPerEpoch"].(uint64) - secondsPerSlot := config["SecondsPerSlot"].(uint64) - genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn) - errCheck(err, "Failed to obtain beacon chain genesis") - epoch = int64(time.Since(genesisTime).Seconds()) / int64(secondsPerSlot*slotsPerEpoch) - if epoch > 0 { - epoch-- - } + RunE: func(cmd *cobra.Command, args []string) error { + res, err := attesterinclusion.Run(cmd) + if err != nil { + return err } - outputIf(debug, fmt.Sprintf("Epoch is %d", epoch)) - - // Obtain the validator. - account, err := attesterInclusionAccount() - errCheck(err, "Failed to obtain account") - validatorIndex, err := grpc.FetchValidatorIndex(eth2GRPCConn, account) - errCheck(err, "Failed to obtain validator") - - // Find the attesting slot for the given epoch. - committees, err := grpc.FetchValidatorCommittees(eth2GRPCConn, uint64(epoch)) - errCheck(err, "Failed to obtain validator committees") - - slot := uint64(0) - committeeIndex := uint64(0) - validatorPositionInCommittee := uint64(0) - found := false - for searchSlot, committee := range committees { - for searchCommitteeIndex, committeeValidatorIndices := range committee { - for position, committeeValidatorIndex := range committeeValidatorIndices { - if validatorIndex == committeeValidatorIndex { - outputIf(verbose, fmt.Sprintf("Validator %d scheduled to attest at slot %d for epoch %d: entry %d in committee %d", validatorIndex, searchSlot, epoch, position, searchCommitteeIndex)) - slot = searchSlot - committeeIndex = uint64(searchCommitteeIndex) - validatorPositionInCommittee = uint64(position) - found = true - break - } - } - } + if viper.GetBool("quiet") { + return nil } - assert(found, fmt.Sprintf("Failed to find attester duty for validator in epoch %d", epoch)) - - startSlot := slot + 1 - endSlot := startSlot + 32 - for curSlot := startSlot; curSlot < endSlot; curSlot++ { - signedBlock, err := grpc.FetchBlock(eth2GRPCConn, curSlot) - errCheck(err, "Failed to obtain block") - if signedBlock == nil { - outputIf(debug, fmt.Sprintf("No block at slot %d", curSlot)) - continue - } - outputIf(debug, fmt.Sprintf("Fetched block %d", curSlot)) - for i, attestation := range signedBlock.Block.Body.Attestations { - outputIf(debug, fmt.Sprintf("Attestation %d is for slot %d and committee %d", i, attestation.Data.Slot, attestation.Data.CommitteeIndex)) - if attestation.Data.Slot == slot && - attestation.Data.CommitteeIndex == committeeIndex && - attestation.AggregationBits.BitAt(validatorPositionInCommittee) { - if verbose { - fmt.Printf("Attestation for epoch %d included in block %d, attestation %d (inclusion delay %d)\n", epoch, curSlot, i, curSlot-slot) - } else if !quiet { - fmt.Printf("Attestation for epoch %d included in block %d (inclusion delay %d)\n", epoch, curSlot, curSlot-slot) - } - os.Exit(_exitSuccess) - } - } + if res != "" { + fmt.Println(res) } - outputIf(verbose, fmt.Sprintf("Attestation for epoch %d not included on the chain", epoch)) - os.Exit(_exitFailure) + return nil }, } -// attesterInclusionAccount obtains the account for the attester inclusion command. -func attesterInclusionAccount() (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 { - pubKey := viper.GetString("pubkey") - pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubKey, "0x")) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", pubKey)) - } - account, err = util.NewScratchAccount(nil, pubKeyBytes) - if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", pubKey)) - } - } - return account, nil -} - func init() { attesterCmd.AddCommand(attesterInclusionCmd) attesterFlags(attesterInclusionCmd) diff --git a/cmd/block/info/input.go b/cmd/block/info/input.go new file mode 100644 index 0000000..b63e8c1 --- /dev/null +++ b/cmd/block/info/input.go @@ -0,0 +1,68 @@ +// 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 blockinfo + +import ( + "context" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/pkg/errors" + "github.com/spf13/viper" + "github.com/wealdtech/ethdo/util" +) + +type dataIn struct { + // System. + timeout time.Duration + quiet bool + verbose bool + debug bool + // Operation. + eth2Client eth2client.Service + jsonOutput bool + // Chain information. + blockID string + stream bool +} + +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.jsonOutput = viper.GetBool("json") + + 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")) + if err != nil { + return nil, errors.Wrap(err, "failed to connect to Ethereum 2 beacon node") + } + + if viper.GetString("blockid") == "" { + data.blockID = "head" + } else { + // Specific slot. + data.blockID = viper.GetString("blockid") + } + + return data, nil +} diff --git a/cmd/block/info/input_internal_test.go b/cmd/block/info/input_internal_test.go new file mode 100644 index 0000000..9677701 --- /dev/null +++ b/cmd/block/info/input_internal_test.go @@ -0,0 +1,126 @@ +// 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 blockinfo + +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{}{}, + err: "timeout is required", + }, + { + name: "ConnectionMissing", + vars: map[string]interface{}{ + "timeout": "5s", + }, + err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: problem with parameters: no address specified", + }, + { + name: "ConnectionBad", + vars: map[string]interface{}{ + "timeout": "5s", + "connection": "localhost:1", + "blockid": "justified", + }, + res: &dataIn{ + timeout: 5 * time.Second, + blockID: "justified", + }, + err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method", + }, + { + name: "BlockIDNil", + vars: map[string]interface{}{ + "timeout": "5s", + "connection": os.Getenv("ETHDO_TEST_CONNECTION"), + }, + res: &dataIn{ + timeout: 5 * time.Second, + blockID: "head", + }, + }, + { + name: "BlockIDSpecific", + vars: map[string]interface{}{ + "timeout": "5s", + "connection": os.Getenv("ETHDO_TEST_CONNECTION"), + "blockid": "justified", + }, + res: &dataIn{ + timeout: 5 * time.Second, + blockID: "justified", + }, + }, + } + + 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) + require.Equal(t, test.res.blockID, res.blockID) + } + }) + } +} diff --git a/cmd/block/info/output.go b/cmd/block/info/output.go new file mode 100644 index 0000000..f900a2a --- /dev/null +++ b/cmd/block/info/output.go @@ -0,0 +1,325 @@ +// 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 blockinfo + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "sort" + "strings" + "time" + "unicode/utf8" + + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/prysmaticlabs/go-bitfield" + "github.com/wealdtech/go-string2eth" +) + +type dataOut struct { + debug bool + verbose bool + eth2Client eth2client.Service + genesisTime time.Time + slotDuration time.Duration + slotsPerEpoch uint64 +} + +func output(ctx context.Context, data *dataOut) (string, error) { + if data == nil { + return "", errors.New("no data") + } + + return "", nil +} + +func outputBlockGeneral(ctx context.Context, verbose bool, block *spec.BeaconBlock, genesisTime time.Time, slotDuration time.Duration, slotsPerEpoch uint64) (string, error) { + bodyRoot, err := block.Body.HashTreeRoot() + if err != nil { + return "", errors.Wrap(err, "failed to generate block root") + } + + res := strings.Builder{} + + res.WriteString(fmt.Sprintf("Slot: %d\n", block.Slot)) + res.WriteString(fmt.Sprintf("Epoch: %d\n", spec.Epoch(uint64(block.Slot)/slotsPerEpoch))) + res.WriteString(fmt.Sprintf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(block.Slot)*int64(slotDuration.Seconds()), 0))) + res.WriteString(fmt.Sprintf("Block root: %#x\n", bodyRoot)) + if verbose { + res.WriteString(fmt.Sprintf("Parent root: %#x\n", block.ParentRoot)) + res.WriteString(fmt.Sprintf("State root: %#x\n", block.StateRoot)) + } + if len(block.Body.Graffiti) > 0 && hex.EncodeToString(block.Body.Graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" { + if utf8.Valid(block.Body.Graffiti) { + res.WriteString(fmt.Sprintf("Graffiti: %s\n", string(block.Body.Graffiti))) + } else { + res.WriteString(fmt.Sprintf("Graffiti: %#x\n", block.Body.Graffiti)) + } + } + + return res.String(), nil +} + +func outputBlockETH1Data(ctx context.Context, eth1Data *spec.ETH1Data) (string, error) { + res := strings.Builder{} + + res.WriteString(fmt.Sprintf("Ethereum 1 deposit count: %d\n", eth1Data.DepositCount)) + res.WriteString(fmt.Sprintf("Ethereum 1 deposit root: %#x\n", eth1Data.DepositRoot)) + res.WriteString(fmt.Sprintf("Ethereum 1 block hash: %#x\n", eth1Data.BlockHash)) + + return res.String(), nil +} + +func outputBlockAttestations(ctx context.Context, eth2Client eth2client.Service, verbose bool, attestations []*spec.Attestation) (string, error) { + res := strings.Builder{} + + validatorCommittees := make(map[spec.Slot]map[spec.CommitteeIndex][]spec.ValidatorIndex) + res.WriteString(fmt.Sprintf("Attestations: %d\n", len(attestations))) + if verbose { + beaconCommitteesProvider, isProvider := eth2Client.(eth2client.BeaconCommitteesProvider) + if isProvider { + for i, att := range attestations { + res.WriteString(fmt.Sprintf(" %d:\n", i)) + + // Fetch committees for this epoch if not already obtained. + committees, exists := validatorCommittees[att.Data.Slot] + if !exists { + beaconCommittees, err := beaconCommitteesProvider.BeaconCommittees(ctx, fmt.Sprintf("%d", att.Data.Slot)) + if err != nil { + return "", errors.Wrap(err, "failed to obtain beacon committees") + } + for _, beaconCommittee := range beaconCommittees { + if _, exists := validatorCommittees[beaconCommittee.Slot]; !exists { + validatorCommittees[beaconCommittee.Slot] = make(map[spec.CommitteeIndex][]spec.ValidatorIndex) + } + validatorCommittees[beaconCommittee.Slot][beaconCommittee.Index] = beaconCommittee.Validators + } + committees = validatorCommittees[att.Data.Slot] + } + + res.WriteString(fmt.Sprintf(" Committee index: %d\n", att.Data.Index)) + res.WriteString(fmt.Sprintf(" Attesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len())) + res.WriteString(fmt.Sprintf(" Aggregation bits: %s\n", bitsToString(att.AggregationBits))) + res.WriteString(fmt.Sprintf(" Attesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.Index]))) + res.WriteString(fmt.Sprintf(" Slot: %d\n", att.Data.Slot)) + res.WriteString(fmt.Sprintf(" Beacon block root: %#x\n", att.Data.BeaconBlockRoot)) + res.WriteString(fmt.Sprintf(" Source epoch: %d\n", att.Data.Source.Epoch)) + res.WriteString(fmt.Sprintf(" Source root: %#x\n", att.Data.Source.Root)) + res.WriteString(fmt.Sprintf(" Target epoch: %d\n", att.Data.Target.Epoch)) + res.WriteString(fmt.Sprintf(" Target root: %#x\n", att.Data.Target.Root)) + } + } + } + + return res.String(), nil +} + +func outputBlockAttesterSlashings(ctx context.Context, eth2Client eth2client.Service, verbose bool, attesterSlashings []*spec.AttesterSlashing) (string, error) { + res := strings.Builder{} + + res.WriteString(fmt.Sprintf("Attester slashings: %d\n", len(attesterSlashings))) + if verbose { + for i, slashing := range attesterSlashings { + // Say what was slashed. + att1 := slashing.Attestation1 + att2 := slashing.Attestation2 + slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices) + if len(slashedIndices) == 0 { + continue + } + + res.WriteString(fmt.Sprintf(" %d:\n", i)) + res.WriteString(fmt.Sprintln(" Slashed validators:")) + validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", slashedIndices) + if err != nil { + return "", errors.Wrap(err, "failed to obtain beacon committees") + } + for k, v := range validators { + res.WriteString(fmt.Sprintf(" %#x (%d)\n", v.Validator.PublicKey[:], k)) + } + + // Say what caused the slashing. + if att1.Data.Target.Epoch == att2.Data.Target.Epoch { + res.WriteString(fmt.Sprintf(" Double voted for same target epoch (%d):\n", att1.Data.Target.Epoch)) + if !bytes.Equal(att1.Data.Target.Root[:], att2.Data.Target.Root[:]) { + res.WriteString(fmt.Sprintf(" Attestation 1 target epoch root: %#x\n", att1.Data.Target.Root)) + res.WriteString(fmt.Sprintf(" Attestation 2target epoch root: %#x\n", att2.Data.Target.Root)) + } + if !bytes.Equal(att1.Data.BeaconBlockRoot[:], att2.Data.BeaconBlockRoot[:]) { + res.WriteString(fmt.Sprintf(" Attestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot)) + res.WriteString(fmt.Sprintf(" Attestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot)) + } + } else if att1.Data.Source.Epoch < att2.Data.Source.Epoch && + att1.Data.Target.Epoch > att2.Data.Target.Epoch { + res.WriteString(" Surround voted:\n") + res.WriteString(fmt.Sprintf(" Attestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch)) + res.WriteString(fmt.Sprintf(" Attestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch)) + } + } + } + + return res.String(), nil +} + +func outputBlockDeposits(ctx context.Context, verbose bool, deposits []*spec.Deposit) (string, error) { + res := strings.Builder{} + + // Deposits. + res.WriteString(fmt.Sprintf("Deposits: %d\n", len(deposits))) + if verbose { + for i, deposit := range deposits { + data := deposit.Data + res.WriteString(fmt.Sprintf(" %d:\n", i)) + res.WriteString(fmt.Sprintf(" Public key: %#x\n", data.PublicKey)) + res.WriteString(fmt.Sprintf(" Amount: %s\n", string2eth.GWeiToString(uint64(data.Amount), true))) + res.WriteString(fmt.Sprintf(" Withdrawal credentials: %#x\n", data.WithdrawalCredentials)) + res.WriteString(fmt.Sprintf(" Signature: %#x\n", data.Signature)) + } + } + + return res.String(), nil +} + +func outputBlockVoluntaryExits(ctx context.Context, eth2Client eth2client.Service, verbose bool, voluntaryExits []*spec.SignedVoluntaryExit) (string, error) { + res := strings.Builder{} + + res.WriteString(fmt.Sprintf("Voluntary exits: %d\n", len(voluntaryExits))) + if verbose { + for i, voluntaryExit := range voluntaryExits { + res.WriteString(fmt.Sprintf(" %d:\n", i)) + validators, err := eth2Client.(eth2client.ValidatorsProvider).Validators(ctx, "head", []spec.ValidatorIndex{voluntaryExit.Message.ValidatorIndex}) + if err != nil { + res.WriteString(fmt.Sprintf(" Error: failed to obtain validators: %v\n", err)) + } else { + res.WriteString(fmt.Sprintf(" Validator: %#x (%d)\n", validators[0].Validator.PublicKey, voluntaryExit.Message.ValidatorIndex)) + res.WriteString(fmt.Sprintf(" Epoch: %d\n", voluntaryExit.Message.Epoch)) + } + } + } + + return res.String(), nil +} + +func outputBlockText(ctx context.Context, data *dataOut, signedBlock *spec.SignedBeaconBlock) (string, error) { + if signedBlock == nil { + return "", errors.New("no block supplied") + } + + body := signedBlock.Message.Body + + res := strings.Builder{} + + // General info. + tmp, err := outputBlockGeneral(ctx, data.verbose, signedBlock.Message, data.genesisTime, data.slotDuration, data.slotsPerEpoch) + if err != nil { + return "", err + } + res.WriteString(tmp) + + // Eth1 data. + if data.verbose { + tmp, err := outputBlockETH1Data(ctx, body.ETH1Data) + if err != nil { + return "", err + } + res.WriteString(tmp) + } + + // Attestations. + tmp, err = outputBlockAttestations(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.Attestations) + if err != nil { + return "", err + } + res.WriteString(tmp) + + // Attester slashings. + tmp, err = outputBlockAttesterSlashings(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.AttesterSlashings) + if err != nil { + return "", err + } + res.WriteString(tmp) + + res.WriteString(fmt.Sprintf("Proposer slashings: %d\n", len(body.ProposerSlashings))) + // TODO verbose proposer slashings. + + tmp, err = outputBlockDeposits(ctx, data.verbose, signedBlock.Message.Body.Deposits) + if err != nil { + return "", err + } + res.WriteString(tmp) + + // Voluntary exits. + tmp, err = outputBlockVoluntaryExits(ctx, data.eth2Client, data.verbose, signedBlock.Message.Body.VoluntaryExits) + if err != nil { + return "", err + } + res.WriteString(tmp) + + return res.String(), nil +} + +// intersection returns a list of items common between the two sets. +func intersection(set1 []uint64, set2 []uint64) []spec.ValidatorIndex { + sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] }) + sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] }) + res := make([]spec.ValidatorIndex, 0) + + set1Pos := 0 + set2Pos := 0 + for set1Pos < len(set1) && set2Pos < len(set2) { + switch { + case set1[set1Pos] < set2[set2Pos]: + set1Pos++ + case set2[set2Pos] < set1[set1Pos]: + set2Pos++ + default: + res = append(res, spec.ValidatorIndex(set1[set1Pos])) + set1Pos++ + set2Pos++ + } + } + + return res +} + +func bitsToString(input bitfield.Bitlist) string { + bits := int(input.Len()) + + res := "" + for i := 0; i < bits; i++ { + if input.BitAt(uint64(i)) { + res = fmt.Sprintf("%s✓", res) + } else { + res = fmt.Sprintf("%s✕", res) + } + if i%8 == 7 { + res = fmt.Sprintf("%s ", res) + } + } + return strings.TrimSpace(res) +} + +func attestingIndices(input bitfield.Bitlist, indices []spec.ValidatorIndex) string { + bits := int(input.Len()) + res := "" + for i := 0; i < bits; i++ { + if input.BitAt(uint64(i)) { + res = fmt.Sprintf("%s%d ", res, indices[i]) + } + } + return strings.TrimSpace(res) +} diff --git a/cmd/block/info/output_internal_test.go b/cmd/block/info/output_internal_test.go new file mode 100644 index 0000000..8023f74 --- /dev/null +++ b/cmd/block/info/output_internal_test.go @@ -0,0 +1,177 @@ +// 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 blockinfo + +import ( + "context" + "testing" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/require" + "github.com/wealdtech/ethdo/testutil" +) + +func TestOutput(t *testing.T) { + tests := []struct { + name string + dataOut *dataOut + res string + err string + }{ + { + name: "Nil", + err: "no data", + }, + { + name: "Good", + dataOut: &dataOut{}, + }, + } + + 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) + } + }) + } +} + +// func TestOutputBlockText(t *testing.T) { +// tests := []struct { +// name string +// dataOut *dataOut +// signedBeaconBlock *spec.SignedBeaconBlock +// err string +// }{ +// { +// name: "Nil", +// err: "no data", +// }, +// { +// name: "Good", +// dataOut: &dataOut{}, +// }, +// } +// +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// res := outputBlockText(context.Background(), test.dataOut, test.signedBeaconBlock) +// if test.err != "" { +// require.EqualError(t, err, test.err) +// } else { +// require.NoError(t, err) +// require.Equal(t, test.res, res) +// } +// }) +// } +// } + +func TestOutputBlockDeposits(t *testing.T) { + tests := []struct { + name string + dataOut *dataOut + verbose bool + deposits []*spec.Deposit + res string + err string + }{ + { + name: "Nil", + res: "Deposits: 0\n", + }, + { + name: "Empty", + res: "Deposits: 0\n", + }, + { + name: "Single", + deposits: []*spec.Deposit{ + { + Data: &spec.DepositData{ + PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), + WithdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + Amount: spec.Gwei(32000000000), + Signature: testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), + }, + }, + }, + res: "Deposits: 1\n", + }, + { + name: "SingleVerbose", + deposits: []*spec.Deposit{ + { + Data: &spec.DepositData{ + PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), + WithdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + Amount: spec.Gwei(32000000000), + Signature: testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), + }, + }, + }, + verbose: true, + res: "Deposits: 1\n 0:\n Public key: 0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c\n Amount: 32 Ether\n Withdrawal credentials: 0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b\n Signature: 0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2\n", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res, err := outputBlockDeposits(context.Background(), test.verbose, test.deposits) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + require.Equal(t, test.res, res) + } + }) + } +} + +func TestOutputBlockETH1Data(t *testing.T) { + tests := []struct { + name string + dataOut *dataOut + verbose bool + eth1Data *spec.ETH1Data + res string + err string + }{ + { + name: "Good", + eth1Data: &spec.ETH1Data{ + DepositRoot: testutil.HexToRoot("0x92407b66d7daf4f30beb84820caae2cbba51add1c4648584101ff3c32151eb83"), + DepositCount: 109936, + BlockHash: testutil.HexToBytes("0x77b03ebaf0f2835b491cbd99a7f4649a03a6e7999678603030a014a3c48b32a4"), + }, + res: "Ethereum 1 deposit count: 109936\nEthereum 1 deposit root: 0x92407b66d7daf4f30beb84820caae2cbba51add1c4648584101ff3c32151eb83\nEthereum 1 block hash: 0x77b03ebaf0f2835b491cbd99a7f4649a03a6e7999678603030a014a3c48b32a4\n", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res, err := outputBlockETH1Data(context.Background(), test.eth1Data) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + require.Equal(t, test.res, res) + } + }) + } +} diff --git a/cmd/block/info/process.go b/cmd/block/info/process.go new file mode 100644 index 0000000..7d3bb04 --- /dev/null +++ b/cmd/block/info/process.go @@ -0,0 +1,107 @@ +// 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 blockinfo + +import ( + "context" + "encoding/json" + "fmt" + "time" + + 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" +) + +var jsonOutput bool +var results *dataOut + +func process(ctx context.Context, data *dataIn) (*dataOut, error) { + if data == nil { + return nil, errors.New("no data") + } + + results = &dataOut{ + debug: data.debug, + verbose: data.verbose, + eth2Client: data.eth2Client, + } + + config, err := results.eth2Client.(eth2client.SpecProvider).Spec(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to connect to obtain configuration information") + } + genesis, err := results.eth2Client.(eth2client.GenesisProvider).Genesis(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to connect to obtain genesis information") + } + results.genesisTime = genesis.GenesisTime + results.slotDuration = config["SECONDS_PER_SLOT"].(time.Duration) + results.slotsPerEpoch = config["SLOTS_PER_EPOCH"].(uint64) + + signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(ctx, data.blockID) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain beacon block") + } + + if err := outputBlock(ctx, data.jsonOutput, signedBlock); err != nil { + return nil, errors.Wrap(err, "failed to output block") + } + + if data.stream { + jsonOutput = data.jsonOutput + err := data.eth2Client.(eth2client.EventsProvider).Events(ctx, []string{"head"}, headEventHandler) + if err != nil { + return nil, errors.Wrap(err, "failed to start block stream") + } + <-ctx.Done() + } + + return &dataOut{}, nil +} + +func headEventHandler(event *api.Event) { + // Only interested in head events. + if event.Topic != "head" { + return + } + + blockID := fmt.Sprintf("%#x", event.Data.(*api.HeadEvent).Block[:]) + signedBlock, err := results.eth2Client.(eth2client.SignedBeaconBlockProvider).SignedBeaconBlock(context.Background(), blockID) + if err != nil { + fmt.Printf("Failed to obtain block: %v\n", err) + } + if err := outputBlock(context.Background(), jsonOutput, signedBlock); err != nil { + fmt.Printf("Failed to display block: %v\n", err) + } +} + +func outputBlock(ctx context.Context, jsonOutput bool, signedBlock *spec.SignedBeaconBlock) error { + switch { + case jsonOutput: + data, err := json.Marshal(signedBlock) + if err != nil { + return errors.Wrap(err, "failed to generate JSON") + } + fmt.Printf("%s\n", string(data)) + default: + data, err := outputBlockText(ctx, results, signedBlock) + if err != nil { + return errors.Wrap(err, "failed to generate text") + } + fmt.Printf("%s\n", data) + } + return nil +} diff --git a/cmd/block/info/process_internal_test.go b/cmd/block/info/process_internal_test.go new file mode 100644 index 0000000..4bfc1df --- /dev/null +++ b/cmd/block/info/process_internal_test.go @@ -0,0 +1,63 @@ +// 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 blockinfo + +import ( + "context" + "os" + "testing" + + "github.com/attestantio/go-eth2-client/auto" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +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")), + ) + require.NoError(t, err) + + tests := []struct { + name string + dataIn *dataIn + err string + }{ + { + name: "Nil", + err: "no data", + }, + { + name: "Client", + dataIn: &dataIn{ + eth2Client: eth2Client, + }, + }, + } + + 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) + } + }) + } +} diff --git a/cmd/block/info/run.go b/cmd/block/info/run.go new file mode 100644 index 0000000..256178a --- /dev/null +++ b/cmd/block/info/run.go @@ -0,0 +1,50 @@ +// 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 blockinfo + +import ( + "context" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Run runs the wallet create data command. +func Run(cmd *cobra.Command) (string, error) { + ctx := context.Background() + dataIn, err := input(ctx) + if err != nil { + return "", errors.Wrap(err, "failed to obtain input") + } + + // Further errors do not need a usage report. + cmd.SilenceUsage = true + + dataOut, err := process(ctx, dataIn) + if err != nil { + return "", errors.Wrap(err, "failed to process") + } + + if viper.GetBool("quiet") { + return "", nil + } + + results, err := output(ctx, dataOut) + if err != nil { + return "", errors.Wrap(err, "failed to obtain output") + } + + return results, nil +} diff --git a/cmd/blockinfo.go b/cmd/blockinfo.go index 2ba9248..ab331f8 100644 --- a/cmd/blockinfo.go +++ b/cmd/blockinfo.go @@ -14,26 +14,13 @@ package cmd import ( - "bytes" - "encoding/hex" "fmt" - "os" - "sort" - "strings" - "time" - "unicode/utf8" - ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" - "github.com/prysmaticlabs/go-bitfield" - "github.com/prysmaticlabs/go-ssz" "github.com/spf13/cobra" - "github.com/wealdtech/ethdo/grpc" - string2eth "github.com/wealdtech/go-string2eth" + "github.com/spf13/viper" + blockinfo "github.com/wealdtech/ethdo/cmd/block/info" ) -var blockInfoSlot int64 -var blockInfoStream bool - var blockInfoCmd = &cobra.Command{ Use: "info", Short: "Obtain information about a block", @@ -42,239 +29,37 @@ var blockInfoCmd = &cobra.Command{ ethdo block info --slot=12345 In quiet mode this will return 0 if the block information is present and not skipped, otherwise 1.`, - Run: func(cmd *cobra.Command, args []string) { - err := connect() - errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain block") - - config, err := grpc.FetchChainConfig(eth2GRPCConn) - errCheck(err, "Failed to obtain beacon chain configuration") - slotsPerEpoch := config["SlotsPerEpoch"].(uint64) - secondsPerSlot := config["SecondsPerSlot"].(uint64) - - genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn) - errCheck(err, "Failed to obtain beacon chain genesis") - - assert(!blockInfoStream || blockInfoSlot == -1, "--slot and --stream are not supported together") - - var slot uint64 - if blockInfoSlot < 0 { - slot, err = grpc.FetchLatestFilledSlot(eth2GRPCConn) - errCheck(err, "Failed to obtain slot of latest block") - } else { - slot = uint64(blockInfoSlot) + RunE: func(cmd *cobra.Command, args []string) error { + res, err := blockinfo.Run(cmd) + if err != nil { + return err } - signedBlock, err := grpc.FetchBlock(eth2GRPCConn, slot) - errCheck(err, "Failed to obtain block") - if signedBlock == nil { - outputIf(!quiet, "No block at that slot") - os.Exit(_exitFailure) + if viper.GetBool("quiet") { + return nil } - outputBlock(signedBlock, genesisTime, secondsPerSlot, slotsPerEpoch) - - if blockInfoStream { - stream, err := grpc.StreamBlocks(eth2GRPCConn) - errCheck(err, "Failed to obtain block stream") - for { - fmt.Println() - signedBlock, err := stream.Recv() - errCheck(err, "Failed to obtain block") - if signedBlock != nil { - outputBlock(signedBlock, genesisTime, secondsPerSlot, slotsPerEpoch) - } - } + if res != "" { + fmt.Println(res) } - - os.Exit(_exitSuccess) + return nil }, } -func outputBlock(signedBlock *ethpb.SignedBeaconBlock, genesisTime time.Time, secondsPerSlot uint64, slotsPerEpoch uint64) { - block := signedBlock.Block - body := block.Body - - // General info. - bodyRoot, err := ssz.HashTreeRoot(block) - errCheck(err, "Failed to calculate block body root") - fmt.Printf("Slot: %d\n", block.Slot) - fmt.Printf("Epoch: %d\n", block.Slot/slotsPerEpoch) - fmt.Printf("Timestamp: %v\n", time.Unix(genesisTime.Unix()+int64(block.Slot*secondsPerSlot), 0)) - fmt.Printf("Block root: %#x\n", bodyRoot) - outputIf(verbose, fmt.Sprintf("Parent root: %#x", block.ParentRoot)) - outputIf(verbose, fmt.Sprintf("State root: %#x", block.StateRoot)) - if len(body.Graffiti) > 0 && hex.EncodeToString(body.Graffiti) != "0000000000000000000000000000000000000000000000000000000000000000" { - if utf8.Valid(body.Graffiti) { - fmt.Printf("Graffiti: %s\n", string(body.Graffiti)) - } else { - fmt.Printf("Graffiti: %#x\n", body.Graffiti) - } - } - - // Eth1 data. - eth1Data := body.Eth1Data - outputIf(verbose, fmt.Sprintf("Ethereum 1 deposit count: %d", eth1Data.DepositCount)) - outputIf(verbose, fmt.Sprintf("Ethereum 1 deposit root: %#x", eth1Data.DepositRoot)) - outputIf(verbose, fmt.Sprintf("Ethereum 1 block hash: %#x", eth1Data.BlockHash)) - - validatorCommittees := make(map[uint64][][]uint64) - - // Attestations. - fmt.Printf("Attestations: %d\n", len(body.Attestations)) - if verbose { - for i, att := range body.Attestations { - fmt.Printf("\t%d:\n", i) - - // Fetch committees for this epoch if not already obtained. - committees, exists := validatorCommittees[att.Data.Slot] - if !exists { - attestationEpoch := att.Data.Slot / slotsPerEpoch - epochCommittees, err := grpc.FetchValidatorCommittees(eth2GRPCConn, attestationEpoch) - errCheck(err, "Failed to obtain committees") - for k, v := range epochCommittees { - validatorCommittees[k] = v - } - committees = validatorCommittees[att.Data.Slot] - } - - fmt.Printf("\t\tCommittee index: %d\n", att.Data.CommitteeIndex) - fmt.Printf("\t\tAttesters: %d/%d\n", att.AggregationBits.Count(), att.AggregationBits.Len()) - fmt.Printf("\t\tAggregation bits: %s\n", bitsToString(att.AggregationBits)) - fmt.Printf("\t\tAttesting indices: %s\n", attestingIndices(att.AggregationBits, committees[att.Data.CommitteeIndex])) - fmt.Printf("\t\tSlot: %d\n", att.Data.Slot) - fmt.Printf("\t\tBeacon block root: %#x\n", att.Data.BeaconBlockRoot) - fmt.Printf("\t\tSource epoch: %d\n", att.Data.Source.Epoch) - fmt.Printf("\t\tSource root: %#x\n", att.Data.Source.Root) - fmt.Printf("\t\tTarget epoch: %d\n", att.Data.Target.Epoch) - fmt.Printf("\t\tTarget root: %#x\n", att.Data.Target.Root) - } - } - - // Attester slashings. - fmt.Printf("Attester slashings: %d\n", len(body.AttesterSlashings)) - if verbose { - for i, slashing := range body.AttesterSlashings { - // Say what was slashed. - att1 := slashing.Attestation_1 - outputIf(debug, fmt.Sprintf("Attestation 1 attesting indices are %v", att1.AttestingIndices)) - att2 := slashing.Attestation_2 - outputIf(debug, fmt.Sprintf("Attestation 2 attesting indices are %v", att2.AttestingIndices)) - slashedIndices := intersection(att1.AttestingIndices, att2.AttestingIndices) - if len(slashedIndices) == 0 { - continue - } - - fmt.Printf("\t%d:\n", i) - - fmt.Println("\t\tSlashed validators:") - for _, slashedIndex := range slashedIndices { - validator, err := grpc.FetchValidatorByIndex(eth2GRPCConn, slashedIndex) - errCheck(err, "Failed to obtain validator information") - fmt.Printf("\t\t\t%#x (%d)\n", validator.PublicKey, slashedIndex) - } - - // Say what caused the slashing. - if att1.Data.Target.Epoch == att2.Data.Target.Epoch { - fmt.Printf("\t\tDouble voted for same target epoch (%d):\n", att1.Data.Target.Epoch) - if !bytes.Equal(att1.Data.Target.Root, att2.Data.Target.Root) { - fmt.Printf("\t\t\tAttestation 1 target epoch root: %#x\n", att1.Data.Target.Root) - fmt.Printf("\t\t\tAttestation 2target epoch root: %#x\n", att2.Data.Target.Root) - } - if !bytes.Equal(att1.Data.BeaconBlockRoot, att2.Data.BeaconBlockRoot) { - fmt.Printf("\t\t\tAttestation 1 beacon block root: %#x\n", att1.Data.BeaconBlockRoot) - fmt.Printf("\t\t\tAttestation 2 beacon block root: %#x\n", att2.Data.BeaconBlockRoot) - } - } else if att1.Data.Source.Epoch < att2.Data.Source.Epoch && - att1.Data.Target.Epoch > att2.Data.Target.Epoch { - fmt.Printf("\t\tSurround voted:\n") - fmt.Printf("\t\t\tAttestation 1 vote: %d->%d\n", att1.Data.Source.Epoch, att1.Data.Target.Epoch) - fmt.Printf("\t\t\tAttestation 2 vote: %d->%d\n", att2.Data.Source.Epoch, att2.Data.Target.Epoch) - } - } - } - - fmt.Printf("Proposer slashings: %d\n", len(body.ProposerSlashings)) - // TODO verbose proposer slashings. - - // Deposits. - fmt.Printf("Deposits: %d\n", len(body.Deposits)) - if verbose { - for i, deposit := range body.Deposits { - data := deposit.Data - fmt.Printf("\t%d:\n", i) - fmt.Printf("\t\tPublic key: %#x\n", data.PublicKey) - fmt.Printf("\t\tAmount: %s\n", string2eth.GWeiToString(data.Amount, true)) - fmt.Printf("\t\tWithdrawal credentials: %#x\n", data.WithdrawalCredentials) - fmt.Printf("\t\tSignature: %#x\n", data.Signature) - } - } - - // Voluntary exits. - fmt.Printf("Voluntary exits: %d\n", len(body.VoluntaryExits)) - if verbose { - for i, voluntaryExit := range body.VoluntaryExits { - fmt.Printf("\t%d:\n", i) - validator, err := grpc.FetchValidatorByIndex(eth2GRPCConn, voluntaryExit.Exit.ValidatorIndex) - errCheck(err, "Failed to obtain validator information") - fmt.Printf("\t\tValidator: %#x (%d)\n", validator.PublicKey, voluntaryExit.Exit.ValidatorIndex) - fmt.Printf("\t\tEpoch: %d\n", voluntaryExit.Exit.Epoch) - } - } -} - -// intersection returns a list of items common between the two sets. -func intersection(set1 []uint64, set2 []uint64) []uint64 { - sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] }) - sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] }) - res := make([]uint64, 0) - - set1Pos := 0 - set2Pos := 0 - for set1Pos < len(set1) && set2Pos < len(set2) { - switch { - case set1[set1Pos] < set2[set2Pos]: - set1Pos++ - case set2[set2Pos] < set1[set1Pos]: - set2Pos++ - default: - res = append(res, set1[set1Pos]) - set1Pos++ - set2Pos++ - } - } - - return res -} - -func bitsToString(input bitfield.Bitlist) string { - bits := int(input.Len()) - - res := "" - for i := 0; i < bits; i++ { - if input.BitAt(uint64(i)) { - res = fmt.Sprintf("%s✓", res) - } else { - res = fmt.Sprintf("%s✕", res) - } - if i%8 == 7 { - res = fmt.Sprintf("%s ", res) - } - } - return strings.TrimSpace(res) -} - -func attestingIndices(input bitfield.Bitlist, indices []uint64) string { - bits := int(input.Len()) - res := "" - for i := 0; i < bits; i++ { - if input.BitAt(uint64(i)) { - res = fmt.Sprintf("%s%d ", res, indices[i]) - } - } - return strings.TrimSpace(res) -} - func init() { blockCmd.AddCommand(blockInfoCmd) blockFlags(blockInfoCmd) - blockInfoCmd.Flags().Int64Var(&blockInfoSlot, "slot", -1, "the latest slot with a block") - blockInfoCmd.Flags().BoolVar(&blockInfoStream, "stream", false, "continually stream blocks as they arrive") + blockInfoCmd.Flags().String("blockid", "head", "the ID of the block to fetch") + blockInfoCmd.Flags().Bool("stream", false, "continually stream blocks as they arrive") + blockInfoCmd.Flags().Bool("json", false, "output data in JSON format") +} + +func blockInfoBindings() { + if err := viper.BindPFlag("blockid", blockInfoCmd.Flags().Lookup("blockid")); err != nil { + panic(err) + } + if err := viper.BindPFlag("stream", blockInfoCmd.Flags().Lookup("stream")); err != nil { + panic(err) + } + if err := viper.BindPFlag("json", blockInfoCmd.Flags().Lookup("json")); err != nil { + panic(err) + } } diff --git a/cmd/chaininfo.go b/cmd/chaininfo.go index c282479..417add9 100644 --- a/cmd/chaininfo.go +++ b/cmd/chaininfo.go @@ -14,12 +14,16 @@ package cmd import ( + "context" "fmt" "os" "time" + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/spf13/cobra" - "github.com/wealdtech/ethdo/grpc" + "github.com/spf13/viper" + "github.com/wealdtech/ethdo/util" ) var chainInfoCmd = &cobra.Command{ @@ -31,31 +35,31 @@ var chainInfoCmd = &cobra.Command{ In quiet mode this will return 0 if the chain information can be obtained, otherwise 1.`, Run: func(cmd *cobra.Command, args []string) { - err := connect() - errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node") - config, err := grpc.FetchChainConfig(eth2GRPCConn) - errCheck(err, "Failed to obtain beacon chain configuration") + ctx := context.Background() - genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn) - errCheck(err, "Failed to obtain genesis time") + eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + errCheck(err, "Failed to connect to Ethereum 2 beacon node") - genesisValidatorsRoot, err := grpc.FetchGenesisValidatorsRoot(eth2GRPCConn) - errCheck(err, "Failed to obtain genesis validators root") + config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx) + errCheck(err, "Failed to obtain beacon chain specification") + + genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx) + errCheck(err, "Failed to obtain beacon chain genesis") if quiet { os.Exit(_exitSuccess) } - if genesisTime.Unix() == 0 { + if genesis.GenesisTime.Unix() == 0 { fmt.Println("Genesis time: undefined") } else { - fmt.Printf("Genesis time: %s\n", genesisTime.Format(time.UnixDate)) - outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesisTime.Unix())) + fmt.Printf("Genesis time: %s\n", genesis.GenesisTime.Format(time.UnixDate)) + outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesis.GenesisTime.Unix())) } - fmt.Printf("Genesis validators root: %#x\n", genesisValidatorsRoot) - fmt.Printf("Genesis fork version: %#x\n", config["GenesisForkVersion"].([]byte)) - fmt.Printf("Seconds per slot: %d\n", config["SecondsPerSlot"].(uint64)) - fmt.Printf("Slots per epoch: %d\n", config["SlotsPerEpoch"].(uint64)) + fmt.Printf("Genesis validators root: %#x\n", genesis.GenesisValidatorsRoot) + fmt.Printf("Genesis fork version: %#x\n", config["GENESIS_FORK_VERSION"].([]byte)) + fmt.Printf("Seconds per slot: %d\n", int(config["SECONDS_PER_SLOT"].(time.Duration).Seconds())) + fmt.Printf("Slots per epoch: %d\n", config["SLOTS_PER_EPOCH"].(uint64)) os.Exit(_exitSuccess) }, @@ -66,17 +70,17 @@ func init() { chainFlags(chainInfoCmd) } -func timestampToSlot(genesis int64, timestamp int64, secondsPerSlot uint64) uint64 { - if timestamp < genesis { +func timestampToSlot(genesis time.Time, timestamp time.Time, secondsPerSlot time.Duration) spec.Slot { + if timestamp.Unix() < genesis.Unix() { return 0 } - return uint64(timestamp-genesis) / secondsPerSlot + return spec.Slot(uint64(timestamp.Unix()-genesis.Unix()) / uint64(secondsPerSlot.Seconds())) } -func slotToTimestamp(genesis int64, slot uint64, secondsPerSlot uint64) int64 { - return genesis + int64(slot*secondsPerSlot) +func slotToTimestamp(genesis time.Time, slot spec.Slot, slotDuration time.Duration) int64 { + return genesis.Unix() + int64(slot)*int64(slotDuration.Seconds()) } -func epochToTimestamp(genesis int64, slot uint64, secondsPerSlot uint64, slotsPerEpoch uint64) int64 { - return genesis + int64(slot*secondsPerSlot*slotsPerEpoch) +func epochToTimestamp(genesis time.Time, slot spec.Slot, slotDuration time.Duration, slotsPerEpoch uint64) int64 { + return genesis.Unix() + int64(slot)*int64(slotDuration.Seconds())*int64(slotsPerEpoch) } diff --git a/cmd/chainstatus.go b/cmd/chainstatus.go index 607491c..d35461d 100644 --- a/cmd/chainstatus.go +++ b/cmd/chainstatus.go @@ -14,16 +14,18 @@ package cmd import ( + "context" "fmt" "os" "time" + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/spf13/cobra" - "github.com/wealdtech/ethdo/grpc" + "github.com/spf13/viper" + "github.com/wealdtech/ethdo/util" ) -var chainStatusSlot bool - var chainStatusCmd = &cobra.Command{ Use: "status", Short: "Obtain status about a chain", @@ -33,70 +35,48 @@ var chainStatusCmd = &cobra.Command{ In quiet mode this will return 0 if the chain status can be obtained, otherwise 1.`, Run: func(cmd *cobra.Command, args []string) { - err := connect() - errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node") - config, err := grpc.FetchChainConfig(eth2GRPCConn) - errCheck(err, "Failed to obtain beacon chain configuration") + ctx := context.Background() - genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn) - errCheck(err, "Failed to obtain genesis time") + eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + errCheck(err, "Failed to connect to Ethereum 2 beacon node") - info, err := grpc.FetchChainInfo(eth2GRPCConn) - errCheck(err, "Failed to obtain chain info") + config, err := eth2Client.(eth2client.SpecProvider).Spec(ctx) + errCheck(err, "Failed to obtain beacon chain specification") - if quiet { - os.Exit(_exitSuccess) + finality, err := eth2Client.(eth2client.FinalityProvider).Finality(ctx, "head") + errCheck(err, "Failed to obtain finality information") + + genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx) + errCheck(err, "Failed to obtain genesis information") + + slotDuration := config["SECONDS_PER_SLOT"].(time.Duration) + curSlot := timestampToSlot(genesis.GenesisTime, time.Now(), slotDuration) + slotsPerEpoch := config["SLOTS_PER_EPOCH"].(uint64) + curEpoch := spec.Epoch(uint64(curSlot) / slotsPerEpoch) + fmt.Printf("Current epoch: %d\n", curEpoch) + fmt.Printf("Justified epoch: %d\n", finality.Justified.Epoch) + if verbose { + distance := curEpoch - finality.Justified.Epoch + fmt.Printf("Justified epoch distance: %d\n", distance) } - - now := time.Now() - slot := timestampToSlot(genesisTime.Unix(), now.Unix(), config["SecondsPerSlot"].(uint64)) - if chainStatusSlot { - fmt.Printf("Current slot: %d\n", slot) - fmt.Printf("Justified slot: %d\n", info.GetJustifiedSlot()) - if verbose { - distance := slot - info.GetJustifiedSlot() - fmt.Printf("Justified slot distance: %d\n", distance) - } - fmt.Printf("Finalized slot: %d\n", info.GetFinalizedSlot()) - if verbose { - distance := slot - info.GetFinalizedSlot() - fmt.Printf("Finalized slot distance: %d\n", distance) - } - if verbose { - fmt.Printf("Prior justified slot: %d\n", info.GetFinalizedSlot()) - distance := slot - info.GetPreviousJustifiedSlot() - fmt.Printf("Prior justified slot distance: %d\n", distance) - } - } else { - slotsPerEpoch := config["SlotsPerEpoch"].(uint64) - epoch := slot / slotsPerEpoch - fmt.Printf("Current epoch: %d\n", epoch) - fmt.Printf("Justified epoch: %d\n", info.GetJustifiedEpoch()) - if verbose { - distance := (slot - info.GetJustifiedSlot()) / slotsPerEpoch - fmt.Printf("Justified epoch distance: %d\n", distance) - } - fmt.Printf("Finalized epoch: %d\n", info.GetFinalizedEpoch()) - if verbose { - distance := (slot - info.GetFinalizedSlot()) / slotsPerEpoch - fmt.Printf("Finalized epoch distance: %d\n", distance) - } - if verbose { - fmt.Printf("Prior justified epoch: %d\n", info.GetPreviousJustifiedEpoch()) - distance := (slot - info.GetPreviousJustifiedSlot()) / slotsPerEpoch - fmt.Printf("Prior justified epoch distance: %d\n", distance) - } + fmt.Printf("Finalized epoch: %d\n", finality.Finalized.Epoch) + if verbose { + distance := curEpoch - finality.Finalized.Epoch + fmt.Printf("Finalized epoch distance: %d\n", distance) + } + if verbose { + fmt.Printf("Prior justified epoch: %d\n", finality.PreviousJustified.Epoch) + distance := curEpoch - finality.PreviousJustified.Epoch + fmt.Printf("Prior justified epoch distance: %d\n", distance) } if verbose { - slotsPerEpoch := config["SlotsPerEpoch"].(uint64) - secondsPerSlot := config["SecondsPerSlot"].(uint64) - epochStartSlot := (slot / slotsPerEpoch) * slotsPerEpoch + epochStartSlot := (uint64(curSlot) / slotsPerEpoch) * slotsPerEpoch fmt.Printf("Epoch slots: %d-%d\n", epochStartSlot, epochStartSlot+slotsPerEpoch-1) - nextSlot := slotToTimestamp(genesisTime.Unix(), slot+1, secondsPerSlot) - fmt.Printf("Time until next slot: %2.1fs\n", float64(time.Until(time.Unix(nextSlot, 0)).Milliseconds())/1000) - nextEpoch := epochToTimestamp(genesisTime.Unix(), slot/slotsPerEpoch+1, secondsPerSlot, slotsPerEpoch) - fmt.Printf("Slots until next epoch: %d\n", (slot/slotsPerEpoch+1)*slotsPerEpoch-slot) + nextSlotTimestamp := slotToTimestamp(genesis.GenesisTime, curSlot+1, slotDuration) + fmt.Printf("Time until next slot: %2.1fs\n", float64(time.Until(time.Unix(nextSlotTimestamp, 0)).Milliseconds())/1000) + nextEpoch := epochToTimestamp(genesis.GenesisTime, spec.Slot(uint64(curSlot)/slotsPerEpoch+1), slotDuration, slotsPerEpoch) + fmt.Printf("Slots until next epoch: %d\n", (uint64(curSlot)/slotsPerEpoch+1)*slotsPerEpoch-uint64(curSlot)) fmt.Printf("Time until next epoch: %2.1fs\n", float64(time.Until(time.Unix(nextEpoch, 0)).Milliseconds())/1000) } @@ -107,6 +87,4 @@ In quiet mode this will return 0 if the chain status can be obtained, otherwise func init() { chainCmd.AddCommand(chainStatusCmd) chainFlags(chainStatusCmd) - chainStatusCmd.Flags().BoolVar(&chainStatusSlot, "slot", false, "Print slot-based values") - } diff --git a/cmd/exitverify.go b/cmd/exitverify.go index a49c2db..d5251ab 100644 --- a/cmd/exitverify.go +++ b/cmd/exitverify.go @@ -14,6 +14,7 @@ package cmd import ( + "bytes" "context" "encoding/hex" "encoding/json" @@ -22,11 +23,11 @@ import ( "os" "strings" + eth2client "github.com/attestantio/go-eth2-client" + spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" - ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/wealdtech/ethdo/grpc" "github.com/wealdtech/ethdo/util" e2types "github.com/wealdtech/go-eth2-types/v2" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" @@ -43,8 +44,7 @@ var exitVerifyCmd = &cobra.Command{ In quiet mode this will return 0 if the the exit is verified correctly, otherwise 1.`, Run: func(cmd *cobra.Command, args []string) { - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() + ctx := context.Background() assert(viper.GetString("account") != "" || exitVerifyPubKey != "", "account or public key is required") account, err := exitVerifyAccount(ctx) @@ -55,23 +55,26 @@ In quiet mode this will return 0 if the the exit is verified correctly, otherwis errCheck(err, "Failed to obtain exit data") // Confirm signature is good. - err = connect() - errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node") - genesisValidatorsRoot, err := grpc.FetchGenesisValidatorsRoot(eth2GRPCConn) - outputIf(debug, fmt.Sprintf("Genesis validators root is %#x", genesisValidatorsRoot)) - errCheck(err, "Failed to obtain genesis validators root") - domain := e2types.Domain(e2types.DomainVoluntaryExit, data.ForkVersion, genesisValidatorsRoot) - exit := ðpb.VoluntaryExit{ - Epoch: data.Epoch, - ValidatorIndex: data.ValidatorIndex, + eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + errCheck(err, "Failed to connect to Ethereum 2 beacon node") + + genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx) + errCheck(err, "Failed to obtain beacon chain genesis") + + domain := e2types.Domain(e2types.DomainVoluntaryExit, data.ForkVersion[:], genesis.GenesisValidatorsRoot[:]) + exit := &spec.VoluntaryExit{ + Epoch: data.Data.Message.Epoch, + ValidatorIndex: data.Data.Message.ValidatorIndex, } - sig, err := e2types.BLSSignatureFromBytes(data.Signature) + sig, err := e2types.BLSSignatureFromBytes(data.Data.Signature[:]) errCheck(err, "Invalid signature") verified, err := verifyStruct(account, exit, domain, sig) errCheck(err, "Failed to verify voluntary exit") assert(verified, "Voluntary exit failed to verify") - // TODO confirm fork version is valid (once we have a way of obtaining the current fork version). + 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") outputIf(verbose, "Verified") os.Exit(_exitSuccess) @@ -79,7 +82,7 @@ In quiet mode this will return 0 if the the exit is verified correctly, otherwis } // obtainExitData obtains exit data from an input, could be JSON itself or a path to JSON. -func obtainExitData(input string) (*validatorExitData, error) { +func obtainExitData(input string) (*util.ValidatorExitData, error) { var err error var data []byte // Input could be JSON or a path to JSON @@ -93,7 +96,7 @@ func obtainExitData(input string) (*validatorExitData, error) { return nil, errors.Wrap(err, "failed to find deposit data file") } } - exitData := &validatorExitData{} + exitData := &util.ValidatorExitData{} err = json.Unmarshal(data, exitData) if err != nil { return nil, errors.Wrap(err, "data is not valid JSON") diff --git a/cmd/networks.go b/cmd/networks.go deleted file mode 100644 index 9de34d3..0000000 --- a/cmd/networks.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright © 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 cmd - -import ( - "fmt" - - "github.com/wealdtech/ethdo/grpc" -) - -// networks is a map of deposit contract addresses to networks. -var networks = map[string]string{ - "16e82d77882a663454ef92806b7deca1d394810f": "Altona", - "0f0f0fc0530007361933eab5db97d09acdd6c1c8": "Onyx", - "07b39f4fde4a38bace212b546dac87c58dfe3fdc": "Medalla", -} - -// network returns the name of the network, if known. -func network() string { - if err := connect(); err != nil { - return "Unknown" - } - - depositContractAddress, err := grpc.FetchDepositContractAddress(eth2GRPCConn) - if err != nil { - return "Unknown" - } - outputIf(debug, fmt.Sprintf("Deposit contract is %x", depositContractAddress)) - - depositContract := fmt.Sprintf("%x", depositContractAddress) - if network, exists := networks[depositContract]; exists { - return network - } - return "Unknown" -} diff --git a/cmd/nodeinfo.go b/cmd/nodeinfo.go index 3659691..7fdef9a 100644 --- a/cmd/nodeinfo.go +++ b/cmd/nodeinfo.go @@ -14,12 +14,14 @@ package cmd import ( + "context" "fmt" "os" - "time" + eth2client "github.com/attestantio/go-eth2-client" "github.com/spf13/cobra" - "github.com/wealdtech/ethdo/grpc" + "github.com/spf13/viper" + "github.com/wealdtech/ethdo/util" ) var nodeInfoCmd = &cobra.Command{ @@ -31,38 +33,24 @@ var nodeInfoCmd = &cobra.Command{ In quiet mode this will return 0 if the node information can be obtained, otherwise 1.`, Run: func(cmd *cobra.Command, args []string) { - err := connect() - errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node") - config, err := grpc.FetchChainConfig(eth2GRPCConn) - errCheck(err, "Failed to obtain beacon chain configuration") + ctx := context.Background() - genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn) - errCheck(err, "Failed to obtain genesis time") + eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + errCheck(err, "Failed to connect to Ethereum 2 beacon node") if quiet { os.Exit(_exitSuccess) } if verbose { - version, metadata, err := grpc.FetchVersion(eth2GRPCConn) - errCheck(err, "Failed to obtain version") + version, err := eth2Client.(eth2client.NodeVersionProvider).NodeVersion(ctx) + errCheck(err, "Failed to obtain node version") fmt.Printf("Version: %s\n", version) - if metadata != "" { - fmt.Printf("Metadata: %s\n", metadata) - } } - syncing, err := grpc.FetchSyncing(eth2GRPCConn) - errCheck(err, "Failed to obtain syncing state") - fmt.Printf("Syncing: %v\n", syncing) - if genesisTime.Unix() == 0 { - fmt.Println("Not reached genesis") - } else { - slot := timestampToSlot(genesisTime.Unix(), time.Now().Unix(), config["SecondsPerSlot"].(uint64)) - fmt.Printf("Current slot: %d\n", slot) - fmt.Printf("Current epoch: %d\n", slot/config["SlotsPerEpoch"].(uint64)) - outputIf(verbose, fmt.Sprintf("Genesis timestamp: %v", genesisTime.Unix())) - } + syncState, err := eth2Client.(eth2client.NodeSyncingProvider).NodeSyncing(ctx) + errCheck(err, "failed to obtain node sync state") + fmt.Printf("Syncing: %t\n", syncState.SyncDistance != 0) os.Exit(_exitSuccess) }, diff --git a/cmd/root.go b/cmd/root.go index bbb8da3..2c96826 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -31,7 +31,6 @@ import ( filesystem "github.com/wealdtech/go-eth2-wallet-store-filesystem" s3 "github.com/wealdtech/go-eth2-wallet-store-s3" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" - "google.golang.org/grpc" ) var cfgFile string @@ -45,9 +44,6 @@ var rootStore string // Store for wallet actions. var store e2wtypes.Store -// Prysm connection. -var eth2GRPCConn *grpc.ClientConn - // RootCmd represents the base command when called without any subcommands var RootCmd = &cobra.Command{ Use: "ethdo", @@ -80,10 +76,16 @@ func persistentPreRun(cmd *cobra.Command, args []string) { accountImportBindings() case "attester/inclusion": attesterInclusionBindings() + case "block/info": + blockInfoBindings() case "exit/verify": exitVerifyBindings() case "validator/depositdata": validatorDepositdataBindings() + case "validator/exit": + validatorExitBindings() + case "validator/info": + validatorInfoBindings() case "wallet/create": walletCreateBindings() case "wallet/import": @@ -182,7 +184,7 @@ func init() { if err := viper.BindPFlag("debug", RootCmd.PersistentFlags().Lookup("debug")); err != nil { panic(err) } - RootCmd.PersistentFlags().String("connection", "localhost:4000", "connection to Ethereum 2 node via GRPC") + RootCmd.PersistentFlags().String("connection", "localhost:4000", "connection to an Ethereum 2 node") if err := viper.BindPFlag("connection", RootCmd.PersistentFlags().Lookup("connection")); err != nil { panic(err) } @@ -210,6 +212,10 @@ func init() { if err := viper.BindPFlag("allow-weak-passphrases", RootCmd.PersistentFlags().Lookup("allow-weak-passphrases")); err != nil { panic(err) } + RootCmd.PersistentFlags().Bool("allow-insecure-connections", false, "allow insecure connections to remote beacon nodes") + if err := viper.BindPFlag("allow-insecure-connections", RootCmd.PersistentFlags().Lookup("allow-insecure-connections")); err != nil { + panic(err) + } } // initConfig reads in config file and ENV variables if set. @@ -331,32 +337,6 @@ func walletAndAccountFromPath(ctx context.Context, path string) (e2wtypes.Wallet return wallet, account, nil } -// connect connects to an Ethereum 2 endpoint. -func connect() error { - if eth2GRPCConn != nil { - // Already connected. - return nil - } - - connection := "" - if viper.GetString("connection") != "" { - connection = viper.GetString("connection") - } - - if connection == "" { - return errors.New("no connection") - } - outputIf(debug, fmt.Sprintf("Connecting to %s", connection)) - - opts := []grpc.DialOption{grpc.WithInsecure()} - - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - var err error - eth2GRPCConn, err = grpc.DialContext(ctx, connection, opts...) - return err -} - // bestPublicKey returns the best public key for operations. // It prefers the composite public key if present, otherwise the public key. func bestPublicKey(account e2wtypes.Account) (e2types.PublicKey, error) { diff --git a/cmd/signing.go b/cmd/signing.go index 169d140..2ae9c98 100644 --- a/cmd/signing.go +++ b/cmd/signing.go @@ -24,17 +24,6 @@ import ( e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) -// signStruct signs an arbitrary structure. -func signStruct(account e2wtypes.Account, data interface{}, domain []byte) (e2types.Signature, error) { - objRoot, err := ssz.HashTreeRoot(data) - outputIf(debug, fmt.Sprintf("Object root is %#x", objRoot)) - if err != nil { - return nil, err - } - - return signRoot(account, objRoot, domain) -} - // verifyStruct verifies the signature of an arbitrary structure. func verifyStruct(account e2wtypes.Account, data interface{}, domain []byte, signature e2types.Signature) (bool, error) { objRoot, err := ssz.HashTreeRoot(data) diff --git a/cmd/validator/depositdata/input.go b/cmd/validator/depositdata/input.go index aceeb59..7a8f70e 100644 --- a/cmd/validator/depositdata/input.go +++ b/cmd/validator/depositdata/input.go @@ -18,10 +18,12 @@ import ( "encoding/hex" "strings" + 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/core" - "github.com/wealdtech/ethdo/grpc" + ethdoutil "github.com/wealdtech/ethdo/util" e2types "github.com/wealdtech/go-eth2-types/v2" util "github.com/wealdtech/go-eth2-util" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" @@ -31,15 +33,19 @@ import ( type dataIn struct { format string withdrawalCredentials []byte - amount uint64 + amount spec.Gwei validatorAccounts []e2wtypes.Account - forkVersion []byte - domain []byte + forkVersion *spec.Version + domain *spec.Domain + passphrases []string } func input() (*dataIn, error) { var err error - data := &dataIn{} + data := &dataIn{ + forkVersion: &spec.Version{}, + domain: &spec.Domain{}, + } if viper.GetString("validatoraccount") == "" { return nil, errors.New("validator account is required") @@ -64,6 +70,8 @@ func input() (*dataIn, error) { data.format = "json" } + data.passphrases = ethdoutil.GetPassphrases() + switch { case viper.GetString("withdrawalaccount") != "": ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) @@ -99,39 +107,49 @@ func input() (*dataIn, error) { if viper.GetString("depositvalue") == "" { return nil, errors.New("deposit value is required") } - data.amount, err = string2eth.StringToGWei(viper.GetString("depositvalue")) + amount, err := string2eth.StringToGWei(viper.GetString("depositvalue")) if err != nil { return nil, errors.Wrap(err, "deposit value is invalid") } + data.amount = spec.Gwei(amount) // This is hard-coded, to allow deposit data to be generated without a connection to the beacon node. if data.amount < 1000000000 { // MIN_DEPOSIT_AMOUNT return nil, errors.New("deposit value must be at least 1 Ether") } - if viper.GetString("forkversion") != "" { - data.forkVersion, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("forkversion"), "0x")) - if err != nil { - return nil, errors.Wrap(err, "failed to decode fork version") - } - if len(data.forkVersion) != 4 { - return nil, errors.New("fork version must be exactly 4 bytes in length") - } - } else { - conn, err := grpc.Connect() - if err != nil { - return nil, errors.Wrap(err, "failed to connect to beacon node") - } - config, err := grpc.FetchChainConfig(conn) - if err != nil { - return nil, errors.Wrap(err, "could not connect to beacon node; supply a connection with --connection or provide a fork version with --forkversion to generate deposit data") - } - genesisForkVersion, exists := config["GenesisForkVersion"] - if !exists { - return nil, errors.New("failed to obtain genesis fork version") - } - data.forkVersion = genesisForkVersion.([]byte) + data.forkVersion, err = inputForkVersion(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain fork version") } - data.domain = e2types.Domain(e2types.DomainDeposit, data.forkVersion, e2types.ZeroGenesisValidatorsRoot) + + copy(data.domain[:], e2types.Domain(e2types.DomainDeposit, data.forkVersion[:], e2types.ZeroGenesisValidatorsRoot)) return data, nil } + +func inputForkVersion(ctx context.Context) (*spec.Version, error) { + if viper.GetString("forkversion") != "" { + forkVersion, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("forkversion"), "0x")) + if err != nil { + return nil, errors.Wrap(err, "failed to decode fork version") + } + if len(forkVersion) != 4 { + return nil, errors.New("fork version must be exactly 4 bytes in length") + } + + res := &spec.Version{} + copy(res[:], forkVersion) + return res, nil + } + + eth2Client, err := ethdoutil.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") + } + + genesis, err := eth2Client.(eth2client.GenesisProvider).Genesis(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain genesis") + } + return &genesis.GenesisForkVersion, nil +} diff --git a/cmd/validator/depositdata/input_internal_test.go b/cmd/validator/depositdata/input_internal_test.go index 9e0ee6d..0d6970a 100644 --- a/cmd/validator/depositdata/input_internal_test.go +++ b/cmd/validator/depositdata/input_internal_test.go @@ -17,8 +17,10 @@ import ( "context" "testing" + spec "github.com/attestantio/go-eth2-client/spec/phase0" "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" @@ -39,17 +41,28 @@ func TestInput(t *testing.T) { viper.Set("passphrase", "pass") interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(), "Interop 0", - hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"), + testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"), []byte("pass"), ) require.NoError(t, err) _, err = testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(), "Interop 1", - hexToBytes("0x51d0b65185db6989ab0b560d6deed19c7ead0e24b9b6372cbecb1f26bdfad000"), + testutil.HexToBytes("0x51d0b65185db6989ab0b560d6deed19c7ead0e24b9b6372cbecb1f26bdfad000"), []byte("pass"), ) require.NoError(t, err) + var forkVersion *spec.Version + { + tmp := testutil.HexToVersion("0x01020304") + forkVersion = &tmp + } + var domain *spec.Domain + { + tmp := testutil.HexToDomain("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0") + domain = &tmp + } + tests := []struct { name string vars map[string]interface{} @@ -167,17 +180,7 @@ func TestInput(t *testing.T) { "depositvalue": "32 Ether", "forkversion": "invalid", }, - err: "failed to decode fork version: encoding/hex: invalid byte: U+0069 'i'", - }, - { - name: "ForkVersionWrongLength", - vars: map[string]interface{}{ - "validatoraccount": "Test/Interop 0", - "withdrawalaccount": "Test/Interop 0", - "depositvalue": "32 Ether", - "forkversion": "0x0102030405", - }, - err: "fork version must be exactly 4 bytes in length", + err: "failed to obtain fork version: failed to decode fork version: encoding/hex: invalid byte: U+0069 'i'", }, { name: "Good", @@ -189,11 +192,11 @@ func TestInput(t *testing.T) { }, res: &dataIn{ format: "json", - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, validatorAccounts: []e2wtypes.Account{interop0}, - forkVersion: hexToBytes("0x01020304"), - domain: hexToBytes("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0"), + forkVersion: forkVersion, + domain: domain, }, }, { @@ -206,11 +209,11 @@ func TestInput(t *testing.T) { }, res: &dataIn{ format: "json", - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, validatorAccounts: []e2wtypes.Account{interop0}, - forkVersion: hexToBytes("0x01020304"), - domain: hexToBytes("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0"), + forkVersion: forkVersion, + domain: domain, }, }, } diff --git a/cmd/validator/depositdata/output.go b/cmd/validator/depositdata/output.go index 56118b8..d4d27e5 100644 --- a/cmd/validator/depositdata/output.go +++ b/cmd/validator/depositdata/output.go @@ -17,19 +17,20 @@ import ( "fmt" "strings" + spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" ) type dataOut struct { format string account string - validatorPubKey []byte + validatorPubKey *spec.BLSPubKey withdrawalCredentials []byte - amount uint64 - signature []byte - forkVersion []byte - depositDataRoot []byte - depositMessageRoot []byte + amount spec.Gwei + signature *spec.BLSSignature + forkVersion *spec.Version + depositDataRoot *spec.Root + depositMessageRoot *spec.Root } func output(data []*dataOut) (string, error) { @@ -57,8 +58,8 @@ func output(data []*dataOut) (string, error) { } func validatorDepositDataOutputRaw(datum *dataOut) (string, error) { - if len(datum.validatorPubKey) != 48 { - return "", errors.New("validator public key must be 48 bytes") + if datum.validatorPubKey == nil { + return "", errors.New("validator public key required") } if len(datum.withdrawalCredentials) != 32 { return "", errors.New("withdrawal credentials must be 32 bytes") @@ -66,14 +67,11 @@ func validatorDepositDataOutputRaw(datum *dataOut) (string, error) { if datum.amount == 0 { return "", errors.New("missing amount") } - if len(datum.signature) != 96 { - return "", errors.New("signature must be 96 bytes") + if datum.signature == nil { + return "", errors.New("signature required") } - if len(datum.depositMessageRoot) != 32 { - return "", errors.New("deposit message root must be 32 bytes") - } - if len(datum.depositDataRoot) != 32 { - return "", errors.New("deposit data root must be 32 bytes") + if datum.depositDataRoot == nil { + return "", errors.New("deposit data root required") } output := fmt.Sprintf( @@ -98,17 +96,17 @@ func validatorDepositDataOutputRaw(datum *dataOut) (string, error) { "0000000000000000000000000000000000000000000000000000000000000060"+ "%x"+ `"`, - datum.depositDataRoot, - datum.validatorPubKey, + *datum.depositDataRoot, + *datum.validatorPubKey, datum.withdrawalCredentials, - datum.signature, + *datum.signature, ) return output, nil } func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) { - if len(datum.validatorPubKey) != 48 { - return "", errors.New("validator public key must be 48 bytes") + if datum.validatorPubKey == nil { + return "", errors.New("validator public key required") } if len(datum.withdrawalCredentials) != 32 { return "", errors.New("withdrawal credentials must be 32 bytes") @@ -116,24 +114,24 @@ func validatorDepositDataOutputLaunchpad(datum *dataOut) (string, error) { if datum.amount == 0 { return "", errors.New("missing amount") } - if len(datum.signature) != 96 { - return "", errors.New("signature must be 96 bytes") + if datum.signature == nil { + return "", errors.New("signature required") } - if len(datum.depositMessageRoot) != 32 { - return "", errors.New("deposit message root must be 32 bytes") + if datum.depositMessageRoot == nil { + return "", errors.New("deposit message root required") } - if len(datum.depositDataRoot) != 32 { - return "", errors.New("deposit data root must be 32 bytes") + if datum.depositDataRoot == nil { + return "", errors.New("deposit data root required") } output := fmt.Sprintf(`{"pubkey":"%x","withdrawal_credentials":"%x","amount":%d,"signature":"%x","deposit_message_root":"%x","deposit_data_root":"%x","fork_version":"%x"}`, - datum.validatorPubKey, + *datum.validatorPubKey, datum.withdrawalCredentials, datum.amount, - datum.signature, - datum.depositMessageRoot, - datum.depositDataRoot, - datum.forkVersion, + *datum.signature, + *datum.depositMessageRoot, + *datum.depositDataRoot, + *datum.forkVersion, ) return output, nil } @@ -142,30 +140,30 @@ func validatorDepositDataOutputJSON(datum *dataOut) (string, error) { if datum.account == "" { return "", errors.New("missing account") } - if len(datum.validatorPubKey) != 48 { - return "", errors.New("validator public key must be 48 bytes") + if datum.validatorPubKey == nil { + return "", errors.New("validator public key required") } if len(datum.withdrawalCredentials) != 32 { return "", errors.New("withdrawal credentials must be 32 bytes") } - if len(datum.signature) != 96 { - return "", errors.New("signature must be 96 bytes") + if datum.signature == nil { + return "", errors.New("signature required") } if datum.amount == 0 { return "", errors.New("missing amount") } - if len(datum.depositDataRoot) != 32 { - return "", errors.New("deposit data root must be 32 bytes") + if datum.depositDataRoot == nil { + return "", errors.New("deposit data root required") } output := fmt.Sprintf(`{"name":"Deposit for %s","account":"%s","pubkey":"%#x","withdrawal_credentials":"%#x","signature":"%#x","value":%d,"deposit_data_root":"%#x","version":2}`, datum.account, datum.account, - datum.validatorPubKey, + *datum.validatorPubKey, datum.withdrawalCredentials, - datum.signature, + *datum.signature, datum.amount, - datum.depositDataRoot, + *datum.depositDataRoot, ) return output, nil } diff --git a/cmd/validator/depositdata/output_internal_test.go b/cmd/validator/depositdata/output_internal_test.go index 7fa2e98..5f26a54 100644 --- a/cmd/validator/depositdata/output_internal_test.go +++ b/cmd/validator/depositdata/output_internal_test.go @@ -14,22 +14,61 @@ package depositdata import ( - "encoding/hex" - "strings" "testing" + spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" + "github.com/wealdtech/ethdo/testutil" ) -func hexToBytes(input string) []byte { - res, err := hex.DecodeString(strings.TrimPrefix(input, "0x")) - if err != nil { - panic(err) - } - return res -} - func TestOutputJSON(t *testing.T) { + var validatorPubKey *spec.BLSPubKey + { + tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c") + validatorPubKey = &tmp + } + var signature *spec.BLSSignature + { + tmp := testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2") + signature = &tmp + } + var forkVersion *spec.Version + { + tmp := testutil.HexToVersion("0x01020304") + forkVersion = &tmp + } + var depositDataRoot *spec.Root + { + tmp := testutil.HexToRoot("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554") + depositDataRoot = &tmp + } + var depositMessageRoot *spec.Root + { + tmp := testutil.HexToRoot("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6") + depositMessageRoot = &tmp + } + + var validatorPubKey2 *spec.BLSPubKey + { + tmp := testutil.HexToPubKey("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b") + validatorPubKey2 = &tmp + } + var signature2 *spec.BLSSignature + { + tmp := testutil.HexToSignature("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e") + signature2 = &tmp + } + var depositDataRoot2 *spec.Root + { + tmp := testutil.HexToRoot("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab") + depositDataRoot2 = &tmp + } + var depositMessageRoot2 *spec.Root + { + tmp := testutil.HexToRoot("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52") + depositMessageRoot2 = &tmp + } + tests := []struct { name string dataOut []*dataOut @@ -52,13 +91,13 @@ func TestOutputJSON(t *testing.T) { dataOut: []*dataOut{ { format: "json", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, err: "missing account", @@ -69,15 +108,15 @@ func TestOutputJSON(t *testing.T) { { format: "json", account: "interop/00000", - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, - err: "validator public key must be 48 bytes", + err: "validator public key required", }, { name: "MissingWithdrawalCredentials", @@ -85,12 +124,12 @@ func TestOutputJSON(t *testing.T) { { format: "json", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), + validatorPubKey: validatorPubKey, amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, err: "withdrawal credentials must be 32 bytes", @@ -101,15 +140,15 @@ func TestOutputJSON(t *testing.T) { { format: "json", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, - err: "signature must be 96 bytes", + err: "signature required", }, { name: "AmountMissing", @@ -117,12 +156,12 @@ func TestOutputJSON(t *testing.T) { { format: "json", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, err: "missing amount", @@ -133,15 +172,15 @@ func TestOutputJSON(t *testing.T) { { format: "json", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositMessageRoot: depositMessageRoot, }, }, - err: "deposit data root must be 32 bytes", + err: "deposit data root required", }, { name: "Single", @@ -149,13 +188,13 @@ func TestOutputJSON(t *testing.T) { { format: "json", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, res: `[{"name":"Deposit for interop/00000","account":"interop/00000","pubkey":"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","signature":"0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","value":32000000000,"deposit_data_root":"0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","version":2}]`, @@ -166,24 +205,24 @@ func TestOutputJSON(t *testing.T) { { format: "json", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, { format: "json", account: "interop/00001", - validatorPubKey: hexToBytes("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b"), - withdrawalCredentials: hexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"), + validatorPubKey: validatorPubKey2, + withdrawalCredentials: testutil.HexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"), amount: 32000000000, - signature: hexToBytes("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab"), - depositMessageRoot: hexToBytes("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52"), + signature: signature2, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot2, + depositMessageRoot: depositMessageRoot2, }, }, res: `[{"name":"Deposit for interop/00000","account":"interop/00000","pubkey":"0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","signature":"0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","value":32000000000,"deposit_data_root":"0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","version":2},{"name":"Deposit for interop/00001","account":"interop/00001","pubkey":"0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","withdrawal_credentials":"0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594","signature":"0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e","value":32000000000,"deposit_data_root":"0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab","version":2}]`, @@ -204,6 +243,53 @@ func TestOutputJSON(t *testing.T) { } func TestOutputLaunchpad(t *testing.T) { + var validatorPubKey *spec.BLSPubKey + { + tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c") + validatorPubKey = &tmp + } + var signature *spec.BLSSignature + { + tmp := testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2") + signature = &tmp + } + var forkVersion *spec.Version + { + tmp := testutil.HexToVersion("0x01020304") + forkVersion = &tmp + } + var depositDataRoot *spec.Root + { + tmp := testutil.HexToRoot("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554") + depositDataRoot = &tmp + } + var depositMessageRoot *spec.Root + { + tmp := testutil.HexToRoot("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6") + depositMessageRoot = &tmp + } + + var validatorPubKey2 *spec.BLSPubKey + { + tmp := testutil.HexToPubKey("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b") + validatorPubKey2 = &tmp + } + var signature2 *spec.BLSSignature + { + tmp := testutil.HexToSignature("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e") + signature2 = &tmp + } + var depositDataRoot2 *spec.Root + { + tmp := testutil.HexToRoot("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab") + depositDataRoot2 = &tmp + } + var depositMessageRoot2 *spec.Root + { + tmp := testutil.HexToRoot("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52") + depositMessageRoot2 = &tmp + } + tests := []struct { name string dataOut []*dataOut @@ -227,15 +313,15 @@ func TestOutputLaunchpad(t *testing.T) { { format: "launchpad", account: "interop/00000", - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, - err: "validator public key must be 48 bytes", + err: "validator public key required", }, { name: "MissingWithdrawalCredentials", @@ -243,12 +329,12 @@ func TestOutputLaunchpad(t *testing.T) { { format: "launchpad", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), + validatorPubKey: validatorPubKey, amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, err: "withdrawal credentials must be 32 bytes", @@ -259,15 +345,15 @@ func TestOutputLaunchpad(t *testing.T) { { format: "launchpad", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, - err: "signature must be 96 bytes", + err: "signature required", }, { name: "AmountMissing", @@ -275,12 +361,12 @@ func TestOutputLaunchpad(t *testing.T) { { format: "launchpad", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, err: "missing amount", @@ -291,15 +377,15 @@ func TestOutputLaunchpad(t *testing.T) { { format: "launchpad", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositMessageRoot: depositMessageRoot, }, }, - err: "deposit data root must be 32 bytes", + err: "deposit data root required", }, { name: "DepositMessageRootMissing", @@ -307,15 +393,15 @@ func TestOutputLaunchpad(t *testing.T) { { format: "launchpad", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, }, }, - err: "deposit message root must be 32 bytes", + err: "deposit message root required", }, { name: "Single", @@ -323,13 +409,13 @@ func TestOutputLaunchpad(t *testing.T) { { format: "launchpad", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"01020304"}]`, @@ -340,24 +426,24 @@ func TestOutputLaunchpad(t *testing.T) { { format: "launchpad", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, { format: "launchpad", account: "interop/00001", - validatorPubKey: hexToBytes("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b"), - withdrawalCredentials: hexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"), + validatorPubKey: validatorPubKey2, + withdrawalCredentials: testutil.HexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"), amount: 32000000000, - signature: hexToBytes("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab"), - depositMessageRoot: hexToBytes("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52"), + signature: signature2, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot2, + depositMessageRoot: depositMessageRoot2, }, }, res: `[{"pubkey":"a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c","withdrawal_credentials":"00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b","amount":32000000000,"signature":"b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","deposit_message_root":"139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6","deposit_data_root":"9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554","fork_version":"01020304"},{"pubkey":"b89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b","withdrawal_credentials":"00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594","amount":32000000000,"signature":"911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e","deposit_message_root":"bb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52","deposit_data_root":"3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab","fork_version":"01020304"}]`, @@ -378,6 +464,53 @@ func TestOutputLaunchpad(t *testing.T) { } func TestOutputRaw(t *testing.T) { + var validatorPubKey *spec.BLSPubKey + { + tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c") + validatorPubKey = &tmp + } + var signature *spec.BLSSignature + { + tmp := testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2") + signature = &tmp + } + var forkVersion *spec.Version + { + tmp := testutil.HexToVersion("0x01020304") + forkVersion = &tmp + } + var depositDataRoot *spec.Root + { + tmp := testutil.HexToRoot("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554") + depositDataRoot = &tmp + } + var depositMessageRoot *spec.Root + { + tmp := testutil.HexToRoot("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6") + depositMessageRoot = &tmp + } + + var validatorPubKey2 *spec.BLSPubKey + { + tmp := testutil.HexToPubKey("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b") + validatorPubKey2 = &tmp + } + var signature2 *spec.BLSSignature + { + tmp := testutil.HexToSignature("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e") + signature2 = &tmp + } + var depositDataRoot2 *spec.Root + { + tmp := testutil.HexToRoot("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab") + depositDataRoot2 = &tmp + } + var depositMessageRoot2 *spec.Root + { + tmp := testutil.HexToRoot("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52") + depositMessageRoot2 = &tmp + } + tests := []struct { name string dataOut []*dataOut @@ -401,15 +534,15 @@ func TestOutputRaw(t *testing.T) { { format: "raw", account: "interop/00000", - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, - err: "validator public key must be 48 bytes", + err: "validator public key required", }, { name: "MissingWithdrawalCredentials", @@ -417,12 +550,12 @@ func TestOutputRaw(t *testing.T) { { format: "raw", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), + validatorPubKey: validatorPubKey, amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, err: "withdrawal credentials must be 32 bytes", @@ -433,15 +566,15 @@ func TestOutputRaw(t *testing.T) { { format: "raw", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, - err: "signature must be 96 bytes", + err: "signature required", }, { name: "AmountMissing", @@ -449,12 +582,12 @@ func TestOutputRaw(t *testing.T) { { format: "raw", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, err: "missing amount", @@ -465,31 +598,15 @@ func TestOutputRaw(t *testing.T) { { format: "raw", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositMessageRoot: depositMessageRoot, }, }, - err: "deposit data root must be 32 bytes", - }, - { - name: "DepositMessageRootMissing", - dataOut: []*dataOut{ - { - format: "raw", - account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - }, - }, - err: "deposit message root must be 32 bytes", + err: "deposit data root required", }, { name: "Single", @@ -497,13 +614,13 @@ func TestOutputRaw(t *testing.T) { { format: "raw", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, res: `["0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001209e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a35540000000000000000000000000000000000000000000000000000000000000030a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b0000000000000000000000000000000000000000000000000000000000000060b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"]`, @@ -514,24 +631,24 @@ func TestOutputRaw(t *testing.T) { { format: "raw", account: "interop/00000", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + validatorPubKey: validatorPubKey, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, { format: "raw", account: "interop/00001", - validatorPubKey: hexToBytes("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b"), - withdrawalCredentials: hexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"), + validatorPubKey: validatorPubKey2, + withdrawalCredentials: testutil.HexToBytes("0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594"), amount: 32000000000, - signature: hexToBytes("0x911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x3b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab"), - depositMessageRoot: hexToBytes("0xbb4b6184b25873cdf430df3838c8d3e3d16cf3dc3b214e2f3ab7df9e6d5a9b52"), + signature: signature2, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot2, + depositMessageRoot: depositMessageRoot2, }, }, res: `["0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001209e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a35540000000000000000000000000000000000000000000000000000000000000030a99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b0000000000000000000000000000000000000000000000000000000000000060b7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2","0x22895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001203b51670e9f266d44c879682a230d60f0d534c64ab25ee68700fe3adb17ddfcab0000000000000000000000000000000000000000000000000000000000000030b89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f35940000000000000000000000000000000000000000000000000000000000000060911fe0766e8b79d711dde46bc2142eb51e35be99e5f7da505af9eaad85707bbb8013f0dea35e30403b3e57bb13054c1d0d389aceeba1d4160a148026212c7e017044e3ea69cd96fbd23b6aa9fd1e6f7e82494fbd5f8fc75856711a6b8998926e"]`, diff --git a/cmd/validator/depositdata/process.go b/cmd/validator/depositdata/process.go index 13ab440..75bbb6d 100644 --- a/cmd/validator/depositdata/process.go +++ b/cmd/validator/depositdata/process.go @@ -14,6 +14,7 @@ package depositdata import ( + "context" "fmt" spec "github.com/attestantio/go-eth2-client/spec/phase0" @@ -41,43 +42,45 @@ func process(data *dataIn) ([]*dataOut, error) { depositMessage := &spec.DepositMessage{ PublicKey: pubKey, WithdrawalCredentials: data.withdrawalCredentials, - Amount: spec.Gwei(data.amount), + Amount: data.amount, } - depositMessageRoot, err := depositMessage.HashTreeRoot() + root, err := depositMessage.HashTreeRoot() if err != nil { return nil, errors.Wrap(err, "failed to generate deposit message root") } + var depositMessageRoot spec.Root + copy(depositMessageRoot[:], root[:]) - sig, err := signing.SignRoot(validatorAccount, depositMessageRoot[:], data.domain) + sig, err := signing.SignRoot(context.Background(), validatorAccount, data.passphrases, depositMessageRoot, *data.domain) if err != nil { return nil, errors.Wrap(err, "failed to sign deposit message") } - var signature spec.BLSSignature - copy(signature[:], sig) depositData := &spec.DepositData{ PublicKey: pubKey, WithdrawalCredentials: data.withdrawalCredentials, - Amount: spec.Gwei(data.amount), - Signature: signature, + Amount: data.amount, + Signature: sig, } - depositDataRoot, err := depositData.HashTreeRoot() + root, err = depositData.HashTreeRoot() if err != nil { return nil, errors.Wrap(err, "failed to generate deposit data root") } + var depositDataRoot spec.Root + copy(depositDataRoot[:], root[:]) validatorWallet := validatorAccount.(e2wtypes.AccountWalletProvider).Wallet() results = append(results, &dataOut{ format: data.format, account: fmt.Sprintf("%s/%s", validatorWallet.Name(), validatorAccount.Name()), - validatorPubKey: validatorPubKey.Marshal(), + validatorPubKey: &pubKey, withdrawalCredentials: data.withdrawalCredentials, amount: data.amount, - signature: sig, + signature: &sig, forkVersion: data.forkVersion, - depositMessageRoot: depositMessageRoot[:], - depositDataRoot: depositDataRoot[:], + depositMessageRoot: &depositMessageRoot, + depositDataRoot: &depositDataRoot, }) } return results, nil diff --git a/cmd/validator/depositdata/process_internal_test.go b/cmd/validator/depositdata/process_internal_test.go index 3f6e232..3503873 100644 --- a/cmd/validator/depositdata/process_internal_test.go +++ b/cmd/validator/depositdata/process_internal_test.go @@ -1,4 +1,4 @@ -// Copyright © 2019, 2020 Weald Technology Trading +// Copyright © 2019, 2020 eald 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,8 +17,10 @@ import ( "context" "testing" + spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/spf13/viper" "github.com/stretchr/testify/require" + "github.com/wealdtech/ethdo/testutil" e2types "github.com/wealdtech/go-eth2-types/v2" keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" nd "github.com/wealdtech/go-eth2-wallet-nd/v2" @@ -36,17 +38,69 @@ func TestProcess(t *testing.T) { viper.Set("passphrase", "pass") interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(), "Interop 0", - hexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"), + testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"), []byte("pass"), ) require.NoError(t, err) interop1, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(), "Interop 1", - hexToBytes("0x51d0b65185db6989ab0b560d6deed19c7ead0e24b9b6372cbecb1f26bdfad000"), + testutil.HexToBytes("0x51d0b65185db6989ab0b560d6deed19c7ead0e24b9b6372cbecb1f26bdfad000"), []byte("pass"), ) require.NoError(t, err) + var validatorPubKey *spec.BLSPubKey + { + tmp := testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c") + validatorPubKey = &tmp + } + var signature *spec.BLSSignature + { + tmp := testutil.HexToSignature("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2") + signature = &tmp + } + var forkVersion *spec.Version + { + tmp := testutil.HexToVersion("0x01020304") + forkVersion = &tmp + } + var domain *spec.Domain + { + tmp := testutil.HexToDomain("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0") + domain = &tmp + } + var depositDataRoot *spec.Root + { + tmp := testutil.HexToRoot("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554") + depositDataRoot = &tmp + } + var depositMessageRoot *spec.Root + { + tmp := testutil.HexToRoot("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6") + depositMessageRoot = &tmp + } + + var validatorPubKey2 *spec.BLSPubKey + { + tmp := testutil.HexToPubKey("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b") + validatorPubKey2 = &tmp + } + var signature2 *spec.BLSSignature + { + tmp := testutil.HexToSignature("0x939aedb76236c971c21227189c6a3a40d07909d19999798490294d284130a913b6f91d41d875768fb3e2ea4dcec672a316e5951272378f5df80a7c34fadb9a4d8462ee817faf50fe8b1c33e72d884fb17e71e665724f9e17bdf11f48eb6e9bfd") + signature2 = &tmp + } + var depositDataRoot2 *spec.Root + { + tmp := testutil.HexToRoot("0x182c7708aad7027bea2f6251eddf62431fae4876ee3e55339082219ae7014443") + depositDataRoot2 = &tmp + } + var depositMessageRoot2 *spec.Root + { + tmp := testutil.HexToRoot("0x1dc5053486d74f5c91fa90e1e86d718d3fb42bb92e5cfdce98e994eb2bff2c46") + depositMessageRoot2 = &tmp + } + tests := []struct { name string dataIn *dataIn @@ -61,23 +115,24 @@ func TestProcess(t *testing.T) { name: "Single", dataIn: &dataIn{ format: "raw", - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + passphrases: []string{"pass"}, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, validatorAccounts: []e2wtypes.Account{interop0}, - forkVersion: hexToBytes("0x01020304"), - domain: hexToBytes("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0"), + forkVersion: forkVersion, + domain: domain, }, res: []*dataOut{ { format: "raw", account: "Test/Interop 0", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), + validatorPubKey: validatorPubKey, amount: 32000000000, - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, }, }, @@ -85,34 +140,35 @@ func TestProcess(t *testing.T) { name: "Double", dataIn: &dataIn{ format: "raw", - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + passphrases: []string{"pass"}, + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), amount: 32000000000, validatorAccounts: []e2wtypes.Account{interop0, interop1}, - forkVersion: hexToBytes("0x01020304"), - domain: hexToBytes("0x03000000ffd2fc34e5796a643f749b0b2b908c4ca3ce58ce24a00c49329a2dc0"), + forkVersion: forkVersion, + domain: domain, }, res: []*dataOut{ { format: "raw", account: "Test/Interop 0", - validatorPubKey: hexToBytes("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), + validatorPubKey: validatorPubKey, amount: 32000000000, - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - signature: hexToBytes("0xb7a757a4c506ac6ac5f2d23e065de7d00dc9f5a6a3f9610a8b60b65f166379139ae382c91ecbbf5c9fabc34b1cd2cf8f0211488d50d8754716d8e72e17c1a00b5d9b37cc73767946790ebe66cf9669abfc5c25c67e1e2d1c2e11429d149c25a2"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x9e51b386f4271c18149dd0f73297a26a4a8c15c3622c44af79c92446f44a3554"), - depositMessageRoot: hexToBytes("0x139b510ea7f2788ab82da1f427d6cbe1db147c15a053db738ad5500cd83754a6"), + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + signature: signature, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot, + depositMessageRoot: depositMessageRoot, }, { format: "raw", account: "Test/Interop 1", - validatorPubKey: hexToBytes("0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b"), + validatorPubKey: validatorPubKey2, amount: 32000000000, - withdrawalCredentials: hexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), - signature: hexToBytes("0x939aedb76236c971c21227189c6a3a40d07909d19999798490294d284130a913b6f91d41d875768fb3e2ea4dcec672a316e5951272378f5df80a7c34fadb9a4d8462ee817faf50fe8b1c33e72d884fb17e71e665724f9e17bdf11f48eb6e9bfd"), - forkVersion: hexToBytes("0x01020304"), - depositDataRoot: hexToBytes("0x182c7708aad7027bea2f6251eddf62431fae4876ee3e55339082219ae7014443"), - depositMessageRoot: hexToBytes("0x1dc5053486d74f5c91fa90e1e86d718d3fb42bb92e5cfdce98e994eb2bff2c46"), + withdrawalCredentials: testutil.HexToBytes("0x00fad2a6bfb0e7f1f0f45460944fbd8dfa7f37da06a4d13b3983cc90bb46963b"), + signature: signature2, + forkVersion: forkVersion, + depositDataRoot: depositDataRoot2, + depositMessageRoot: depositMessageRoot2, }, }, }, diff --git a/cmd/validator/exit/input.go b/cmd/validator/exit/input.go new file mode 100644 index 0000000..9ea4c49 --- /dev/null +++ b/cmd/validator/exit/input.go @@ -0,0 +1,147 @@ +// 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/core" + "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.Data + return inputChainData(ctx, data) +} + +func inputAccount(ctx context.Context, data *dataIn) (*dataIn, error) { + var err error + _, data.account, err = core.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") + } + 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 +} diff --git a/cmd/validator/exit/input_internal_test.go b/cmd/validator/exit/input_internal_test.go new file mode 100644 index 0000000..151f21b --- /dev/null +++ b/cmd/validator/exit/input_internal_test.go @@ -0,0 +1,186 @@ +// 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"), + "timeout": "5s", + "key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866", + }, + res: &dataIn{ + timeout: 5 * time.Second, + }, + }, + { + name: "AccountUnknown", + vars: map[string]interface{}{ + "connection": os.Getenv("ETHDO_TEST_CONNECTION"), + "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"), + "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"), + "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"), + "timeout": "5s", + "exit": `{"message":{"epoch":"123","validator_index":"456"},"signature":"0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"}`, + }, + res: &dataIn{ + timeout: 5 * time.Second, + }, + }, + { + name: "ClientBad", + vars: map[string]interface{}{ + "connection": "localhost:1", + "timeout": "5s", + "key": "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866", + }, + err: "failed to connect to Ethereum 2 beacon node: failed to connect to beacon node: failed to connect to Ethereum 2 client with any known method", + }, + { + name: "EpochProvided", + vars: map[string]interface{}{ + "connection": os.Getenv("ETHDO_TEST_CONNECTION"), + "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) + } + }) + } +} diff --git a/cmd/validator/exit/output.go b/cmd/validator/exit/output.go new file mode 100644 index 0000000..309d7b8 --- /dev/null +++ b/cmd/validator/exit/output.go @@ -0,0 +1,57 @@ +// 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/json" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/wealdtech/ethdo/util" +) + +type dataOut struct { + jsonOutput bool + forkVersion spec.Version + signedVoluntaryExit *spec.SignedVoluntaryExit +} + +func output(ctx context.Context, data *dataOut) (string, error) { + if data == nil { + return "", errors.New("no data") + } + + if data.signedVoluntaryExit == nil { + return "", errors.New("no signed voluntary exit") + } + + if data.jsonOutput { + return outputJSON(ctx, data) + } + + return "", nil +} + +func outputJSON(ctx context.Context, data *dataOut) (string, error) { + validatorExitData := &util.ValidatorExitData{ + Data: 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 +} diff --git a/cmd/validator/exit/output_internal_test.go b/cmd/validator/exit/output_internal_test.go new file mode 100644 index 0000000..144ee99 --- /dev/null +++ b/cmd/validator/exit/output_internal_test.go @@ -0,0 +1,97 @@ +// 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: `{"data":{"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) + } + }) + } +} diff --git a/cmd/validator/exit/process.go b/cmd/validator/exit/process.go new file mode 100644 index 0000000..22d5d28 --- /dev/null +++ b/cmd/validator/exit/process.go @@ -0,0 +1,133 @@ +// 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" + + 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/core" + "github.com/wealdtech/ethdo/signing" +) + +// maxFutureEpochs is the farthest in the future for which an exit will be created. +var maxFutureEpochs = spec.Epoch(1024) + +func process(ctx context.Context, data *dataIn) (*dataOut, error) { + if data == nil { + return nil, errors.New("no data") + } + + if data.epoch > data.currentEpoch { + if data.epoch-data.currentEpoch > maxFutureEpochs { + return nil, errors.New("not generating exit for an epoch in the far future") + } + } + results := &dataOut{ + forkVersion: data.fork.CurrentVersion, + jsonOutput: data.jsonOutput, + } + + validator, err := fetchValidator(ctx, data) + if err != nil { + return nil, err + } + + exit, err := generateExit(ctx, data, validator) + if err != nil { + return nil, errors.Wrap(err, "failed to generate voluntary exit") + } + root, err := exit.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "failed to generate root for voluntary exit") + } + + if data.account != nil { + signature, err := signing.SignRoot(ctx, data.account, data.passphrases, root, data.domain) + if err != nil { + return nil, errors.Wrap(err, "failed to sign voluntary exit") + } + + results.signedVoluntaryExit = &spec.SignedVoluntaryExit{ + Message: exit, + Signature: signature, + } + } else { + results.signedVoluntaryExit = data.signedVoluntaryExit + } + + if !data.jsonOutput { + if err := broadcastExit(ctx, data, results); err != nil { + return nil, errors.Wrap(err, "failed to broadcast voluntary exit") + } + } + + return results, nil +} + +func generateExit(ctx context.Context, data *dataIn, validator *api.Validator) (*spec.VoluntaryExit, error) { + if data == nil { + return nil, errors.New("no data") + } + + if data.signedVoluntaryExit != nil { + return data.signedVoluntaryExit.Message, nil + } + + if validator == nil { + return nil, errors.New("no validator") + } + + exit := &spec.VoluntaryExit{ + Epoch: data.epoch, + ValidatorIndex: validator.Index, + } + return exit, nil +} + +func broadcastExit(ctx context.Context, data *dataIn, results *dataOut) error { + return data.eth2Client.(eth2client.VoluntaryExitSubmitter).SubmitVoluntaryExit(ctx, results.signedVoluntaryExit) +} + +func fetchValidator(ctx context.Context, data *dataIn) (*api.Validator, error) { + // Validator. + if data.account == nil { + return nil, nil + } + + var validator *api.Validator + validatorPubKeys := make([]spec.BLSPubKey, 1) + pubKey, err := core.BestPublicKey(data.account) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain public key for account") + } + copy(validatorPubKeys[0][:], pubKey.Marshal()) + validators, err := data.eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, "head", validatorPubKeys) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain validator from beacon node") + } + if len(validators) == 0 { + return nil, errors.New("validator not known by beacon node") + } + for _, v := range validators { + validator = v + } + if validator.Status != api.ValidatorStateActiveOngoing { + return nil, errors.New("validator is not active; cannot exit") + } + return validator, nil +} diff --git a/cmd/validator/exit/process_internal_test.go b/cmd/validator/exit/process_internal_test.go new file mode 100644 index 0000000..aeb2ec4 --- /dev/null +++ b/cmd/validator/exit/process_internal_test.go @@ -0,0 +1,234 @@ +// 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" + + api "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/auto" + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/spf13/viper" + "github.com/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 TestProcess(t *testing.T) { + if os.Getenv("ETHDO_TEST_CONNECTION") == "" { + t.Skip("ETHDO_TEST_CONNECTION not configured; cannot run tests") + } + + require.NoError(t, e2types.InitBLS()) + + eth2Client, err := auto.New(context.Background(), + auto.WithAddress(os.Getenv("ETHDO_TEST_CONNECTION")), + ) + require.NoError(t, err) + store := scratch.New() + require.NoError(t, e2wallet.UseStore(store)) + testWallet, err := nd.CreateWallet(context.Background(), "Test wallet", store, keystorev4.New()) + require.NoError(t, err) + require.NoError(t, testWallet.(e2wtypes.WalletLocker).Unlock(context.Background(), nil)) + viper.Set("passphrase", "pass") + interop0, err := testWallet.(e2wtypes.WalletAccountImporter).ImportAccount(context.Background(), + "Interop 0", + testutil.HexToBytes("0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"), + []byte("pass"), + ) + require.NoError(t, err) + + // activeValidator := &api.Validator{ + // Index: 123, + // Balance: 32123456789, + // Status: api.ValidatorStateActiveOngoing, + // Validator: &spec.Validator{ + // PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), + // WithdrawalCredentials: nil, + // EffectiveBalance: 32000000000, + // Slashed: false, + // ActivationEligibilityEpoch: 0, + // ActivationEpoch: 0, + // ExitEpoch: 0, + // WithdrawableEpoch: 0, + // }, + // } + + epochFork := &spec.Fork{ + PreviousVersion: spec.Version{0x00, 0x00, 0x00, 0x00}, + CurrentVersion: spec.Version{0x00, 0x00, 0x00, 0x00}, + Epoch: 0, + } + + tests := []struct { + name string + dataIn *dataIn + err string + }{ + { + name: "Nil", + err: "no data", + }, + { + name: "EpochTooLate", + dataIn: &dataIn{ + timeout: 5 * time.Second, + eth2Client: eth2Client, + fork: epochFork, + currentEpoch: 10, + account: interop0, + passphrases: []string{"pass"}, + epoch: 9999999, + domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}, + }, + err: "not generating exit for an epoch in the far future", + }, + { + name: "AccountUnknown", + dataIn: &dataIn{ + timeout: 5 * time.Second, + eth2Client: eth2Client, + fork: epochFork, + currentEpoch: 10, + account: interop0, + passphrases: []string{"pass"}, + epoch: 10, + domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}, + }, + err: "validator not known by beacon node", + }, + // { + // name: "Good", + // dataIn: &dataIn{ + // timeout: 5 * time.Second, + // eth2Client: eth2Client, + // fork: epochFork, + // currentEpoch: 10, + // account: interop0, + // passphrases: []string{"pass"}, + // epoch: 10, + // domain: spec.Domain{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}, + // }, + // }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := process(context.Background(), test.dataIn) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestGenerateExit(t *testing.T) { + activeValidator := &api.Validator{ + Index: 123, + Balance: 32123456789, + Status: api.ValidatorStateActiveOngoing, + Validator: &spec.Validator{ + PublicKey: testutil.HexToPubKey("0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c"), + WithdrawalCredentials: nil, + EffectiveBalance: 32000000000, + Slashed: false, + ActivationEligibilityEpoch: 0, + ActivationEpoch: 0, + ExitEpoch: 0, + WithdrawableEpoch: 0, + }, + } + + tests := []struct { + name string + validator *api.Validator + dataIn *dataIn + err string + }{ + { + name: "Nil", + err: "no data", + }, + { + name: "SignedVoluntaryExitGood", + dataIn: &dataIn{ + signedVoluntaryExit: &spec.SignedVoluntaryExit{ + Message: &spec.VoluntaryExit{ + Epoch: spec.Epoch(123), + ValidatorIndex: spec.ValidatorIndex(456), + }, + Signature: spec.BLSSignature{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + }, + }, + }, + }, + { + name: "ValidatorMissing", + dataIn: &dataIn{}, + err: "no validator", + }, + { + name: "ValidatorGood", + dataIn: &dataIn{}, + validator: activeValidator, + }, + { + name: "Good", + dataIn: &dataIn{ + signedVoluntaryExit: &spec.SignedVoluntaryExit{ + Message: &spec.VoluntaryExit{ + Epoch: spec.Epoch(123), + ValidatorIndex: spec.ValidatorIndex(456), + }, + Signature: spec.BLSSignature{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, + }, + }, + }, + validator: activeValidator, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := generateExit(context.Background(), test.dataIn, test.validator) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/cmd/validator/exit/run.go b/cmd/validator/exit/run.go new file mode 100644 index 0000000..faad2af --- /dev/null +++ b/cmd/validator/exit/run.go @@ -0,0 +1,50 @@ +// 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" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Run runs the wallet create data command. +func Run(cmd *cobra.Command) (string, error) { + ctx := context.Background() + dataIn, err := input(ctx) + if err != nil { + return "", errors.Wrap(err, "failed to obtain input") + } + + // Further errors do not need a usage report. + cmd.SilenceUsage = true + + dataOut, err := process(ctx, dataIn) + if err != nil { + return "", errors.Wrap(err, "failed to process") + } + + if viper.GetBool("quiet") { + return "", nil + } + + results, err := output(ctx, dataOut) + if err != nil { + return "", errors.Wrap(err, "failed to obtain output") + } + + return results, nil +} diff --git a/cmd/validatorexit.go b/cmd/validatorexit.go index 1ef7e01..e1bebe2 100644 --- a/cmd/validatorexit.go +++ b/cmd/validatorexit.go @@ -14,29 +14,13 @@ package cmd import ( - "context" - "encoding/hex" - "encoding/json" "fmt" - "os" - "strings" - "time" - "github.com/pkg/errors" - ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/wealdtech/ethdo/grpc" - "github.com/wealdtech/ethdo/util" - e2types "github.com/wealdtech/go-eth2-types/v2" - e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" + validatorexit "github.com/wealdtech/ethdo/cmd/validator/exit" ) -var validatorExitEpoch int64 -var validatorExitKey string -var validatorExitJSON string -var validatorExitJSONOutput bool - var validatorExitCmd = &cobra.Command{ Use: "exit", Short: "Send an exit request for a validator", @@ -45,219 +29,41 @@ var validatorExitCmd = &cobra.Command{ ethdo validator exit --account=primary/validator --passphrase=secret In quiet mode this will return 0 if the transaction has been generated, otherwise 1.`, - Run: func(cmd *cobra.Command, args []string) { - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - err := connect() - errCheck(err, "Failed to obtain connect to Ethereum 2 beacon chain node") - - exit, signature, forkVersion := validatorExitHandleInput(ctx) - validatorExitHandleExit(ctx, exit, signature, forkVersion) - os.Exit(_exitSuccess) + RunE: func(cmd *cobra.Command, args []string) error { + res, err := validatorexit.Run(cmd) + if err != nil { + return err + } + if viper.GetBool("quiet") { + return nil + } + if res != "" { + fmt.Println(res) + } + return nil }, } -func validatorExitHandleInput(ctx context.Context) (*ethpb.VoluntaryExit, e2types.Signature, []byte) { - if validatorExitJSON != "" { - return validatorExitHandleJSONInput(validatorExitJSON) - } - if viper.GetString("account") != "" { - _, account, err := walletAndAccountFromInput(ctx) - errCheck(err, "Failed to obtain account") - outputIf(debug, fmt.Sprintf("Account %s obtained", account.Name())) - return validatorExitHandleAccountInput(ctx, account) - } - if validatorExitKey != "" { - privKeyBytes, err := hex.DecodeString(strings.TrimPrefix(validatorExitKey, "0x")) - errCheck(err, fmt.Sprintf("Failed to decode key %s", validatorExitKey)) - account, err := util.NewScratchAccount(privKeyBytes, nil) - errCheck(err, "Invalid private key") - return validatorExitHandleAccountInput(ctx, account) - } - die("one of --json, --account or --key is required") - return nil, nil, nil -} - -func validatorExitHandleJSONInput(input string) (*ethpb.VoluntaryExit, e2types.Signature, []byte) { - data := &validatorExitData{} - err := json.Unmarshal([]byte(input), data) - errCheck(err, "Invalid JSON input") - exit := ðpb.VoluntaryExit{ - Epoch: data.Epoch, - ValidatorIndex: data.ValidatorIndex, - } - signature, err := e2types.BLSSignatureFromBytes(data.Signature) - errCheck(err, "Invalid signature") - return exit, signature, data.ForkVersion -} - -func validatorExitHandleAccountInput(ctx context.Context, account e2wtypes.Account) (*ethpb.VoluntaryExit, e2types.Signature, []byte) { - exit := ðpb.VoluntaryExit{} - - // Beacon chain config required for later work. - config, err := grpc.FetchChainConfig(eth2GRPCConn) - errCheck(err, "Failed to obtain beacon chain configuration") - secondsPerSlot, ok := config["SecondsPerSlot"].(uint64) - assert(ok, "Failed to obtain seconds per slot from chain") - slotsPerEpoch, ok := config["SlotsPerEpoch"].(uint64) - assert(ok, "Failed to obtain slots per epoch from chain") - secondsPerEpoch := secondsPerSlot * slotsPerEpoch - - // Fetch the validator's index. - index, err := grpc.FetchValidatorIndex(eth2GRPCConn, account) - errCheck(err, "Failed to obtain validator index") - outputIf(debug, fmt.Sprintf("Validator index is %d", index)) - exit.ValidatorIndex = index - - // Ensure the validator is active. - state, err := grpc.FetchValidatorState(eth2GRPCConn, account) - errCheck(err, "Failed to obtain validator state") - outputIf(debug, fmt.Sprintf("Validator state is %v", state)) - assert(state == ethpb.ValidatorStatus_ACTIVE, "Validator must be active to exit") - - if validatorExitEpoch < 0 { - // Ensure the validator has been active long enough to exit. - validator, err := grpc.FetchValidator(eth2GRPCConn, account) - errCheck(err, "Failed to obtain validator information") - outputIf(debug, fmt.Sprintf("Activation epoch is %v", validator.ActivationEpoch)) - shardCommitteePeriod, ok := config["ShardCommitteePeriod"].(uint64) - assert(ok, "Failed to obtain shard committee period from chain") - earliestExitEpoch := validator.ActivationEpoch + shardCommitteePeriod - - genesisTime, err := grpc.FetchGenesisTime(eth2GRPCConn) - errCheck(err, "Failed to obtain genesis time") - - currentEpoch := uint64(time.Since(genesisTime).Seconds()) / secondsPerEpoch - assert(currentEpoch >= earliestExitEpoch, fmt.Sprintf("Validator cannot exit until %s ( epoch %d); transaction not sent", genesisTime.Add(time.Duration(secondsPerEpoch*earliestExitEpoch)*time.Second).Format(time.UnixDate), earliestExitEpoch)) - outputIf(verbose, "Validator confirmed to be in a suitable state") - exit.Epoch = currentEpoch - } else { - // User-specified epoch; no checks. - exit.Epoch = uint64(validatorExitEpoch) - } - - // TODO fetch current fork version from config (currently using genesis fork version) - forkVersion := config["GenesisForkVersion"].([]byte) - outputIf(debug, fmt.Sprintf("Current fork version is %x", forkVersion)) - genesisValidatorsRoot, err := grpc.FetchGenesisValidatorsRoot(eth2GRPCConn) - outputIf(debug, fmt.Sprintf("Genesis validators root is %x", genesisValidatorsRoot)) - errCheck(err, "Failed to obtain genesis validators root") - domain := e2types.Domain(e2types.DomainVoluntaryExit, forkVersion, genesisValidatorsRoot) - - alreadyUnlocked, err := unlock(account) - errCheck(err, "Failed to unlock account; please confirm passphrase is correct") - signature, err := signStruct(account, exit, domain) - if !alreadyUnlocked { - errCheck(lock(account), "Failed to re-lock account") - } - errCheck(err, "Failed to sign exit proposal") - - return exit, signature, forkVersion -} - -// validatorExitHandleExit handles the exit request. -func validatorExitHandleExit(ctx context.Context, exit *ethpb.VoluntaryExit, signature e2types.Signature, forkVersion []byte) { - if validatorExitJSONOutput { - data := &validatorExitData{ - Epoch: exit.Epoch, - ValidatorIndex: exit.ValidatorIndex, - Signature: signature.Marshal(), - ForkVersion: forkVersion, - } - res, err := json.Marshal(data) - errCheck(err, "Failed to generate JSON") - outputIf(!quiet, string(res)) - } else { - proposal := ðpb.SignedVoluntaryExit{ - Exit: exit, - Signature: signature.Marshal(), - } - - validatorClient := ethpb.NewBeaconNodeValidatorClient(eth2GRPCConn) - _, err := validatorClient.ProposeExit(ctx, proposal) - errCheck(err, "Failed to propose exit") - outputIf(!quiet, "Validator exit transaction sent") - } -} - func init() { validatorCmd.AddCommand(validatorExitCmd) validatorFlags(validatorExitCmd) - validatorExitCmd.Flags().Int64Var(&validatorExitEpoch, "epoch", -1, "Epoch at which to exit (defaults to current epoch)") - validatorExitCmd.Flags().StringVar(&validatorExitKey, "key", "", "Private key if account not known by ethdo") - validatorExitCmd.Flags().BoolVar(&validatorExitJSONOutput, "json-output", false, "Print JSON transaction; do not broadcast to network") - validatorExitCmd.Flags().StringVar(&validatorExitJSON, "json", "", "Use JSON as created by --json-output to exit") + validatorExitCmd.Flags().Int64("epoch", -1, "Epoch at which to exit (defaults to current epoch)") + validatorExitCmd.Flags().String("key", "", "Private key if validator not known by ethdo") + validatorExitCmd.Flags().String("exit", "", "Use pre-defined JSON data as created by --json to exit") + validatorExitCmd.Flags().Bool("json", false, "Generate JSON data for an exit; do not broadcast to network") } -type validatorExitData struct { - Epoch uint64 `json:"epoch"` - ValidatorIndex uint64 `json:"validator_index"` - Signature []byte `json:"signature"` - ForkVersion []byte `json:"fork_version"` -} - -// MarshalJSON implements custom JSON marshaller. -func (d *validatorExitData) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`{"epoch":%d,"validator_index":%d,"signature":"%#x","fork_version":"%#x"}`, d.Epoch, d.ValidatorIndex, d.Signature, d.ForkVersion)), nil -} - -// UnmarshalJSON implements custom JSON unmarshaller. -func (d *validatorExitData) UnmarshalJSON(data []byte) error { - var v map[string]interface{} - if err := json.Unmarshal(data, &v); err != nil { - return err - } - - if val, exists := v["epoch"]; exists { - var ok bool - epoch, ok := val.(float64) - if !ok { - return errors.New("epoch invalid") - } - d.Epoch = uint64(epoch) - } else { - return errors.New("epoch missing") - } - - if val, exists := v["validator_index"]; exists { - var ok bool - validatorIndex, ok := val.(float64) - if !ok { - return errors.New("validator_index invalid") - } - d.ValidatorIndex = uint64(validatorIndex) - } else { - return errors.New("validator_index missing") - } - - if val, exists := v["signature"]; exists { - signatureBytes, ok := val.(string) - if !ok { - return errors.New("signature invalid") - } - signature, err := hex.DecodeString(strings.TrimPrefix(signatureBytes, "0x")) - if err != nil { - return errors.Wrap(err, "signature invalid") - } - d.Signature = signature - } else { - return errors.New("signature missing") - } - - if val, exists := v["fork_version"]; exists { - forkVersionBytes, ok := val.(string) - if !ok { - return errors.New("fork version invalid") - } - forkVersion, err := hex.DecodeString(strings.TrimPrefix(forkVersionBytes, "0x")) - if err != nil { - return errors.Wrap(err, "fork version invalid") - } - d.ForkVersion = forkVersion - } else { - return errors.New("fork version missing") - } - - return nil +func validatorExitBindings() { + if err := viper.BindPFlag("epoch", validatorExitCmd.Flags().Lookup("epoch")); err != nil { + panic(err) + } + if err := viper.BindPFlag("key", validatorExitCmd.Flags().Lookup("key")); err != nil { + panic(err) + } + if err := viper.BindPFlag("exit", validatorExitCmd.Flags().Lookup("exit")); err != nil { + panic(err) + } + if err := viper.BindPFlag("json", validatorExitCmd.Flags().Lookup("json")); err != nil { + panic(err) + } } diff --git a/cmd/validatorinfo.go b/cmd/validatorinfo.go index 98b4836..935e777 100644 --- a/cmd/validatorinfo.go +++ b/cmd/validatorinfo.go @@ -24,20 +24,19 @@ import ( "os" "strconv" "strings" - "time" + 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" - ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/wealdtech/ethdo/grpc" + "github.com/wealdtech/ethdo/core" "github.com/wealdtech/ethdo/util" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" string2eth "github.com/wealdtech/go-string2eth" ) -var validatorInfoPubKey string - var validatorInfoCmd = &cobra.Command{ Use: "info", Short: "Obtain information about a validator", @@ -47,78 +46,81 @@ var validatorInfoCmd = &cobra.Command{ In quiet mode this will return 0 if the validator information can be obtained, otherwise 1.`, Run: func(cmd *cobra.Command, args []string) { - assert(viper.GetString("account") != "" || validatorInfoPubKey != "", "--account or --pubkey is required") + ctx := context.Background() - err := connect() - errCheck(err, "Failed to obtain connection to Ethereum 2 beacon chain node") + eth2Client, err := util.ConnectToBeaconNode(ctx, viper.GetString("connection"), viper.GetDuration("timeout"), viper.GetBool("allow-insecure-connections")) + errCheck(err, "Failed to connect to Ethereum 2 beacon node") account, err := validatorInfoAccount() errCheck(err, "Failed to obtain validator account") + pubKeys := make([]spec.BLSPubKey, 1) + pubKey, err := core.BestPublicKey(account) + errCheck(err, "Failed to obtain validator public key") + copy(pubKeys[0][:], pubKey.Marshal()) + validators, err := eth2Client.(eth2client.ValidatorsProvider).ValidatorsByPubKey(ctx, "head", pubKeys) + errCheck(err, "Failed to obtain validator information") + if len(validators) == 0 { + fmt.Println("Validator not known by beacon node") + os.Exit(_exitSuccess) + } + + var validator *api.Validator + for _, v := range validators { + validator = v + } + if verbose { - network := network() + network, err := util.Network(ctx, eth2Client) + errCheck(err, "Failed to obtain network") outputIf(debug, fmt.Sprintf("Network is %s", network)) pubKey, err := bestPublicKey(account) if err == nil { deposits, totalDeposited, err := graphData(network, pubKey.Marshal()) if err == nil { fmt.Printf("Number of deposits: %d\n", deposits) - fmt.Printf("Total deposited: %s\n", string2eth.GWeiToString(totalDeposited, true)) + fmt.Printf("Total deposited: %s\n", string2eth.GWeiToString(uint64(totalDeposited), true)) } } } - validatorInfo, err := grpc.FetchValidatorInfo(eth2GRPCConn, account) - errCheck(err, "Failed to obtain validator information") - validator, err := grpc.FetchValidator(eth2GRPCConn, account) - if err != nil { - // We can live with this. - validator = nil - } - if validatorInfo.Status != ethpb.ValidatorStatus_DEPOSITED && - validatorInfo.Status != ethpb.ValidatorStatus_UNKNOWN_STATUS { - errCheck(err, "Failed to obtain validator definition") - } - assert(validatorInfo.Status != ethpb.ValidatorStatus_UNKNOWN_STATUS, "Not known as a validator") - if quiet { os.Exit(_exitSuccess) } - outputIf(verbose, fmt.Sprintf("Epoch of data: %v", validatorInfo.Epoch)) - outputIf(verbose && validatorInfo.Status != ethpb.ValidatorStatus_DEPOSITED, fmt.Sprintf("Index: %v", validatorInfo.Index)) - outputIf(verbose, fmt.Sprintf("Public key: %#x", validatorInfo.PublicKey)) - fmt.Printf("Status: %s\n", strings.Title(strings.ToLower(validatorInfo.Status.String()))) - fmt.Printf("Balance: %s\n", string2eth.GWeiToString(validatorInfo.Balance, true)) - - if validatorInfo.Status == ethpb.ValidatorStatus_ACTIVE || - validatorInfo.Status == ethpb.ValidatorStatus_EXITING || - validatorInfo.Status == ethpb.ValidatorStatus_SLASHING { - fmt.Printf("Effective balance: %s\n", string2eth.GWeiToString(validatorInfo.EffectiveBalance, true)) - } - - if validator != nil { - outputIf(verbose, fmt.Sprintf("Withdrawal credentials: %#x", validator.WithdrawalCredentials)) - } - - transition := time.Unix(int64(validatorInfo.TransitionTimestamp), 0) - transitionPassed := int64(validatorInfo.TransitionTimestamp) <= time.Now().Unix() - switch validatorInfo.Status { - case ethpb.ValidatorStatus_DEPOSITED: - if validatorInfo.TransitionTimestamp != 0 { - fmt.Printf("Inclusion in chain: %s\n", transition) - } - case ethpb.ValidatorStatus_PENDING: - fmt.Printf("Activation: %s\n", transition) - case ethpb.ValidatorStatus_EXITING, ethpb.ValidatorStatus_SLASHING: - fmt.Printf("Attesting finishes: %s\n", transition) - case ethpb.ValidatorStatus_EXITED: - if transitionPassed { - fmt.Printf("Funds withdrawable: Now\n") - } else { - fmt.Printf("Funds withdrawable: %s\n", transition) + if verbose { + if validator.Status.HasActivated() { + fmt.Printf("Index: %d\n", validator.Index) } + fmt.Printf("Public key: %#x\n", validator.Validator.PublicKey) } + fmt.Printf("Status: %v\n", validator.Status) + fmt.Printf("Balance: %s\n", string2eth.GWeiToString(uint64(validator.Balance), true)) + if validator.Status.IsActive() { + fmt.Printf("Effective balance: %s\n", string2eth.GWeiToString(uint64(validator.Validator.EffectiveBalance), true)) + } + if verbose { + fmt.Printf("Withdrawal credentials: %#x\n", validator.Validator.WithdrawalCredentials) + } + + // transition := time.Unix(int64(validatorInfo.TransitionTimestamp), 0) + // transitionPassed := int64(validatorInfo.TransitionTimestamp) <= time.Now().Unix() + // switch validatorInfo.Status { + // case ethpb.ValidatorStatus_DEPOSITED: + // if validatorInfo.TransitionTimestamp != 0 { + // fmt.Printf("Inclusion in chain: %s\n", transition) + // } + // case ethpb.ValidatorStatus_PENDING: + // fmt.Printf("Activation: %s\n", transition) + // case ethpb.ValidatorStatus_EXITING, ethpb.ValidatorStatus_SLASHING: + // fmt.Printf("Attesting finishes: %s\n", transition) + // case ethpb.ValidatorStatus_EXITED: + // if transitionPassed { + // fmt.Printf("Funds withdrawable: Now\n") + // } else { + // fmt.Printf("Funds withdrawable: %s\n", transition) + // } + // } os.Exit(_exitSuccess) }, @@ -128,29 +130,37 @@ In quiet mode this will return 0 if the validator information can be obtained, o func validatorInfoAccount() (e2wtypes.Account, error) { var account e2wtypes.Account var err error - if viper.GetString("account") != "" { + switch { + case 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(validatorInfoPubKey, "0x")) + case viper.GetString("pubkey") != "": + pubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(viper.GetString("pubkey"), "0x")) if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", validatorInfoPubKey)) + return nil, errors.Wrap(err, fmt.Sprintf("failed to decode public key %s", viper.GetString("pubkey"))) } account, err = util.NewScratchAccount(nil, pubKeyBytes) if err != nil { - return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", validatorInfoPubKey)) + return nil, errors.Wrap(err, fmt.Sprintf("invalid public key %s", viper.GetString("pubkey"))) } + default: + return nil, errors.New("neither account nor public key supplied") } return account, nil } // graphData returns data from the graph about number and amount of deposits -func graphData(network string, validatorPubKey []byte) (uint64, uint64, error) { - subgraph := fmt.Sprintf("attestantio/eth2deposits-%s", strings.ToLower(network)) +func graphData(network string, validatorPubKey []byte) (uint64, spec.Gwei, error) { + subgraph := "" + if network == "Mainnet" { + subgraph = "attestantio/eth2deposits" + } else { + subgraph = fmt.Sprintf("attestantio/eth2deposits-%s", strings.ToLower(network)) + } query := fmt.Sprintf(`{"query": "{deposits(where: {validatorPubKey:\"%#x\"}) { id amount withdrawalCredentials }}"}`, validatorPubKey) url := fmt.Sprintf("https://api.thegraph.com/subgraphs/name/%s", subgraph) graphResp, err := http.Post(url, "application/json", bytes.NewBufferString(query)) @@ -180,7 +190,7 @@ func graphData(network string, validatorPubKey []byte) (uint64, uint64, error) { return 0, 0, errors.Wrap(err, "invalid data returned from existing deposit check") } deposits := uint64(0) - totalDeposited := uint64(0) + totalDeposited := spec.Gwei(0) if response.Data != nil && len(response.Data.Deposits) > 0 { for _, deposit := range response.Data.Deposits { deposits++ @@ -188,7 +198,7 @@ func graphData(network string, validatorPubKey []byte) (uint64, uint64, error) { if err != nil { return 0, 0, errors.Wrap(err, fmt.Sprintf("invalid deposit amount from pre-existing deposit %s", deposit.Amount)) } - totalDeposited += depositAmount + totalDeposited += spec.Gwei(depositAmount) } } return deposits, totalDeposited, nil @@ -196,6 +206,12 @@ func graphData(network string, validatorPubKey []byte) (uint64, uint64, error) { func init() { validatorCmd.AddCommand(validatorInfoCmd) - validatorInfoCmd.Flags().StringVar(&validatorInfoPubKey, "pubkey", "", "Public key for which to obtain status") + validatorInfoCmd.Flags().String("pubkey", "", "Public key for which to obtain status") validatorFlags(validatorInfoCmd) } + +func validatorInfoBindings() { + if err := viper.BindPFlag("pubkey", validatorInfoCmd.Flags().Lookup("pubkey")); err != nil { + panic(err) + } +} diff --git a/docs/usage.md b/docs/usage.md index c8bf1d0..df2672d 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -385,9 +385,8 @@ Validator commands focus on interaction with Ethereum 2 validators. `ethdo validator exit` sends a transaction to the chain to tell an active validator to exit the validation queue. Options include: - `epoch` specify an epoch before which this exit is not valid - - `json-output` generate JSON output rather than sending a transaction immediately - - `json` use JSON input created by the `--json-output` option rather than generate data from scratch - - `forkversion` specify a specific fork version; default is to fetch it from the chain but this can be used when generating offline deposits + - `json` generate JSON output rather than sending a transaction immediately + - `exit` use JSON exit input created by the `--json` option rather than generate data from scratch ```sh $ ethdo validator exit --account=Validators/1 --passphrase="my validator secret" diff --git a/go.mod b/go.mod index 57f031a..7e23be5 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.13 require ( github.com/OneOfOne/xxhash v1.2.5 // indirect github.com/attestantio/dirk v0.9.2 - github.com/attestantio/go-eth2-client v0.6.9 + github.com/attestantio/go-eth2-client v0.6.10 github.com/ferranbt/fastssz v0.0.0-20201030134205-9b9624098321 github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gofrs/uuid v3.3.0+incompatible diff --git a/go.sum b/go.sum index cd94c77..6a74e90 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/attestantio/go-eth2-client v0.6.8 h1:Lsjx5P0pB8ruZBfJUbqy5hpevD4Zt8Z0 github.com/attestantio/go-eth2-client v0.6.8/go.mod h1:lYEayGHzZma9HMUJgyxFIzDWRck8n2IedP7KTkIwe0g= github.com/attestantio/go-eth2-client v0.6.9 h1:Hbf4tX9MvxCsLokED8Ic3tQxmEAb/phoBkBmk8sKJm0= github.com/attestantio/go-eth2-client v0.6.9/go.mod h1:ODAZ4yS1YYYew/EsgGsVb/siNEoa505CrGsvlVFdkfo= +github.com/attestantio/go-eth2-client v0.6.10 h1:PMNBMLk6xfMEUqhaUnsI0/HZRrstZF18Gt6Dm5GelW4= +github.com/attestantio/go-eth2-client v0.6.10/go.mod h1:ODAZ4yS1YYYew/EsgGsVb/siNEoa505CrGsvlVFdkfo= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.32.6 h1:HoswAabUWgnrUF7X/9dr4WRgrr8DyscxXvTDm7Qw/5c= @@ -510,6 +512,7 @@ github.com/prysmaticlabs/go-bitfield v0.0.0-20200618145306-2ae0807bef65/go.mod h github.com/prysmaticlabs/go-ssz v0.0.0-20200101200214-e24db4d9e963/go.mod h1:VecIJZrewdAuhVckySLFt2wAAHRME934bSDurP8ftkc= github.com/prysmaticlabs/go-ssz v0.0.0-20200612203617-6d5c9aa213ae h1:7qd0Af1ozWKBU3c93YW2RH+/09hJns9+ftqWUZyts9c= github.com/prysmaticlabs/go-ssz v0.0.0-20200612203617-6d5c9aa213ae/go.mod h1:VecIJZrewdAuhVckySLFt2wAAHRME934bSDurP8ftkc= +github.com/r3labs/sse/v2 v2.3.0 h1:R/UMa0ML6AYKQ8irQNHhY+204lz1LytDIdKhCxSVAd8= github.com/r3labs/sse/v2 v2.3.0/go.mod h1:hUrYMKfu9WquG9MyI0r6TKiNH+6Sw/QPKm2YbNbU5g8= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -1139,6 +1142,7 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/grpc/beaconchain.go b/grpc/beaconchain.go deleted file mode 100644 index 28fcf29..0000000 --- a/grpc/beaconchain.go +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright © 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 grpc - -import ( - "context" - "strconv" - "strings" - - "github.com/gogo/protobuf/types" - "github.com/pkg/errors" - ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" - "github.com/spf13/viper" - e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" - "google.golang.org/grpc" -) - -// FetchChainConfig fetches the chain configuration from the beacon node. -// It tweaks the output to make it easier to work with by setting appropriate -// types. -func FetchChainConfig(conn *grpc.ClientConn) (map[string]interface{}, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - config, err := beaconClient.GetBeaconConfig(ctx, &types.Empty{}) - if err != nil { - return nil, err - } - results := make(map[string]interface{}) - for k, v := range config.Config { - // Handle integers - if v == "0" { - results[k] = uint64(0) - continue - } - intVal, err := strconv.ParseUint(v, 10, 64) - if err == nil && intVal != 0 { - results[k] = intVal - continue - } - - // Handle byte arrays - if strings.HasPrefix(v, "[") { - vals := strings.Split(v[1:len(v)-1], " ") - res := make([]byte, len(vals)) - for i, val := range vals { - intVal, err := strconv.Atoi(val) - if err != nil { - return nil, errors.Wrapf(err, "failed to convert value %q for %s", v, k) - } - res[i] = byte(intVal) - } - results[k] = res - continue - } - - // String (or unhandled format) - results[k] = v - } - return results, nil -} - -func FetchLatestFilledSlot(conn *grpc.ClientConn) (uint64, error) { - if conn == nil { - return 0, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - chainHead, err := beaconClient.GetChainHead(ctx, &types.Empty{}) - if err != nil { - return 0, errors.Wrap(err, "failed to obtain latest") - } - - return chainHead.HeadSlot, nil -} - -// FetchValidatorCommittees fetches the validator committees for a given epoch. -func FetchValidatorCommittees(conn *grpc.ClientConn, epoch uint64) (map[uint64][][]uint64, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - req := ðpb.ListCommitteesRequest{ - QueryFilter: ðpb.ListCommitteesRequest_Epoch{ - Epoch: epoch, - }, - } - resp, err := beaconClient.ListBeaconCommittees(ctx, req) - if err != nil { - return nil, errors.Wrap(err, "failed to obtain committees") - } - - res := make(map[uint64][][]uint64) - for slot, committees := range resp.Committees { - res[slot] = make([][]uint64, len(resp.Committees)) - for i, committee := range committees.Committees { - res[slot][uint64(i)] = make([]uint64, len(committee.ValidatorIndices)) - indices := make([]uint64, len(committee.ValidatorIndices)) - copy(indices, committee.ValidatorIndices) - res[slot][uint64(i)] = indices - } - } - - return res, nil -} - -// FetchValidator fetches the validator definition from the beacon node. -func FetchValidator(conn *grpc.ClientConn, account e2wtypes.Account) (*ethpb.Validator, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - var pubKey []byte - if pubKeyProvider, ok := account.(e2wtypes.AccountCompositePublicKeyProvider); ok { - pubKey = pubKeyProvider.CompositePublicKey().Marshal() - } else if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok { - pubKey = pubKeyProvider.PublicKey().Marshal() - } else { - return nil, errors.New("Unable to obtain public key") - } - - req := ðpb.GetValidatorRequest{ - QueryFilter: ðpb.GetValidatorRequest_PublicKey{ - PublicKey: pubKey, - }, - } - return beaconClient.GetValidator(ctx, req) -} - -// FetchValidatorByIndex fetches the validator definition from the beacon node. -func FetchValidatorByIndex(conn *grpc.ClientConn, index uint64) (*ethpb.Validator, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - req := ðpb.GetValidatorRequest{ - QueryFilter: ðpb.GetValidatorRequest_Index{ - Index: index, - }, - } - return beaconClient.GetValidator(ctx, req) -} - -// FetchValidatorBalance fetches the validator balance from the beacon node. -func FetchValidatorBalance(conn *grpc.ClientConn, account e2wtypes.Account) (uint64, error) { - if conn == nil { - return 0, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - var pubKey []byte - if pubKeyProvider, ok := account.(e2wtypes.AccountCompositePublicKeyProvider); ok { - pubKey = pubKeyProvider.CompositePublicKey().Marshal() - } else if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok { - pubKey = pubKeyProvider.PublicKey().Marshal() - } else { - return 0, errors.New("Unable to obtain public key") - } - - res, err := beaconClient.ListValidatorBalances(ctx, ðpb.ListValidatorBalancesRequest{ - PublicKeys: [][]byte{pubKey}, - }) - if err != nil { - return 0, err - } - if len(res.Balances) == 0 { - return 0, errors.New("unknown validator") - } - return res.Balances[0].Balance, nil -} - -// FetchValidatorPerformance fetches the validator performance from the beacon node. -func FetchValidatorPerformance(conn *grpc.ClientConn, account e2wtypes.Account) (bool, bool, bool, uint64, int64, error) { - if conn == nil { - return false, false, false, 0, 0, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - var pubKey []byte - if pubKeyProvider, ok := account.(e2wtypes.AccountCompositePublicKeyProvider); ok { - pubKey = pubKeyProvider.CompositePublicKey().Marshal() - } else if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok { - pubKey = pubKeyProvider.PublicKey().Marshal() - } else { - return false, false, false, 0, 0, errors.New("Unable to obtain public key") - } - - req := ðpb.ValidatorPerformanceRequest{ - PublicKeys: [][]byte{pubKey}, - } - res, err := beaconClient.GetValidatorPerformance(ctx, req) - if err != nil { - return false, false, false, 0, 0, err - } - if len(res.InclusionDistances) == 0 { - return false, false, false, 0, 0, errors.New("unknown validator") - } - return res.CorrectlyVotedHead[0], - res.CorrectlyVotedSource[0], - res.CorrectlyVotedTarget[0], - res.InclusionDistances[0], - int64(res.BalancesAfterEpochTransition[0]) - int64(res.BalancesBeforeEpochTransition[0]), - err -} - -// FetchValidatorInfo fetches current validator info from the beacon node. -func FetchValidatorInfo(conn *grpc.ClientConn, account e2wtypes.Account) (*ethpb.ValidatorInfo, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - stream, err := beaconClient.StreamValidatorsInfo(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to contact beacon node") - } - - var pubKey []byte - if pubKeyProvider, ok := account.(e2wtypes.AccountCompositePublicKeyProvider); ok { - pubKey = pubKeyProvider.CompositePublicKey().Marshal() - } else if pubKeyProvider, ok := account.(e2wtypes.AccountPublicKeyProvider); ok { - pubKey = pubKeyProvider.PublicKey().Marshal() - } else { - return nil, errors.New("Unable to obtain public key") - } - - changeSet := ðpb.ValidatorChangeSet{ - Action: ethpb.SetAction_SET_VALIDATOR_KEYS, - PublicKeys: [][]byte{pubKey}, - } - err = stream.Send(changeSet) - if err != nil { - return nil, errors.Wrap(err, "failed to send validator public key") - } - return stream.Recv() -} - -// FetchChainInfo fetches current chain info from the beacon node. -func FetchChainInfo(conn *grpc.ClientConn) (*ethpb.ChainHead, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - return beaconClient.GetChainHead(ctx, &types.Empty{}) -} - -// FetchBlock fetches a block at a given slot from the beacon node. -func FetchBlock(conn *grpc.ClientConn, slot uint64) (*ethpb.SignedBeaconBlock, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - beaconClient := ethpb.NewBeaconChainClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - req := ðpb.ListBlocksRequest{} - if slot == 0 { - req.QueryFilter = ðpb.ListBlocksRequest_Genesis{Genesis: true} - } else { - req.QueryFilter = ðpb.ListBlocksRequest_Slot{Slot: slot} - } - resp, err := beaconClient.ListBlocks(ctx, req) - if err != nil { - return nil, err - } - if len(resp.BlockContainers) == 0 { - return nil, nil - } - return resp.BlockContainers[0].Block, nil -} - -func StreamBlocks(conn *grpc.ClientConn) (ethpb.BeaconChain_StreamBlocksClient, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - - beaconClient := ethpb.NewBeaconChainClient(conn) - stream, err := beaconClient.StreamBlocks(context.Background(), &types.Empty{}) - if err != nil { - return nil, err - } - - return stream, nil -} diff --git a/grpc/beaconnode.go b/grpc/beaconnode.go deleted file mode 100644 index 90059db..0000000 --- a/grpc/beaconnode.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright © 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 grpc - -import ( - "context" - - "github.com/pkg/errors" - "github.com/spf13/viper" - "google.golang.org/grpc" - - ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" - wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" -) - -// FetchValidatorIndex fetches the index of a validator. -func FetchValidatorIndex(conn *grpc.ClientConn, account wtypes.Account) (uint64, error) { - if conn == nil { - return 0, errors.New("no connection to beacon node") - } - validatorClient := ethpb.NewBeaconNodeValidatorClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - var pubKey []byte - if pubKeyProvider, ok := account.(wtypes.AccountCompositePublicKeyProvider); ok { - pubKey = pubKeyProvider.CompositePublicKey().Marshal() - } else if pubKeyProvider, ok := account.(wtypes.AccountPublicKeyProvider); ok { - pubKey = pubKeyProvider.PublicKey().Marshal() - } else { - return 0, errors.New("Unable to obtain public key") - } - - // Fetch the account. - req := ðpb.ValidatorIndexRequest{ - PublicKey: pubKey, - } - resp, err := validatorClient.ValidatorIndex(ctx, req) - if err != nil { - return 0, err - } - - return resp.Index, nil -} - -// FetchValidatorState fetches the state of a validator. -func FetchValidatorState(conn *grpc.ClientConn, account wtypes.Account) (ethpb.ValidatorStatus, error) { - if conn == nil { - return ethpb.ValidatorStatus_UNKNOWN_STATUS, errors.New("no connection to beacon node") - } - validatorClient := ethpb.NewBeaconNodeValidatorClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - - var pubKey []byte - if pubKeyProvider, ok := account.(wtypes.AccountCompositePublicKeyProvider); ok { - pubKey = pubKeyProvider.CompositePublicKey().Marshal() - } else if pubKeyProvider, ok := account.(wtypes.AccountPublicKeyProvider); ok { - pubKey = pubKeyProvider.PublicKey().Marshal() - } else { - return ethpb.ValidatorStatus_UNKNOWN_STATUS, errors.New("Unable to obtain public key") - } - - // Fetch the account. - req := ðpb.ValidatorStatusRequest{ - PublicKey: pubKey, - } - resp, err := validatorClient.ValidatorStatus(ctx, req) - if err != nil { - return ethpb.ValidatorStatus_UNKNOWN_STATUS, err - } - - return resp.Status, nil -} diff --git a/grpc/connect.go b/grpc/connect.go deleted file mode 100644 index aa41db5..0000000 --- a/grpc/connect.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © 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 grpc - -import ( - "context" - - "github.com/pkg/errors" - "github.com/spf13/viper" - "google.golang.org/grpc" -) - -// Connect connects to an Ethereum 2 endpoint. -func Connect() (*grpc.ClientConn, error) { - connection := "" - if viper.GetString("connection") != "" { - connection = viper.GetString("connection") - } - - if connection == "" { - return nil, errors.New("no connection") - } - // outputIf(debug, fmt.Sprintf("Connecting to %s", connection)) - - opts := []grpc.DialOption{grpc.WithInsecure()} - - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - return grpc.DialContext(ctx, connection, opts...) -} diff --git a/grpc/node.go b/grpc/node.go deleted file mode 100644 index f14574d..0000000 --- a/grpc/node.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright © 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 grpc - -import ( - "context" - "time" - - "github.com/gogo/protobuf/types" - "github.com/pkg/errors" - "github.com/spf13/viper" - "google.golang.org/grpc" - - ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" -) - -// FetchGenesisTime fetches the genesis time. -func FetchGenesisTime(conn *grpc.ClientConn) (time.Time, error) { - if conn == nil { - return time.Now(), errors.New("no connection to beacon node") - } - client := ethpb.NewNodeClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - res, err := client.GetGenesis(ctx, &types.Empty{}) - if err != nil { - return time.Now(), err - } - return time.Unix(res.GetGenesisTime().Seconds, 0), nil -} - -// FetchGenesisValidatorsRoot fetches the genesis validators root. -func FetchGenesisValidatorsRoot(conn *grpc.ClientConn) ([]byte, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - client := ethpb.NewNodeClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - res, err := client.GetGenesis(ctx, &types.Empty{}) - if err != nil { - return nil, err - } - return res.GetGenesisValidatorsRoot(), nil -} - -// FetchDepositContractAddress fetches the address of the deposit contract. -func FetchDepositContractAddress(conn *grpc.ClientConn) ([]byte, error) { - if conn == nil { - return nil, errors.New("no connection to beacon node") - } - client := ethpb.NewNodeClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - res, err := client.GetGenesis(ctx, &types.Empty{}) - if err != nil { - return nil, err - } - return res.DepositContractAddress, nil -} - -// FetchVersion fetches the version and metadata from the server. -func FetchVersion(conn *grpc.ClientConn) (string, string, error) { - if conn == nil { - return "", "", errors.New("no connection to beacon node") - } - client := ethpb.NewNodeClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - version, err := client.GetVersion(ctx, &types.Empty{}) - if err != nil { - return "", "", err - } - return version.Version, version.Metadata, nil -} - -// FetchSyncing returns true if the node is syncing, otherwise false. -func FetchSyncing(conn *grpc.ClientConn) (bool, error) { - if conn == nil { - return false, errors.New("no connection to beacon node") - } - client := ethpb.NewNodeClient(conn) - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - syncStatus, err := client.GetSyncStatus(ctx, &types.Empty{}) - if err != nil { - return false, err - } - return syncStatus.Syncing, nil -} diff --git a/signing/misc.go b/signing/misc.go index 8a9c482..264236a 100644 --- a/signing/misc.go +++ b/signing/misc.go @@ -17,13 +17,11 @@ import ( "context" "github.com/pkg/errors" - "github.com/spf13/viper" - "github.com/wealdtech/ethdo/util" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) -// unlock attempts to unlock an account. It returns true if the account was already unlocked. -func unlock(account e2wtypes.Account) (bool, error) { +// Unlock attempts to unlock an account. It returns true if the account was already unlocked. +func Unlock(ctx context.Context, account e2wtypes.Account, passphrases []string) (bool, error) { locker, isAccountLocker := account.(e2wtypes.AccountLocker) if !isAccountLocker { // outputIf(debug, "Account does not support unlocking") @@ -31,9 +29,7 @@ func unlock(account e2wtypes.Account) (bool, error) { return true, nil } - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) alreadyUnlocked, err := locker.IsUnlocked(ctx) - cancel() if err != nil { return false, errors.Wrap(err, "unable to ascertain if account is unlocked") } @@ -43,10 +39,8 @@ func unlock(account e2wtypes.Account) (bool, error) { } // Not already unlocked; attempt to unlock it. - for _, passphrase := range util.GetPassphrases() { - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) + for _, passphrase := range passphrases { err = locker.Unlock(ctx, []byte(passphrase)) - cancel() if err == nil { // Unlocked. return false, nil @@ -57,13 +51,11 @@ func unlock(account e2wtypes.Account) (bool, error) { return false, errors.New("failed to unlock account") } -// lock attempts to lock an account. -func lock(account e2wtypes.Account) error { +// Lock attempts to lock an account. +func Lock(ctx context.Context, account e2wtypes.Account) error { locker, isAccountLocker := account.(e2wtypes.AccountLocker) if !isAccountLocker { return nil } - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() return locker.Lock(ctx) } diff --git a/signing/signroot.go b/signing/signroot.go index 4d830eb..211abb0 100644 --- a/signing/signroot.go +++ b/signing/signroot.go @@ -16,65 +16,59 @@ package signing import ( "context" + spec "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/pkg/errors" - "github.com/spf13/viper" e2types "github.com/wealdtech/go-eth2-types/v2" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) // SignRoot signs a root with a domain. -func SignRoot(account e2wtypes.Account, root []byte, domain []byte) ([]byte, error) { +func SignRoot(ctx context.Context, account e2wtypes.Account, passphrases []string, root spec.Root, domain spec.Domain) (spec.BLSSignature, error) { // Ensure input is as expected. if account == nil { - return nil, errors.New("account not specified") - } - if len(root) != 32 { - return nil, errors.New("root must be 32 bytes in length") - } - if len(domain) != 32 { - return nil, errors.New("domain must be 32 bytes in length") + return spec.BLSSignature{}, errors.New("account not specified") } - alreadyUnlocked, err := unlock(account) + alreadyUnlocked, err := Unlock(ctx, account, passphrases) if err != nil { - return nil, err + return spec.BLSSignature{}, err } var signature e2types.Signature // outputIf(debug, fmt.Sprintf("Signing %x (%d)", data, len(data))) if protectingSigner, isProtectingSigner := account.(e2wtypes.AccountProtectingSigner); isProtectingSigner { // Signer takes root and domain. - signature, err = signProtected(protectingSigner, root, domain) + signature, err = signProtected(ctx, protectingSigner, root, domain) } else if signer, isSigner := account.(e2wtypes.AccountSigner); isSigner { - signature, err = sign(signer, root, domain) + signature, err = sign(ctx, signer, root, domain) } else { - return nil, errors.New("account does not provide signing facility") + return spec.BLSSignature{}, errors.New("account does not provide signing facility") } if err != nil { - return nil, err + return spec.BLSSignature{}, err } if !alreadyUnlocked { - if err := lock(account); err != nil { - return nil, errors.Wrap(err, "failed to lock account") + if err := Lock(ctx, account); err != nil { + return spec.BLSSignature{}, errors.Wrap(err, "failed to lock account") } } - return signature.Marshal(), nil + var sig spec.BLSSignature + copy(sig[:], signature.Marshal()) + return sig, nil } -func sign(account e2wtypes.AccountSigner, root []byte, domain []byte) (e2types.Signature, error) { +func sign(ctx context.Context, account e2wtypes.AccountSigner, root spec.Root, domain spec.Domain) (e2types.Signature, error) { container := &Container{ - Root: root, - Domain: domain, + Root: root[:], + Domain: domain[:], } signingRoot, err := container.HashTreeRoot() if err != nil { return nil, errors.Wrap(err, "failed to generate hash tree root") } - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() signature, err := account.Sign(ctx, signingRoot[:]) if err != nil { return nil, errors.Wrap(err, "failed to sign") @@ -83,10 +77,8 @@ func sign(account e2wtypes.AccountSigner, root []byte, domain []byte) (e2types.S return signature, err } -func signProtected(account e2wtypes.AccountProtectingSigner, data []byte, domain []byte) (e2types.Signature, error) { - ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout")) - defer cancel() - signature, err := account.SignGeneric(ctx, data, domain) +func signProtected(ctx context.Context, account e2wtypes.AccountProtectingSigner, root spec.Root, domain spec.Domain) (e2types.Signature, error) { + signature, err := account.SignGeneric(ctx, root[:], domain[:]) if err != nil { return nil, errors.Wrap(err, "failed to sign") } diff --git a/testutil/bytes.go b/testutil/bytes.go index 6e81714..47fd2ec 100644 --- a/testutil/bytes.go +++ b/testutil/bytes.go @@ -16,6 +16,8 @@ package testutil import ( "encoding/hex" "strings" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" ) // HexToBytes converts a hex string to a byte array. @@ -27,3 +29,57 @@ func HexToBytes(input string) []byte { } return res } + +// HexToPubKey converts a hex string to a spec public key. +// This should only be used for pre-defined test strings; it will panic if the input is invalid. +func HexToPubKey(input string) spec.BLSPubKey { + data := HexToBytes(input) + var res spec.BLSPubKey + copy(res[:], data) + return res +} + +// HexToSignature converts a hex string to a spec signature. +// This should only be used for pre-defined test strings; it will panic if the input is invalid. +func HexToSignature(input string) spec.BLSSignature { + data := HexToBytes(input) + var res spec.BLSSignature + copy(res[:], data) + return res +} + +// HexToDomainType converts a hex string to a spec domain type. +// This should only be used for pre-defined test strings; it will panic if the input is invalid. +func HexToDomainType(input string) spec.DomainType { + data := HexToBytes(input) + var res spec.DomainType + copy(res[:], data) + return res +} + +// HexToDomain converts a hex string to a spec domain. +// This should only be used for pre-defined test strings; it will panic if the input is invalid. +func HexToDomain(input string) spec.Domain { + data := HexToBytes(input) + var res spec.Domain + copy(res[:], data) + return res +} + +// HexToVersion converts a hex string to a spec version. +// This should only be used for pre-defined test strings; it will panic if the input is invalid. +func HexToVersion(input string) spec.Version { + data := HexToBytes(input) + var res spec.Version + copy(res[:], data) + return res +} + +// HexToRoot converts a hex string to a spec root. +// This should only be used for pre-defined test strings; it will panic if the input is invalid. +func HexToRoot(input string) spec.Root { + data := HexToBytes(input) + var res spec.Root + copy(res[:], data) + return res +} diff --git a/util/account.go b/util/account.go new file mode 100644 index 0000000..e1b54e9 --- /dev/null +++ b/util/account.go @@ -0,0 +1,62 @@ +// Copyright © 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 util + +import ( + "context" + + "github.com/pkg/errors" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" +) + +// UnlockAccount attempts to unlock an account. It returns true if the account was already unlocked. +func UnlockAccount(ctx context.Context, account e2wtypes.Account, passphrases []string) (bool, error) { + locker, isAccountLocker := account.(e2wtypes.AccountLocker) + if !isAccountLocker { + // This account doesn't support unlocking; return okay. + return true, nil + } + + alreadyUnlocked, err := locker.IsUnlocked(ctx) + if err != nil { + return false, errors.Wrap(err, "unable to ascertain if account is unlocked") + } + + if alreadyUnlocked { + return true, nil + } + + // Not already unlocked; attempt to unlock it. + for _, passphrase := range passphrases { + err = locker.Unlock(ctx, []byte(passphrase)) + if err == nil { + // Unlocked. + return false, nil + } + } + + // Failed to unlock it. + return false, errors.New("failed to unlock account") +} + +// LockAccount attempts to lock an account. +func LockAccount(ctx context.Context, account e2wtypes.Account) error { + locker, isAccountLocker := account.(e2wtypes.AccountLocker) + if !isAccountLocker { + // This account doesn't support locking; return okay. + return nil + } + + return locker.Lock(ctx) +} diff --git a/util/beaconnode.go b/util/beaconnode.go new file mode 100644 index 0000000..cdc9f51 --- /dev/null +++ b/util/beaconnode.go @@ -0,0 +1,52 @@ +// Copyright © 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 util + +import ( + "context" + "fmt" + "net/url" + "time" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/auto" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// 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 !allowInsecure { + // Ensure the connection is either secure or local. + connectionURL, err := url.Parse(address) + if err != nil { + return nil, errors.Wrap(err, "failed to parse connection") + } + if connectionURL.Scheme == "http" && + connectionURL.Host != "localhost" && + connectionURL.Host != "127.0.0.1" { + fmt.Println("Connections to remote beacon nodes should be secure. This warning can be silenced with --allow-insecure-connections") + } + } + eth2Client, err := auto.New(ctx, + auto.WithLogLevel(zerolog.Disabled), + auto.WithAddress(address), + auto.WithTimeout(timeout), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to connect to beacon node") + } + + return eth2Client, nil +} diff --git a/util/networks.go b/util/networks.go new file mode 100644 index 0000000..6905492 --- /dev/null +++ b/util/networks.go @@ -0,0 +1,55 @@ +// Copyright © 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 util + +import ( + "context" + "fmt" + + eth2client "github.com/attestantio/go-eth2-client" + "github.com/pkg/errors" +) + +// networks is a map of deposit contract addresses to networks. +var networks = map[string]string{ + "00000000219ab540356cbb839cbe05303d7705fa": "Mainnet", + "07b39f4fde4a38bace212b546dac87c58dfe3fdc": "Medalla", +} + +// Network returns the name of the network., calculated from the deposit contract information. +// If not known, returns "Unknown". +func Network(ctx context.Context, eth2Client eth2client.Service) (string, error) { + var address []byte + var err error + + if provider, isProvider := eth2Client.(eth2client.DepositContractProvider); isProvider { + address, err = provider.DepositContractAddress(ctx) + if err != nil { + return "", errors.Wrap(err, "failed to obtain deposit contract address") + } + } else if provider, isProvider := eth2Client.(eth2client.SpecProvider); isProvider { + config, err := provider.Spec(ctx) + if err != nil { + return "", errors.Wrap(err, "failed to obtain deposit contract address") + } + address = config["DEPOSIT_CONTRACT_ADDRESS"].([]byte) + } + // outputIf(debug, fmt.Sprintf("Deposit contract is %#x", address)) + + depositContract := fmt.Sprintf("%x", address) + if network, exists := networks[depositContract]; exists { + return network, nil + } + return "Unknown", nil +} diff --git a/util/scratchaccount.go b/util/scratchaccount.go index dee78c8..39262d5 100644 --- a/util/scratchaccount.go +++ b/util/scratchaccount.go @@ -14,17 +14,18 @@ package util import ( + "context" "errors" "github.com/google/uuid" - types "github.com/wealdtech/go-eth2-types/v2" + e2types "github.com/wealdtech/go-eth2-types/v2" ) // ScratchAccount is an account that exists temporarily. type ScratchAccount struct { id uuid.UUID - privKey types.PrivateKey - pubKey types.PublicKey + privKey e2types.PrivateKey + pubKey e2types.PublicKey unlocked bool } @@ -37,7 +38,7 @@ func NewScratchAccount(privKey []byte, pubKey []byte) (*ScratchAccount, error) { } func newScratchAccountFromPrivKey(privKey []byte) (*ScratchAccount, error) { - key, err := types.BLSPrivateKeyFromBytes(privKey) + key, err := e2types.BLSPrivateKeyFromBytes(privKey) if err != nil { return nil, err } @@ -49,7 +50,7 @@ func newScratchAccountFromPrivKey(privKey []byte) (*ScratchAccount, error) { } func newScratchAccountFromPubKey(pubKey []byte) (*ScratchAccount, error) { - key, err := types.BLSPublicKeyFromBytes(pubKey) + key, err := e2types.BLSPublicKeyFromBytes(pubKey) if err != nil { return nil, err } @@ -67,7 +68,7 @@ func (a *ScratchAccount) Name() string { return "scratch" } -func (a *ScratchAccount) PublicKey() types.PublicKey { +func (a *ScratchAccount) PublicKey() e2types.PublicKey { return a.pubKey } @@ -75,21 +76,22 @@ func (a *ScratchAccount) Path() string { return "" } -func (a *ScratchAccount) Lock() { +func (a *ScratchAccount) Lock(ctx context.Context) error { a.unlocked = false + return nil } -func (a *ScratchAccount) Unlock([]byte) error { +func (a *ScratchAccount) Unlock(ctx context.Context, passphrase []byte) error { a.unlocked = true return nil } -func (a *ScratchAccount) IsUnlocked() bool { - return a.unlocked +func (a *ScratchAccount) IsUnlocked(ctx context.Context) (bool, error) { + return a.unlocked, nil } -func (a *ScratchAccount) Sign(data []byte) (types.Signature, error) { - if !a.IsUnlocked() { +func (a *ScratchAccount) Sign(ctx context.Context, data []byte) (e2types.Signature, error) { + if !a.unlocked { return nil, errors.New("locked") } if a.privKey == nil { diff --git a/util/scratchaccount_test.go b/util/scratchaccount_test.go index e6b1c96..500aa64 100644 --- a/util/scratchaccount_test.go +++ b/util/scratchaccount_test.go @@ -14,6 +14,7 @@ package util_test import ( + "context" "testing" "github.com/stretchr/testify/require" @@ -62,20 +63,27 @@ func TestScratchAccountFromPrivKey(t *testing.T) { require.Equal(t, "scratch", account.Name()) require.Equal(t, "", account.Path()) require.NotNil(t, account.PublicKey()) - require.False(t, account.IsUnlocked()) - _, err := account.Sign(testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")) + unlocked, err := account.IsUnlocked(context.Background()) + require.NoError(t, err) + require.False(t, unlocked) + _, err = account.Sign(context.Background(), testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")) require.EqualError(t, err, "locked") - require.NoError(t, account.Unlock(nil)) - require.True(t, account.IsUnlocked()) - signature, err := account.Sign(testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")) + err = account.Unlock(context.Background(), nil) + require.NoError(t, err) + unlocked, err = account.IsUnlocked(context.Background()) + require.NoError(t, err) + require.True(t, unlocked) + signature, err := account.Sign(context.Background(), testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")) if test.sigErr == "" { require.NoError(t, err) require.Equal(t, test.signature, signature.Marshal()) } else { require.EqualError(t, err, test.sigErr) } - account.Lock() - require.False(t, account.IsUnlocked()) + require.NoError(t, account.Lock(context.Background())) + unlocked, err = account.IsUnlocked(context.Background()) + require.NoError(t, err) + require.False(t, unlocked) } else { require.EqualError(t, err, test.err) } @@ -119,15 +127,21 @@ func TestScratchAccountFromPublicKey(t *testing.T) { require.NotNil(t, account.ID()) require.Equal(t, "scratch", account.Name()) require.Equal(t, "", account.Path()) - require.False(t, account.IsUnlocked()) - _, err := account.Sign(testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")) + unlocked, err := account.IsUnlocked(context.Background()) + require.NoError(t, err) + require.False(t, unlocked) + _, err = account.Sign(context.Background(), testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")) require.EqualError(t, err, "locked") - require.NoError(t, account.Unlock(nil)) - require.True(t, account.IsUnlocked()) - _, err = account.Sign(testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")) + require.NoError(t, account.Unlock(context.Background(), nil)) + unlocked, err = account.IsUnlocked(context.Background()) + require.NoError(t, err) + require.True(t, unlocked) + _, err = account.Sign(context.Background(), testutil.HexToBytes("0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")) require.EqualError(t, err, "no private key") - account.Lock() - require.False(t, account.IsUnlocked()) + account.Lock(context.Background()) + unlocked, err = account.IsUnlocked(context.Background()) + require.NoError(t, err) + require.False(t, unlocked) } else { require.EqualError(t, err, test.err) } diff --git a/util/validatorexitdata.go b/util/validatorexitdata.go new file mode 100644 index 0000000..9a3f993 --- /dev/null +++ b/util/validatorexitdata.go @@ -0,0 +1,63 @@ +// Copyright © 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 util + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// ValidatorExitData contains data for a validator exit. +type ValidatorExitData struct { + Data *spec.SignedVoluntaryExit + ForkVersion spec.Version +} + +type validatorExitJSON struct { + Data *spec.SignedVoluntaryExit `json:"data"` + ForkVersion string `json:"fork_version"` +} + +// MarshalJSON implements custom JSON marshaller. +func (d *ValidatorExitData) MarshalJSON() ([]byte, error) { + validatorExitJSON := &validatorExitJSON{ + Data: d.Data, + ForkVersion: fmt.Sprintf("%#x", d.ForkVersion), + } + return json.Marshal(validatorExitJSON) +} + +// UnmarshalJSON implements custom JSON unmarshaller. +func (d *ValidatorExitData) UnmarshalJSON(data []byte) error { + validatorExitJSON := &validatorExitJSON{} + + if err := json.Unmarshal(data, validatorExitJSON); err != nil { + return errors.Wrap(err, "failed to unmarshal JSON") + } + + d.Data = validatorExitJSON.Data + + forkVersion, err := hex.DecodeString(strings.TrimPrefix(validatorExitJSON.ForkVersion, "0x")) + if err != nil { + return errors.Wrap(err, "failed to parse fork version") + } + copy(d.ForkVersion[:], forkVersion) + + return nil +} diff --git a/util/validatorexitdata_test.go b/util/validatorexitdata_test.go new file mode 100644 index 0000000..1b986ef --- /dev/null +++ b/util/validatorexitdata_test.go @@ -0,0 +1,57 @@ +// Copyright © 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 util_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + "github.com/wealdtech/ethdo/util" +) + +func TestUnmarshal(t *testing.T) { + + tests := []struct { + name string + in []byte + err string + }{ + { + name: "Nil", + err: "unexpected end of JSON input", + }, + { + name: "Invalid", + in: []byte(`invalid`), + err: "invalid character 'i' looking for beginning of value", + }, + { + name: "Good", + in: []byte(`{"data":{"message":{"epoch":"0","validator_index":"0"},"signature":"0xb74eade64ebf1e02cc57e5d29517032c6ca99132fb8e7fb7e6d58c68713e581ef0ef88e2a6c599a007d997782abdd50b0f9763500a93a971c89cb2275583fe755d7c0e64f459ff22fcef5cab3f80848f0356e67c142b9cf3ee65613f56283d6e"},"fork_version":"0x00000001"}`), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var res util.ValidatorExitData + err := json.Unmarshal(test.in, &res) + if test.err != "" { + require.EqualError(t, err, test.err) + } else { + require.NoError(t, err) + } + }) + } +}