From 0ea2bbbc7218a438f3686fb633337bdca1b2ccad Mon Sep 17 00:00:00 2001 From: Preston Van Loon Date: Mon, 13 Apr 2020 18:16:42 -0700 Subject: [PATCH] Revert keymanager changes (#5416) * Revert "Updates for remote keymanager (#5260)" This reverts commit bbcd895db50ce5e7c0ecb64210471cf56f63b373. * Revert "Remove keystore keymanager from validator (#5236)" This reverts commit 46008770c162e741251e13772fd7356b43a9af87. * Revert "Update eth2 wallet keymanager (#4984)" This reverts commit 7f7ef43f218598a671aaeb327342d7e5130fe8b1. Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com> --- WORKSPACE | 56 +++---- shared/cmd/flags.go | 2 +- validator/BUILD.bazel | 3 + validator/accounts/BUILD.bazel | 31 ++++ validator/accounts/account.go | 159 +++++++++++++++++++ validator/accounts/account_test.go | 37 +++++ validator/client/BUILD.bazel | 2 + validator/client/service_test.go | 36 +++-- validator/client/validator_aggregate_test.go | 7 +- validator/client/validator_attest_test.go | 41 +++-- validator/client/validator_propose_test.go | 29 ++-- validator/flags/flags.go | 18 ++- validator/keymanager/BUILD.bazel | 8 +- validator/keymanager/direct_keystore.go | 121 ++++++++++++++ validator/keymanager/remote.go | 80 ++-------- validator/keymanager/remote_internal_test.go | 58 ------- validator/keymanager/wallet.go | 5 +- validator/main.go | 59 +++++++ validator/node/BUILD.bazel | 1 + validator/node/node.go | 28 +++- validator/node/node_test.go | 16 +- validator/usage.go | 2 + 22 files changed, 557 insertions(+), 242 deletions(-) create mode 100644 validator/accounts/BUILD.bazel create mode 100644 validator/accounts/account.go create mode 100644 validator/accounts/account_test.go create mode 100644 validator/keymanager/direct_keystore.go delete mode 100644 validator/keymanager/remote_internal_test.go diff --git a/WORKSPACE b/WORKSPACE index 9b9e9c5a8c4d..3af68c1a5873 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1515,86 +1515,74 @@ go_repository( go_repository( name = "com_github_wealdtech_go_eth2_wallet", + commit = "6970d62e60d86fdae3c3e510e800e8a60d755a7d", importpath = "github.com/wealdtech/go-eth2-wallet", - sum = "h1:H/T1n0SNd0jTsbf4rA4YxigsBPFWRUWgobsTOjzW4Hw=", - version = "v1.9.2", ) go_repository( - name = "com_github_wealdtech_go_eth2_wallet_hd_v2", - importpath = "github.com/wealdtech/go-eth2-wallet-hd/v2", - sum = "h1:oqE/+zFOKteklEemecMWGlyNmPv+5OBaHmAo1LKG6LE=", - version = "v2.0.0", + name = "com_github_wealdtech_go_eth2_wallet_hd", + commit = "ce0a252a01c621687e9786a64899cfbfe802ba73", + importpath = "github.com/wealdtech/go-eth2-wallet-hd", ) go_repository( - name = "com_github_wealdtech_go_eth2_wallet_nd_v2", - importpath = "github.com/wealdtech/go-eth2-wallet-nd/v2", - sum = "h1:nWsbiaSVa1kwRdwPX5NfXsrowlRBjqoRpDv37i8ZecE=", - version = "v2.0.0", + name = "com_github_wealdtech_go_eth2_wallet_nd", + commit = "12c8c41cdbd16797ff292e27f58e126bb89e9706", + importpath = "github.com/wealdtech/go-eth2-wallet-nd", ) go_repository( name = "com_github_wealdtech_go_eth2_wallet_store_filesystem", + commit = "1eea6a48d75380047d2ebe7c8c4bd8985bcfdeca", importpath = "github.com/wealdtech/go-eth2-wallet-store-filesystem", - sum = "h1:px7vV01opCUeeHjvdiBdkPbdnr60Ygq01Ddjy4dIbfg=", - version = "v1.7.1", ) go_repository( name = "com_github_wealdtech_go_eth2_wallet_store_s3", + commit = "1c821b5161f7bb0b3efa2030eff687eea5e70e53", importpath = "github.com/wealdtech/go-eth2-wallet-store-s3", - sum = "h1:f86TIVHqYkmDYc8VLsiIJ/KbGtNMeCGhkefqpXUVmYE=", - version = "v1.6.1", ) go_repository( name = "com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4", + commit = "0c11c07b9544eb662210fadded94f40f309d8c8f", importpath = "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4", - sum = "h1:IcpS4VpXhYz+TVupB5n6C6IQzaKwG+Rc8nvgCa/da4c=", - version = "v1.0.0", ) go_repository( - name = "com_github_wealdtech_go_eth2_wallet_types_v2", - importpath = "github.com/wealdtech/go-eth2-wallet-types/v2", - sum = "h1:EyTwHO7zXtYkf62h3MqSB3OWc8pv0dnFl41yykUJY3s=", - version = "v2.0.0", + name = "com_github_wealdtech_go_eth2_wallet_types", + commit = "af67d8101be61e7c4dd8126d2b3eba20cff5dab2", + importpath = "github.com/wealdtech/go-eth2-wallet-types", ) go_repository( - name = "com_github_wealdtech_go_eth2_types_v2", - importpath = "github.com/wealdtech/go-eth2-types/v2", - sum = "h1:L1Eg55aArRpUR2H8dnpSevHlSGRDuRQbQwA4IyYh0Js=", - version = "v2.0.2", + name = "com_github_wealdtech_go_eth2_types", + commit = "f9c31ddf180537dd5712d5998a3d56c45864d71f", + importpath = "github.com/wealdtech/go-eth2-types", ) go_repository( name = "com_github_wealdtech_go_eth2_util", + commit = "326ebb1755651131bb8f4506ea9a23be6d9ad1dd", importpath = "github.com/wealdtech/go-eth2-util", - sum = "h1:m56HKJgWSuNy53Gt5GN7HcoFaGRCl1uE3OGWhIhWh1M=", - version = "v1.1.2", ) go_repository( name = "com_github_wealdtech_go_ecodec", + commit = "7473d835445a3490e61a5fcf48fe4e9755a37957", importpath = "github.com/wealdtech/go-ecodec", - sum = "h1:yggrTSckcPJRaxxOxQF7FPm21kgE8WA6+f5jdq5Kr8o=", - version = "v1.1.0", ) go_repository( name = "com_github_wealdtech_go_bytesutil", + commit = "e564d0ade555b9f97494f0f669196ddcc6bc531d", importpath = "github.com/wealdtech/go-bytesutil", - sum = "h1:6XrN7OIQhhBjQy/PZ1HZ3ySE8v8UDyxzERkOgmsIc1g=", - version = "v1.1.0", ) go_repository( name = "com_github_wealdtech_go_indexer", + commit = "334862c32b1e3a5c6738a2618f5c0a8ebeb8cd51", importpath = "github.com/wealdtech/go-indexer", - sum = "h1:/S4rfWQbSOnnYmwnvuTVatDibZ8o1s9bmTCHO16XINg=", - version = "v1.0.0", ) go_repository( @@ -1662,8 +1650,8 @@ go_repository( name = "com_github_wealdtech_eth2_signer_api", build_file_proto_mode = "disable_global", importpath = "github.com/wealdtech/eth2-signer-api", - sum = "h1:AL4bRJDW6lyRc0ROPruVTEHt7Xs+EV2lRBPen2plOr8=", - version = "v1.2.0", + sum = "h1:fqJYjKwG/FeUAJYYiZblIP6agiz3WWB+Hxpw85Fnr5I=", + version = "v1.0.1", ) go_repository( diff --git a/shared/cmd/flags.go b/shared/cmd/flags.go index 1dafdc6e4d73..7fb8bde867a9 100644 --- a/shared/cmd/flags.go +++ b/shared/cmd/flags.go @@ -15,7 +15,7 @@ var ( // DataDirFlag defines a path on disk. DataDirFlag = &cli.StringFlag{ Name: "datadir", - Usage: "Data directory for the databases", + Usage: "Data directory for the databases and keystore", Value: DefaultDataDir(), } // EnableTracingFlag defines a flag to enable p2p message tracing. diff --git a/validator/BUILD.bazel b/validator/BUILD.bazel index 5f5ea28b3440..88cda1bbdb31 100644 --- a/validator/BUILD.bazel +++ b/validator/BUILD.bazel @@ -17,7 +17,9 @@ go_library( "//shared/debug:go_default_library", "//shared/featureconfig:go_default_library", "//shared/logutil:go_default_library", + "//shared/params:go_default_library", "//shared/version:go_default_library", + "//validator/accounts:go_default_library", "//validator/flags:go_default_library", "//validator/node:go_default_library", "@com_github_joonix_log//:go_default_library", @@ -54,6 +56,7 @@ go_image( "//shared/logutil:go_default_library", "//shared/params:go_default_library", "//shared/version:go_default_library", + "//validator/accounts:go_default_library", "//validator/flags:go_default_library", "//validator/node:go_default_library", "@com_github_joonix_log//:go_default_library", diff --git a/validator/accounts/BUILD.bazel b/validator/accounts/BUILD.bazel new file mode 100644 index 000000000000..266eacc20f01 --- /dev/null +++ b/validator/accounts/BUILD.bazel @@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["account.go"], + importpath = "github.com/prysmaticlabs/prysm/validator/accounts", + visibility = [ + "//validator:__pkg__", + "//validator:__subpackages__", + ], + deps = [ + "//contracts/deposit-contract:go_default_library", + "//shared/keystore:go_default_library", + "//shared/params:go_default_library", + "@com_github_pkg_errors//:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", + "@org_golang_x_crypto//ssh/terminal:go_default_library", + ], +) + +go_test( + name = "go_default_test", + size = "small", + srcs = ["account_test.go"], + embed = [":go_default_library"], + deps = [ + "//shared/keystore:go_default_library", + "//shared/params:go_default_library", + "//shared/testutil:go_default_library", + ], +) diff --git a/validator/accounts/account.go b/validator/accounts/account.go new file mode 100644 index 000000000000..8940dfc9e286 --- /dev/null +++ b/validator/accounts/account.go @@ -0,0 +1,159 @@ +package accounts + +import ( + "bufio" + "encoding/hex" + "fmt" + "io" + "os" + "strings" + + "github.com/pkg/errors" + contract "github.com/prysmaticlabs/prysm/contracts/deposit-contract" + "github.com/prysmaticlabs/prysm/shared/keystore" + "github.com/prysmaticlabs/prysm/shared/params" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh/terminal" +) + +var log = logrus.WithField("prefix", "accounts") + +// DecryptKeysFromKeystore extracts a set of validator private keys from +// an encrypted keystore directory and a password string. +func DecryptKeysFromKeystore(directory string, password string) (map[string]*keystore.Key, error) { + validatorPrefix := params.BeaconConfig().ValidatorPrivkeyFileName + ks := keystore.NewKeystore(directory) + validatorKeys, err := ks.GetKeys(directory, validatorPrefix, password, true /* warnOnFail */) + if err != nil { + return nil, errors.Wrap(err, "could not get private key") + } + return validatorKeys, nil +} + +// VerifyAccountNotExists checks if a validator has not yet created an account +// and keystore in the provided directory string. +func VerifyAccountNotExists(directory string, password string) error { + if directory == "" || password == "" { + return errors.New("expected a path to the validator keystore and password to be provided, received nil") + } + shardWithdrawalKeyFile := params.BeaconConfig().WithdrawalPrivkeyFileName + validatorKeyFile := params.BeaconConfig().ValidatorPrivkeyFileName + // First, if the keystore already exists, throws an error as there can only be + // one keystore per validator client. + ks := keystore.NewKeystore(directory) + if _, err := ks.GetKeys(directory, shardWithdrawalKeyFile, password, false /* warnOnFail */); err == nil { + return fmt.Errorf("keystore at path already exists: %s", shardWithdrawalKeyFile) + } + if _, err := ks.GetKeys(directory, validatorKeyFile, password, false /* warnOnFail */); err == nil { + return fmt.Errorf("keystore at path already exists: %s", validatorKeyFile) + } + return nil +} + +// NewValidatorAccount sets up a validator client's secrets and generates the necessary deposit data +// parameters needed to deposit into the deposit contract on the ETH1.0 chain. Specifically, this +// generates a BLS private and public key, and then logs the serialized deposit input hex string +// to be used in an ETH1.0 transaction by the validator. +func NewValidatorAccount(directory string, password string) error { + shardWithdrawalKeyFile := directory + params.BeaconConfig().WithdrawalPrivkeyFileName + validatorKeyFile := directory + params.BeaconConfig().ValidatorPrivkeyFileName + ks := keystore.NewKeystore(directory) + // If the keystore does not exists at the path, we create a new one for the validator. + shardWithdrawalKey, err := keystore.NewKey() + if err != nil { + return err + } + shardWithdrawalKeyFile = shardWithdrawalKeyFile + hex.EncodeToString(shardWithdrawalKey.PublicKey.Marshal())[:12] + if err := ks.StoreKey(shardWithdrawalKeyFile, shardWithdrawalKey, password); err != nil { + return errors.Wrap(err, "unable to store key") + } + log.WithField( + "path", + shardWithdrawalKeyFile, + ).Info("Keystore generated for shard withdrawals at path") + validatorKey, err := keystore.NewKey() + if err != nil { + return err + } + validatorKeyFile = validatorKeyFile + hex.EncodeToString(validatorKey.PublicKey.Marshal())[:12] + if err := ks.StoreKey(validatorKeyFile, validatorKey, password); err != nil { + return errors.Wrap(err, "unable to store key") + } + log.WithField( + "path", + validatorKeyFile, + ).Info("Keystore generated for validator signatures at path") + + data, depositRoot, err := keystore.DepositInput(validatorKey, shardWithdrawalKey, params.BeaconConfig().MaxEffectiveBalance) + if err != nil { + return errors.Wrap(err, "unable to generate deposit data") + } + testAcc, err := contract.Setup() + if err != nil { + return errors.Wrap(err, "unable to create simulated backend") + } + testAcc.TxOpts.GasLimit = 1000000 + + tx, err := testAcc.Contract.Deposit(testAcc.TxOpts, data.PublicKey, data.WithdrawalCredentials, data.Signature, depositRoot) + if err != nil { + return errors.Wrap(err, "unable to create deposit transaction") + } + log.Info(`Account creation complete! Copy and paste the raw transaction data shown below when issuing a transaction into the ETH1.0 deposit contract to activate your validator client`) + fmt.Printf(` +========================Raw Transaction Data======================= + +%#x + +=================================================================== +`, tx.Data()) + return nil +} + +// Exists checks if a validator account at a given keystore path exists. +func Exists(keystorePath string) (bool, error) { + /* #nosec */ + f, err := os.Open(keystorePath) + if err != nil { + return false, nil + } + defer func() { + if err := f.Close(); err != nil { + log.Fatal(err) + } + }() + + _, err = f.Readdirnames(1) // Or f.Readdir(1) + if err == io.EOF { + return false, nil + } + return true, err +} + +// CreateValidatorAccount creates a validator account from the given cli context. +func CreateValidatorAccount(path string, passphrase string) (string, string, error) { + if passphrase == "" { + reader := bufio.NewReader(os.Stdin) + log.Info("Create a new validator account for eth2") + log.Info("Enter a password:") + bytePassword, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + log.Fatalf("Could not read account password: %v", err) + } + text := string(bytePassword) + passphrase = strings.Replace(text, "\n", "", -1) + log.Infof("Keystore path to save your private keys (leave blank for default %s):", path) + text, err = reader.ReadString('\n') + if err != nil { + log.Fatal(err) + } + text = strings.Replace(text, "\n", "", -1) + if text != "" { + path = text + } + } + + if err := NewValidatorAccount(path, passphrase); err != nil { + return "", "", errors.Wrapf(err, "could not initialize validator account") + } + return path, passphrase, nil +} diff --git a/validator/accounts/account_test.go b/validator/accounts/account_test.go new file mode 100644 index 000000000000..18c524ad33b9 --- /dev/null +++ b/validator/accounts/account_test.go @@ -0,0 +1,37 @@ +package accounts + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/prysmaticlabs/prysm/shared/keystore" + "github.com/prysmaticlabs/prysm/shared/params" + "github.com/prysmaticlabs/prysm/shared/testutil" +) + +func TestNewValidatorAccount_AccountExists(t *testing.T) { + directory := testutil.TempDir() + "/testkeystore" + defer os.RemoveAll(directory) + validatorKey, err := keystore.NewKey() + if err != nil { + t.Fatalf("Cannot create new key: %v", err) + } + ks := keystore.NewKeystore(directory) + if err := ks.StoreKey(directory+params.BeaconConfig().ValidatorPrivkeyFileName, validatorKey, ""); err != nil { + t.Fatalf("Unable to store key %v", err) + } + if err := NewValidatorAccount(directory, ""); err != nil { + t.Errorf("Should support multiple keys: %v", err) + } + files, _ := ioutil.ReadDir(directory) + if len(files) != 3 { + t.Errorf("multiple validators were not created only %v files in directory", len(files)) + for _, f := range files { + t.Errorf("%v\n", f.Name()) + } + } + if err := os.RemoveAll(directory); err != nil { + t.Fatalf("Could not remove directory: %v", err) + } +} diff --git a/validator/client/BUILD.bazel b/validator/client/BUILD.bazel index c17f0267478d..d7d275221f8f 100644 --- a/validator/client/BUILD.bazel +++ b/validator/client/BUILD.bazel @@ -72,10 +72,12 @@ go_test( "//shared/bls:go_default_library", "//shared/bytesutil:go_default_library", "//shared/featureconfig:go_default_library", + "//shared/keystore:go_default_library", "//shared/mock:go_default_library", "//shared/params:go_default_library", "//shared/roughtime:go_default_library", "//shared/testutil:go_default_library", + "//validator/accounts:go_default_library", "//validator/db:go_default_library", "//validator/internal:go_default_library", "//validator/keymanager:go_default_library", diff --git a/validator/client/service_test.go b/validator/client/service_test.go index 1cbd807724d9..63a596ea47ff 100644 --- a/validator/client/service_test.go +++ b/validator/client/service_test.go @@ -9,44 +9,48 @@ import ( "github.com/prysmaticlabs/prysm/shared" "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/keystore" "github.com/prysmaticlabs/prysm/shared/testutil" + "github.com/prysmaticlabs/prysm/validator/accounts" "github.com/prysmaticlabs/prysm/validator/keymanager" logTest "github.com/sirupsen/logrus/hooks/test" ) var _ = shared.Service(&ValidatorService{}) -var validatorPubKey *bls.PublicKey -var secKeyMap map[[48]byte]*bls.SecretKey -var pubKeyMap map[[48]byte]*bls.PublicKey -var secKeyMapThreeValidators map[[48]byte]*bls.SecretKey -var pubKeyMapThreeValidators map[[48]byte]*bls.PublicKey +var validatorKey *keystore.Key +var validatorPubKey [48]byte +var keyMap map[[48]byte]*keystore.Key +var keyMapThreeValidators map[[48]byte]*keystore.Key var testKeyManager keymanager.KeyManager var testKeyManagerThreeValidators keymanager.KeyManager func keySetup() { - pubKeyMap = make(map[[48]byte]*bls.PublicKey) - secKeyMap = make(map[[48]byte]*bls.SecretKey) - pubKeyMapThreeValidators = make(map[[48]byte]*bls.PublicKey) - secKeyMapThreeValidators = make(map[[48]byte]*bls.SecretKey) + keyMap = make(map[[48]byte]*keystore.Key) + keyMapThreeValidators = make(map[[48]byte]*keystore.Key) + + validatorKey, _ = keystore.NewKey() + copy(validatorPubKey[:], validatorKey.PublicKey.Marshal()) + keyMap[validatorPubKey] = validatorKey sks := make([]*bls.SecretKey, 1) - sks[0] = bls.RandKey() + sks[0] = validatorKey.SecretKey testKeyManager = keymanager.NewDirect(sks) - validatorPubKey = sks[0].PublicKey() sks = make([]*bls.SecretKey, 3) for i := 0; i < 3; i++ { - secKey := bls.RandKey() + vKey, _ := keystore.NewKey() var pubKey [48]byte - copy(pubKey[:], secKey.PublicKey().Marshal()) - secKeyMapThreeValidators[pubKey] = secKey - pubKeyMapThreeValidators[pubKey] = secKey.PublicKey() - sks[i] = secKey + copy(pubKey[:], vKey.PublicKey.Marshal()) + keyMapThreeValidators[pubKey] = vKey + sks[i] = vKey.SecretKey } testKeyManagerThreeValidators = keymanager.NewDirect(sks) } func TestMain(m *testing.M) { + dir := testutil.TempDir() + "/keystore1" + defer os.RemoveAll(dir) + accounts.NewValidatorAccount(dir, "1234") keySetup() os.Exit(m.Run()) } diff --git a/validator/client/validator_aggregate_test.go b/validator/client/validator_aggregate_test.go index 793986f8315f..9a33eb3e9887 100644 --- a/validator/client/validator_aggregate_test.go +++ b/validator/client/validator_aggregate_test.go @@ -6,7 +6,6 @@ import ( "github.com/golang/mock/gomock" ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" - "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/roughtime" "github.com/prysmaticlabs/prysm/shared/testutil" @@ -19,7 +18,7 @@ func TestSubmitAggregateAndProof_GetDutiesRequestFailure(t *testing.T) { validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} defer finish() - validator.SubmitAggregateAndProof(context.Background(), 0, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAggregateAndProof(context.Background(), 0, validatorPubKey) testutil.AssertLogsContain(t, hook, "Could not fetch validator assignment") } @@ -30,7 +29,7 @@ func TestSubmitAggregateAndProof_Ok(t *testing.T) { validator.duties = ðpb.DutiesResponse{ Duties: []*ethpb.DutiesResponse_Duty{ { - PublicKey: validatorPubKey.Marshal(), + PublicKey: validatorKey.PublicKey.Marshal(), }, }, } @@ -61,7 +60,7 @@ func TestSubmitAggregateAndProof_Ok(t *testing.T) { gomock.AssignableToTypeOf(ðpb.SignedAggregateSubmitRequest{}), ).Return(ðpb.SignedAggregateSubmitResponse{}, nil) - validator.SubmitAggregateAndProof(context.Background(), 0, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAggregateAndProof(context.Background(), 0, validatorPubKey) } func TestWaitForSlotTwoThird_WaitCorrectly(t *testing.T) { diff --git a/validator/client/validator_attest_test.go b/validator/client/validator_attest_test.go index 4cd0b795e253..c6cbfbe8464b 100644 --- a/validator/client/validator_attest_test.go +++ b/validator/client/validator_attest_test.go @@ -13,7 +13,6 @@ import ( "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" slashpb "github.com/prysmaticlabs/prysm/proto/slashing" - "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/roughtime" @@ -28,7 +27,7 @@ func TestRequestAttestation_ValidatorDutiesRequestFailure(t *testing.T) { validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{}} defer finish() - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) testutil.AssertLogsContain(t, hook, "Could not fetch validator assignment") } @@ -39,7 +38,7 @@ func TestAttestToBlockHead_SubmitAttestationRequestFailure(t *testing.T) { defer finish() validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ { - PublicKey: validatorPubKey.Marshal(), + PublicKey: validatorKey.PublicKey.Marshal(), CommitteeIndex: 5, Committee: make([]uint64, 111), ValidatorIndex: 0, @@ -61,7 +60,7 @@ func TestAttestToBlockHead_SubmitAttestationRequestFailure(t *testing.T) { gomock.AssignableToTypeOf(ðpb.Attestation{}), ).Return(nil, errors.New("something went wrong")) - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) testutil.AssertLogsContain(t, hook, "Could not submit attestation to beacon node") } @@ -72,7 +71,7 @@ func TestAttestToBlockHead_AttestsCorrectly(t *testing.T) { committee := []uint64{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ { - PublicKey: validatorPubKey.Marshal(), + PublicKey: validatorKey.PublicKey.Marshal(), CommitteeIndex: 5, Committee: committee, ValidatorIndex: validatorIndex, @@ -99,7 +98,7 @@ func TestAttestToBlockHead_AttestsCorrectly(t *testing.T) { generatedAttestation = att }).Return(ðpb.AttestResponse{}, nil /* error */) - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) aggregationBitfield := bitfield.NewBitlist(uint64(len(committee))) aggregationBitfield.SetBitAt(4, true) @@ -117,7 +116,7 @@ func TestAttestToBlockHead_AttestsCorrectly(t *testing.T) { t.Fatal(err) } - sig, err := validator.keyManager.Sign(bytesutil.ToBytes48(validatorPubKey.Marshal()), root) + sig, err := validator.keyManager.Sign(validatorPubKey, root) if err != nil { t.Fatal(err) } @@ -141,7 +140,7 @@ func TestAttestToBlockHead_BlocksDoubleAtt(t *testing.T) { committee := []uint64{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ { - PublicKey: validatorPubKey.Marshal(), + PublicKey: validatorKey.PublicKey.Marshal(), CommitteeIndex: 5, Committee: committee, ValidatorIndex: validatorIndex, @@ -165,8 +164,8 @@ func TestAttestToBlockHead_BlocksDoubleAtt(t *testing.T) { gomock.AssignableToTypeOf(ðpb.Attestation{}), ).Return(ðpb.AttestResponse{}, nil /* error */) - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) testutil.AssertLogsContain(t, hook, "Attempted to make a slashable attestation, rejected") } @@ -182,7 +181,7 @@ func TestAttestToBlockHead_BlocksSurroundAtt(t *testing.T) { committee := []uint64{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ { - PublicKey: validatorPubKey.Marshal(), + PublicKey: validatorKey.PublicKey.Marshal(), CommitteeIndex: 5, Committee: committee, ValidatorIndex: validatorIndex, @@ -206,7 +205,7 @@ func TestAttestToBlockHead_BlocksSurroundAtt(t *testing.T) { gomock.AssignableToTypeOf(ðpb.Attestation{}), ).Return(ðpb.AttestResponse{}, nil /* error */) - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) m.validatorClient.EXPECT().GetAttestationData( gomock.Any(), // ctx @@ -217,7 +216,7 @@ func TestAttestToBlockHead_BlocksSurroundAtt(t *testing.T) { Source: ðpb.Checkpoint{Root: []byte("C"), Epoch: 0}, }, nil) - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) testutil.AssertLogsContain(t, hook, "Attempted to make a slashable attestation, rejected") } @@ -233,7 +232,7 @@ func TestAttestToBlockHead_BlocksSurroundedAtt(t *testing.T) { committee := []uint64{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ { - PublicKey: validatorPubKey.Marshal(), + PublicKey: validatorKey.PublicKey.Marshal(), CommitteeIndex: 5, Committee: committee, ValidatorIndex: validatorIndex, @@ -257,7 +256,7 @@ func TestAttestToBlockHead_BlocksSurroundedAtt(t *testing.T) { gomock.AssignableToTypeOf(ðpb.Attestation{}), ).Return(ðpb.AttestResponse{}, nil /* error */) - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) m.validatorClient.EXPECT().GetAttestationData( gomock.Any(), // ctx @@ -268,7 +267,7 @@ func TestAttestToBlockHead_BlocksSurroundedAtt(t *testing.T) { Source: ðpb.Checkpoint{Root: []byte("C"), Epoch: 1}, }, nil) - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) testutil.AssertLogsContain(t, hook, "Attempted to make a slashable attestation, rejected") } @@ -294,7 +293,7 @@ func TestAttestToBlockHead_DoesNotAttestBeforeDelay(t *testing.T) { ).Return(ðpb.AttestResponse{}, nil /* error */).Times(0) timer := time.NewTimer(1 * time.Second) - go validator.SubmitAttestation(context.Background(), 0, bytesutil.ToBytes48(validatorPubKey.Marshal())) + go validator.SubmitAttestation(context.Background(), 0, validatorPubKey) <-timer.C } @@ -311,7 +310,7 @@ func TestAttestToBlockHead_DoesAttestAfterDelay(t *testing.T) { committee := []uint64{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ { - PublicKey: validatorPubKey.Marshal(), + PublicKey: validatorKey.PublicKey.Marshal(), CommitteeIndex: 5, Committee: committee, ValidatorIndex: validatorIndex, @@ -338,7 +337,7 @@ func TestAttestToBlockHead_DoesAttestAfterDelay(t *testing.T) { gomock.Any(), ).Return(ðpb.AttestResponse{}, nil).Times(1) - validator.SubmitAttestation(context.Background(), 0, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 0, validatorPubKey) } func TestAttestToBlockHead_CorrectBitfieldLength(t *testing.T) { @@ -348,7 +347,7 @@ func TestAttestToBlockHead_CorrectBitfieldLength(t *testing.T) { committee := []uint64{0, 3, 4, 2, validatorIndex, 6, 8, 9, 10} validator.duties = ðpb.DutiesResponse{Duties: []*ethpb.DutiesResponse_Duty{ { - PublicKey: validatorPubKey.Marshal(), + PublicKey: validatorKey.PublicKey.Marshal(), CommitteeIndex: 5, Committee: committee, ValidatorIndex: validatorIndex, @@ -374,7 +373,7 @@ func TestAttestToBlockHead_CorrectBitfieldLength(t *testing.T) { generatedAttestation = att }).Return(ðpb.AttestResponse{}, nil /* error */) - validator.SubmitAttestation(context.Background(), 30, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.SubmitAttestation(context.Background(), 30, validatorPubKey) if len(generatedAttestation.AggregationBits) != 2 { t.Errorf("Wanted length %d, received %d", 2, len(generatedAttestation.AggregationBits)) diff --git a/validator/client/validator_propose_test.go b/validator/client/validator_propose_test.go index cf4028532d21..7e04200d4495 100644 --- a/validator/client/validator_propose_test.go +++ b/validator/client/validator_propose_test.go @@ -8,7 +8,6 @@ import ( "github.com/golang/mock/gomock" lru "github.com/hashicorp/golang-lru" ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" - "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/testutil" @@ -22,7 +21,7 @@ type mocks struct { } func setup(t *testing.T) (*validator, *mocks, func()) { - valDB := db.SetupDB(t, [][48]byte{bytesutil.ToBytes48(validatorPubKey.Marshal())}) + valDB := db.SetupDB(t, [][48]byte{validatorPubKey}) ctrl := gomock.NewController(t) m := &mocks{ validatorClient: internal.NewMockBeaconNodeValidatorClient(ctrl), @@ -49,7 +48,7 @@ func TestProposeBlock_DoesNotProposeGenesisBlock(t *testing.T) { hook := logTest.NewGlobal() validator, _, finish := setup(t) defer finish() - validator.ProposeBlock(context.Background(), 0, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), 0, validatorPubKey) testutil.AssertLogsContain(t, hook, "Assigned to genesis slot, skipping proposal") } @@ -64,7 +63,7 @@ func TestProposeBlock_DomainDataFailed(t *testing.T) { gomock.Any(), // epoch ).Return(nil /*response*/, errors.New("uh oh")) - validator.ProposeBlock(context.Background(), 1, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), 1, validatorPubKey) testutil.AssertLogsContain(t, hook, "Failed to sign randao reveal") } @@ -83,7 +82,7 @@ func TestProposeBlock_RequestBlockFailed(t *testing.T) { gomock.Any(), // block request ).Return(nil /*response*/, errors.New("uh oh")) - validator.ProposeBlock(context.Background(), 1, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), 1, validatorPubKey) testutil.AssertLogsContain(t, hook, "Failed to request block from beacon node") } @@ -112,7 +111,7 @@ func TestProposeBlock_ProposeBlockFailed(t *testing.T) { gomock.AssignableToTypeOf(ðpb.SignedBeaconBlock{}), ).Return(nil /*response*/, errors.New("uh oh")) - validator.ProposeBlock(context.Background(), 1, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), 1, validatorPubKey) testutil.AssertLogsContain(t, hook, "Failed to propose block") } @@ -146,10 +145,10 @@ func TestProposeBlock_BlocksDoubleProposal(t *testing.T) { gomock.AssignableToTypeOf(ðpb.SignedBeaconBlock{}), ).Return(ðpb.ProposeResponse{}, nil /*error*/) - validator.ProposeBlock(context.Background(), params.BeaconConfig().SlotsPerEpoch*5+2, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), params.BeaconConfig().SlotsPerEpoch*5+2, validatorPubKey) testutil.AssertLogsDoNotContain(t, hook, "Tried to sign a double proposal") - validator.ProposeBlock(context.Background(), params.BeaconConfig().SlotsPerEpoch*5+2, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), params.BeaconConfig().SlotsPerEpoch*5+2, validatorPubKey) testutil.AssertLogsContain(t, hook, "Tried to sign a double proposal") } @@ -184,10 +183,10 @@ func TestProposeBlock_BlocksDoubleProposal_After54KEpochs(t *testing.T) { ).Return(ðpb.ProposeResponse{}, nil /*error*/) farFuture := (params.BeaconConfig().WeakSubjectivityPeriod + 9) * params.BeaconConfig().SlotsPerEpoch - validator.ProposeBlock(context.Background(), farFuture, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), farFuture, validatorPubKey) testutil.AssertLogsDoNotContain(t, hook, "Tried to sign a double proposal") - validator.ProposeBlock(context.Background(), farFuture, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), farFuture, validatorPubKey) testutil.AssertLogsContain(t, hook, "Tried to sign a double proposal") } @@ -222,11 +221,11 @@ func TestProposeBlock_AllowsPastProposals(t *testing.T) { ).Times(2).Return(ðpb.ProposeResponse{}, nil /*error*/) farAhead := (params.BeaconConfig().WeakSubjectivityPeriod + 9) * params.BeaconConfig().SlotsPerEpoch - validator.ProposeBlock(context.Background(), farAhead, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), farAhead, validatorPubKey) testutil.AssertLogsDoNotContain(t, hook, "Tried to sign a double proposal") past := (params.BeaconConfig().WeakSubjectivityPeriod - 400) * params.BeaconConfig().SlotsPerEpoch - validator.ProposeBlock(context.Background(), past, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), past, validatorPubKey) testutil.AssertLogsDoNotContain(t, hook, "Tried to sign a double proposal") } @@ -260,7 +259,7 @@ func TestProposeBlock_AllowsSameEpoch(t *testing.T) { gomock.AssignableToTypeOf(ðpb.SignedBeaconBlock{}), ).Times(2).Return(ðpb.ProposeResponse{}, nil /*error*/) - pubKey := bytesutil.ToBytes48(validatorPubKey.Marshal()) + pubKey := validatorPubKey farAhead := (params.BeaconConfig().WeakSubjectivityPeriod + 9) * params.BeaconConfig().SlotsPerEpoch validator.ProposeBlock(context.Background(), farAhead, pubKey) testutil.AssertLogsDoNotContain(t, hook, "Tried to sign a double proposal") @@ -293,7 +292,7 @@ func TestProposeBlock_BroadcastsBlock(t *testing.T) { gomock.AssignableToTypeOf(ðpb.SignedBeaconBlock{}), ).Return(ðpb.ProposeResponse{}, nil /*error*/) - validator.ProposeBlock(context.Background(), 1, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), 1, validatorPubKey) } func TestProposeBlock_BroadcastsBlock_WithGraffiti(t *testing.T) { @@ -327,7 +326,7 @@ func TestProposeBlock_BroadcastsBlock_WithGraffiti(t *testing.T) { return ðpb.ProposeResponse{}, nil }) - validator.ProposeBlock(context.Background(), 1, bytesutil.ToBytes48(validatorPubKey.Marshal())) + validator.ProposeBlock(context.Background(), 1, validatorPubKey) if string(sentBlock.Block.Body.Graffiti) != string(validator.graffiti) { t.Errorf("Block was broadcast with the wrong graffiti field, wanted \"%v\", got \"%v\"", string(validator.graffiti), string(sentBlock.Block.Body.Graffiti)) diff --git a/validator/flags/flags.go b/validator/flags/flags.go index b8cc39dc2e19..4f4ab7f65c0e 100644 --- a/validator/flags/flags.go +++ b/validator/flags/flags.go @@ -16,17 +16,22 @@ var ( Name: "tls-cert", Usage: "Certificate for secure gRPC. Pass this and the tls-key flag in order to use gRPC securely.", } - // UnencryptedKeysFlag specifies a file path of a JSON file of unencrypted validator keys; this should only - // be used for test networks. + // KeystorePathFlag defines the location of the keystore directory for a validator's account. + KeystorePathFlag = &cli.StringFlag{ + Name: "keystore-path", + Usage: "Path to the desired keystore directory", + } + // UnencryptedKeysFlag specifies a file path of a JSON file of unencrypted validator keys as an + // alternative from launching the validator client from decrypting a keystore directory. UnencryptedKeysFlag = &cli.StringFlag{ Name: "unencrypted-keys", - Usage: "Filepath to a JSON file of unencrypted validator keys for launching the validator client on test networks", + Usage: "Filepath to a JSON file of unencrypted validator keys for easier launching of the validator client", Value: "", } // KeyManager specifies the key manager to use. KeyManager = &cli.StringFlag{ Name: "keymanager", - Usage: "For specifying the keymanager to use (remote, wallet, unencrypted, interop)", + Usage: "The keymanger to use (unencrypted, interop, keystore, wallet)", Value: "", } // KeyManagerOpts specifies the key manager options. @@ -35,6 +40,11 @@ var ( Usage: "The options for the keymanger, either a JSON string or path to same", Value: "", } + // PasswordFlag defines the password value for storing and retrieving validator private keys from the keystore. + PasswordFlag = &cli.StringFlag{ + Name: "password", + Usage: "String value of the password for your validator private keys", + } // DisablePenaltyRewardLogFlag defines the ability to not log reward/penalty information during deployment DisablePenaltyRewardLogFlag = &cli.BoolFlag{ Name: "disable-rewards-penalties-logging", diff --git a/validator/keymanager/BUILD.bazel b/validator/keymanager/BUILD.bazel index 044a18233e02..65cdb2b70d18 100644 --- a/validator/keymanager/BUILD.bazel +++ b/validator/keymanager/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "direct.go", "direct_interop.go", + "direct_keystore.go", "direct_unencrypted.go", "keymanager.go", "log.go", @@ -18,15 +19,17 @@ go_library( "//shared/bls:go_default_library", "//shared/bytesutil:go_default_library", "//shared/interop:go_default_library", + "//validator/accounts:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_wealdtech_eth2_signer_api//pb/v1:go_default_library", "@com_github_wealdtech_go_eth2_wallet//:go_default_library", "@com_github_wealdtech_go_eth2_wallet_store_filesystem//:go_default_library", - "@com_github_wealdtech_go_eth2_wallet_types_v2//:go_default_library", + "@com_github_wealdtech_go_eth2_wallet_types//:go_default_library", "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//credentials:go_default_library", + "@org_golang_x_crypto//ssh/terminal:go_default_library", ], ) @@ -36,7 +39,6 @@ go_test( "direct_interop_test.go", "direct_test.go", "opts_test.go", - "remote_internal_test.go", "remote_test.go", "wallet_test.go", ], @@ -46,7 +48,7 @@ go_test( "//shared/bytesutil:go_default_library", "//shared/testutil:go_default_library", "@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library", - "@com_github_wealdtech_go_eth2_wallet_nd_v2//:go_default_library", + "@com_github_wealdtech_go_eth2_wallet_nd//:go_default_library", "@com_github_wealdtech_go_eth2_wallet_store_filesystem//:go_default_library", ], ) diff --git a/validator/keymanager/direct_keystore.go b/validator/keymanager/direct_keystore.go new file mode 100644 index 000000000000..cd9a67cbe2d6 --- /dev/null +++ b/validator/keymanager/direct_keystore.go @@ -0,0 +1,121 @@ +package keymanager + +import ( + "encoding/json" + "os" + "os/user" + "path/filepath" + "runtime" + "strings" + + "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/validator/accounts" + "golang.org/x/crypto/ssh/terminal" +) + +// Keystore is a key manager that loads keys from a standard keystore. +type Keystore struct { + *Direct +} + +type keystoreOpts struct { + Path string `json:"path"` + Passphrase string `json:"passphrase"` +} + +var keystoreOptsHelp = `The keystore key manager generates keys and stores them in a local encrypted store. The options are: + - path This is the filesystem path to where keys will be stored. Defaults to the user's home directory if not supplied + - passphrase This is the passphrase used to encrypt keys. Will be asked for if not supplied +A sample set of options are: + { + "path": "/home/me/keys", // Store the keys in '/home/me/keys' + "passphrase": "secret" // Use the passphrase 'secret' to encrypt and decrypt keys + }` + +// NewKeystore creates a key manager populated with the keys from the keystore at the given path. +func NewKeystore(input string) (KeyManager, string, error) { + opts := &keystoreOpts{} + err := json.Unmarshal([]byte(input), opts) + if err != nil { + return nil, keystoreOptsHelp, err + } + + if strings.Contains(opts.Path, "$") || strings.Contains(opts.Path, "~") || strings.Contains(opts.Path, "%") { + log.WithField("path", opts.Path).Warn("Keystore path contains unexpanded shell expansion characters") + } + + if opts.Path == "" { + opts.Path = defaultValidatorDir() + } + + exists, err := accounts.Exists(opts.Path) + if err != nil { + return nil, keystoreOptsHelp, err + } + if !exists { + // If an account does not exist, we create a new one and start the node. + opts.Path, opts.Passphrase, err = accounts.CreateValidatorAccount(opts.Path, opts.Passphrase) + if err != nil { + return nil, keystoreOptsHelp, err + } + } else { + if opts.Passphrase == "" { + log.Info("Enter your validator account password:") + bytePassword, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return nil, keystoreOptsHelp, err + } + text := string(bytePassword) + opts.Passphrase = strings.Replace(text, "\n", "", -1) + } + + if err := accounts.VerifyAccountNotExists(opts.Path, opts.Passphrase); err == nil { + log.Info("No account found, creating new validator account...") + } + } + + keyMap, err := accounts.DecryptKeysFromKeystore(opts.Path, opts.Passphrase) + if err != nil { + return nil, keystoreOptsHelp, err + } + + km := &Unencrypted{ + Direct: &Direct{ + publicKeys: make(map[[48]byte]*bls.PublicKey), + secretKeys: make(map[[48]byte]*bls.SecretKey), + }, + } + for _, key := range keyMap { + pubKey := bytesutil.ToBytes48(key.PublicKey.Marshal()) + km.publicKeys[pubKey] = key.PublicKey + km.secretKeys[pubKey] = key.SecretKey + } + return km, "", nil +} + +func homeDir() string { + if home := os.Getenv("HOME"); home != "" { + return home + } + if usr, err := user.Current(); err == nil { + return usr.HomeDir + } + return "" +} + +func defaultValidatorDir() string { + // Try to place the data folder in the user's home dir + home := homeDir() + if home != "" { + if runtime.GOOS == "darwin" { + return filepath.Join(home, "Library", "Eth2Validators") + } else if runtime.GOOS == "windows" { + return filepath.Join(home, "AppData", "Roaming", "Eth2Validators") + } else { + return filepath.Join(home, ".eth2validators") + } + } + // As we cannot guess a stable location, return empty and handle later + return "" +} diff --git a/validator/keymanager/remote.go b/validator/keymanager/remote.go index 3d8f571643f8..675a0acbb093 100644 --- a/validator/keymanager/remote.go +++ b/validator/keymanager/remote.go @@ -5,10 +5,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/json" - "fmt" "io/ioutil" - "regexp" - "strings" "github.com/pkg/errors" ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" @@ -19,11 +16,6 @@ import ( "google.golang.org/grpc/credentials" ) -const ( - // maxMessageSize is the largest message that can be received over GRPC. Set to 8MB, which handles ~128K keys. - maxMessageSize = 8 * 1024 * 1024 -) - // Remote is a key manager that accesses a remote wallet daemon. type Remote struct { paths []string @@ -123,8 +115,6 @@ func NewRemoteWallet(input string) (KeyManager, string, error) { grpcOpts := []grpc.DialOption{ // Require TLS with client certificate. grpc.WithTransportCredentials(clientCreds), - // Receive large messages without erroring. - grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMessageSize)), } conn, err := grpc.Dial(opts.Location, grpcOpts...) @@ -177,9 +167,9 @@ func (km *Remote) SignGeneric(pubKey [48]byte, root [32]byte, domain [32]byte) ( return nil, err } switch resp.State { - case pb.ResponseState_DENIED: + case pb.SignState_DENIED: return nil, ErrDenied - case pb.ResponseState_FAILED: + case pb.SignState_FAILED: return nil, ErrCannotSign } return bls.SignatureFromBytes(resp.Signature) @@ -208,9 +198,9 @@ func (km *Remote) SignProposal(pubKey [48]byte, domain [32]byte, data *ethpb.Bea return nil, err } switch resp.State { - case pb.ResponseState_DENIED: + case pb.SignState_DENIED: return nil, ErrDenied - case pb.ResponseState_FAILED: + case pb.SignState_FAILED: return nil, ErrCannotSign } return bls.SignatureFromBytes(resp.Signature) @@ -246,9 +236,9 @@ func (km *Remote) SignAttestation(pubKey [48]byte, domain [32]byte, data *ethpb. return nil, err } switch resp.State { - case pb.ResponseState_DENIED: + case pb.SignState_DENIED: return nil, ErrDenied - case pb.ResponseState_FAILED: + case pb.SignState_FAILED: return nil, ErrCannotSign } return bls.SignatureFromBytes(resp.Signature) @@ -260,30 +250,12 @@ func (km *Remote) RefreshValidatingKeys() error { listAccountsReq := &pb.ListAccountsRequest{ Paths: km.paths, } - resp, err := listerClient.ListAccounts(context.Background(), listAccountsReq) + accountsResp, err := listerClient.ListAccounts(context.Background(), listAccountsReq) if err != nil { - return err - } - if resp.State == pb.ResponseState_DENIED { - return errors.New("attempt to fetch keys denied") - } - if resp.State == pb.ResponseState_FAILED { - return errors.New("attempt to fetch keys failed") + panic(err) } - verificationRegexes := pathsToVerificationRegexes(km.paths) - accounts := make(map[[48]byte]*accountInfo, len(resp.Accounts)) - for _, account := range resp.Accounts { - verified := false - for _, verificationRegex := range verificationRegexes { - if verificationRegex.Match([]byte(account.Name)) { - verified = true - break - } - } - if !verified { - log.WithField("path", account.Name).Warn("Received unwanted account from server; ignoring") - continue - } + accounts := make(map[[48]byte]*accountInfo, len(accountsResp.Accounts)) + for _, account := range accountsResp.Accounts { account := &accountInfo{ Name: account.Name, PubKey: account.PublicKey, @@ -293,35 +265,3 @@ func (km *Remote) RefreshValidatingKeys() error { km.accounts = accounts return nil } - -// pathsToVerificationRegexes turns path specifiers in to regexes to ensure accounts we are given are good. -func pathsToVerificationRegexes(paths []string) []*regexp.Regexp { - regexes := make([]*regexp.Regexp, 0, len(paths)) - for _, path := range paths { - log := log.WithField("path", path) - parts := strings.Split(path, "/") - if len(parts) == 0 || len(parts[0]) == 0 { - log.Debug("Invalid path") - continue - } - if len(parts) == 1 { - parts = append(parts, ".*") - } - if strings.HasPrefix(parts[1], "^") { - parts[1] = parts[1][1:] - } - var specifier string - if strings.HasSuffix(parts[1], "$") { - specifier = fmt.Sprintf("^%s/%s", parts[0], parts[1]) - } else { - specifier = fmt.Sprintf("^%s/%s$", parts[0], parts[1]) - } - regex, err := regexp.Compile(specifier) - if err != nil { - log.WithField("specifier", specifier).WithError(err).Warn("Invalid path regex") - continue - } - regexes = append(regexes, regex) - } - return regexes -} diff --git a/validator/keymanager/remote_internal_test.go b/validator/keymanager/remote_internal_test.go deleted file mode 100644 index 9d6a4dc4690e..000000000000 --- a/validator/keymanager/remote_internal_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package keymanager - -import ( - "testing" -) - -func TestPathsToVerificationRegexes(t *testing.T) { - tests := []struct { - name string - paths []string - regexes []string - err string - }{ - { - name: "Empty", - regexes: []string{}, - }, - { - name: "IgnoreBadPaths", - paths: []string{"", "/", "/Account"}, - regexes: []string{}, - }, - { - name: "Simple", - paths: []string{"Wallet/Account"}, - regexes: []string{"^Wallet/Account$"}, - }, - { - name: "Multiple", - paths: []string{"Wallet/Account1", "Wallet/Account2"}, - regexes: []string{"^Wallet/Account1$", "^Wallet/Account2$"}, - }, - { - name: "IgnoreInvalidRegex", - paths: []string{"Wallet/Account1", "Bad/***", "Wallet/Account2"}, - regexes: []string{"^Wallet/Account1$", "^Wallet/Account2$"}, - }, - { - name: "TidyExistingAnchors", - paths: []string{"Wallet/^.*$", "Wallet/Foo.*Bar$", "Wallet/^Account"}, - regexes: []string{"^Wallet/.*$", "^Wallet/Foo.*Bar$", "^Wallet/Account$"}, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - regexes := pathsToVerificationRegexes(test.paths) - if len(regexes) != len(test.regexes) { - t.Fatalf("Unexpected number of regexes: expected %v, received %v", len(test.regexes), len(regexes)) - } - for i := range regexes { - if regexes[i].String() != test.regexes[i] { - t.Fatalf("Unexpected regex %d: expected %v, received %v", i, test.regexes[i], regexes[i].String()) - } - } - }) - } -} diff --git a/validator/keymanager/wallet.go b/validator/keymanager/wallet.go index 68ccad83424b..471cfdcc2e44 100644 --- a/validator/keymanager/wallet.go +++ b/validator/keymanager/wallet.go @@ -11,7 +11,7 @@ import ( "github.com/prysmaticlabs/prysm/shared/bytesutil" e2wallet "github.com/wealdtech/go-eth2-wallet" filesystem "github.com/wealdtech/go-eth2-wallet-store-filesystem" - e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types" ) type walletOpts struct { @@ -127,7 +127,8 @@ func (km *Wallet) Sign(pubKey [48]byte, root [32]byte) (*bls.Signature, error) { if !exists { return nil, ErrNoSuchKey } - sig, err := account.Sign(root[:]) + // TODO(#4817) Update with new library to remove domain here. + sig, err := account.Sign(root[:], 0) if err != nil { return nil, err } diff --git a/validator/main.go b/validator/main.go index fef024dfb9bc..d38f12585ac8 100644 --- a/validator/main.go +++ b/validator/main.go @@ -11,7 +11,9 @@ import ( "github.com/prysmaticlabs/prysm/shared/debug" "github.com/prysmaticlabs/prysm/shared/featureconfig" "github.com/prysmaticlabs/prysm/shared/logutil" + "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/version" + "github.com/prysmaticlabs/prysm/validator/accounts" "github.com/prysmaticlabs/prysm/validator/flags" "github.com/prysmaticlabs/prysm/validator/node" "github.com/sirupsen/logrus" @@ -36,6 +38,8 @@ var appFlags = []cli.Flag{ flags.BeaconRPCProviderFlag, flags.CertFlag, flags.GraffitiFlag, + flags.KeystorePathFlag, + flags.PasswordFlag, flags.DisablePenaltyRewardLogFlag, flags.UnencryptedKeysFlag, flags.InteropStartIndex, @@ -77,6 +81,61 @@ func main() { starts proposer services, shardp2p connections, and more` app.Version = version.GetVersion() app.Action = startNode + app.Commands = []*cli.Command{ + { + Name: "accounts", + Category: "accounts", + Usage: "defines useful functions for interacting with the validator client's account", + Subcommands: []*cli.Command{ + { + Name: "create", + Description: `creates a new validator account keystore containing private keys for Ethereum Serenity - +this command outputs a deposit data string which can be used to deposit Ether into the ETH1.0 deposit +contract in order to activate the validator client`, + Flags: []cli.Flag{ + flags.KeystorePathFlag, + flags.PasswordFlag, + }, + Action: func(ctx *cli.Context) error { + featureconfig.ConfigureValidator(ctx) + if featureconfig.Get().MinimalConfig { + log.Warn("Using Minimal Config") + params.UseMinimalConfig() + } + + if keystoreDir, _, err := accounts.CreateValidatorAccount(ctx.String(flags.KeystorePathFlag.Name), ctx.String(flags.PasswordFlag.Name)); err != nil { + log.WithError(err).Fatalf("Could not create validator at path: %s", keystoreDir) + } + return nil + }, + }, + { + Name: "keys", + Description: `lists the private keys for 'keystore' keymanager keys`, + Flags: []cli.Flag{ + flags.KeystorePathFlag, + flags.PasswordFlag, + }, + Action: func(ctx *cli.Context) error { + if ctx.String(flags.KeystorePathFlag.Name) == "" { + log.Fatalf("%s is required", flags.KeystorePathFlag.Name) + } + if ctx.String(flags.PasswordFlag.Name) == "" { + log.Fatalf("%s is required", flags.PasswordFlag.Name) + } + keystores, err := accounts.DecryptKeysFromKeystore(ctx.String(flags.KeystorePathFlag.Name), ctx.String(flags.PasswordFlag.Name)) + if err != nil { + log.WithError(err).Fatalf("Failed to decrypt keystore keys at path %s", ctx.String(flags.KeystorePathFlag.Name)) + } + for _, v := range keystores { + fmt.Printf("Public key: %#x private key: %#x\n", v.PublicKey.Marshal(), v.SecretKey.Marshal()) + } + return nil + }, + }, + }, + }, + } app.Flags = appFlags app.Before = func(ctx *cli.Context) error { diff --git a/validator/node/BUILD.bazel b/validator/node/BUILD.bazel index e2efb2231eac..4551639f544d 100644 --- a/validator/node/BUILD.bazel +++ b/validator/node/BUILD.bazel @@ -7,6 +7,7 @@ go_test( embed = [":go_default_library"], deps = [ "//shared/testutil:go_default_library", + "//validator/accounts:go_default_library", "@in_gopkg_urfave_cli_v2//:go_default_library", ], ) diff --git a/validator/node/node.go b/validator/node/node.go index a4ad499cce3c..be34e4023d86 100644 --- a/validator/node/node.go +++ b/validator/node/node.go @@ -219,25 +219,37 @@ func selectKeyManager(ctx *cli.Context) (keymanager.KeyManager, error) { manager = "interop" opts = fmt.Sprintf(`{"keys":%d,"offset":%d}`, numValidatorKeys, ctx.Uint64(flags.InteropStartIndex.Name)) log.Warn(fmt.Sprintf("--interop-num-validators and --interop-start-index flags are deprecated. Please use --keymanager=interop --keymanageropts='%s'", opts)) + } else if keystorePath := ctx.String(flags.KeystorePathFlag.Name); keystorePath != "" { + manager = "keystore" + opts = fmt.Sprintf(`{"path":%q,"passphrase":%q}`, keystorePath, ctx.String(flags.PasswordFlag.Name)) + log.Warn(fmt.Sprintf("--keystore-path flag is deprecated. Please use --keymanager=keystore --keymanageropts='%s'", opts)) + } else { + // Default if no choice made + manager = "keystore" + passphrase := ctx.String(flags.PasswordFlag.Name) + if passphrase == "" { + log.Warn("Implicit selection of keymanager is deprecated. Please use --keymanager=keystore or select a different keymanager") + } else { + opts = fmt.Sprintf(`{"passphrase":%q}`, passphrase) + log.Warn(`Implicit selection of keymanager is deprecated. Please use --keymanager=keystore --keymanageropts='{"passphrase":""}' or select a different keymanager`) + } } } - if manager == "" { - return nil, fmt.Errorf("please supply a keymanager with --keymanager") - } - var km keymanager.KeyManager var help string var err error switch manager { - case "remote": - km, help, err = keymanager.NewRemoteWallet(opts) - case "wallet": - km, help, err = keymanager.NewWallet(opts) case "interop": km, help, err = keymanager.NewInterop(opts) case "unencrypted": km, help, err = keymanager.NewUnencrypted(opts) + case "keystore": + km, help, err = keymanager.NewKeystore(opts) + case "wallet": + km, help, err = keymanager.NewWallet(opts) + case "remote": + km, help, err = keymanager.NewRemoteWallet(opts) default: return nil, fmt.Errorf("unknown keymanager %q", manager) } diff --git a/validator/node/node_test.go b/validator/node/node_test.go index dffc88d6cc70..3bb47eed3020 100644 --- a/validator/node/node_test.go +++ b/validator/node/node_test.go @@ -3,10 +3,10 @@ package node import ( "flag" "os" - "path/filepath" "testing" "github.com/prysmaticlabs/prysm/shared/testutil" + "github.com/prysmaticlabs/prysm/validator/accounts" "gopkg.in/urfave/cli.v2" ) @@ -14,14 +14,18 @@ import ( func TestNode_Builds(t *testing.T) { app := cli.App{} set := flag.NewFlagSet("test", 0) - tmpDir := testutil.TempDir() - defer os.RemoveAll(tmpDir) - set.String("datadir", filepath.Join(tmpDir, "datadir"), "the node data directory") - set.String("keymanager", "interop", "key manager") - set.String("keymanageropts", `{"keys":16,"offset":0}`, `key manager options`) + set.String("datadir", testutil.TempDir()+"/datadir", "the node data directory") + dir := testutil.TempDir() + "/keystore1" + defer os.RemoveAll(dir) + defer os.RemoveAll(testutil.TempDir() + "/datadir") + set.String("keystore-path", dir, "path to keystore") + set.String("password", "1234", "validator account password") set.String("verbosity", "debug", "log verbosity") context := cli.NewContext(&app, set, nil) + if err := accounts.NewValidatorAccount(dir, "1234"); err != nil { + t.Fatalf("Could not create validator account: %v", err) + } _, err := NewValidatorClient(context) if err != nil { t.Fatalf("Failed to create ValidatorClient: %v", err) diff --git a/validator/usage.go b/validator/usage.go index 11f7b9678bbe..244ce9b586a4 100644 --- a/validator/usage.go +++ b/validator/usage.go @@ -76,6 +76,8 @@ var appHelpFlagGroups = []flagGroup{ flags.CertFlag, flags.KeyManager, flags.KeyManagerOpts, + flags.KeystorePathFlag, + flags.PasswordFlag, flags.DisablePenaltyRewardLogFlag, flags.UnencryptedKeysFlag, flags.GraffitiFlag,