From 648584b3561c3b08d1b2d78bf3d7feaa11e30689 Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Mon, 3 Feb 2020 17:13:58 +0000 Subject: [PATCH] Add wallet keymanager (#4687) * Add wallet keymanager * Read keymanageropts from file if not JSON Co-authored-by: Nishant Das Co-authored-by: Raul Jordan --- WORKSPACE | 78 ++++++++++++++++++++ validator/keymanager/BUILD.bazel | 3 + validator/keymanager/keymanager.go | 3 + validator/keymanager/wallet.go | 110 +++++++++++++++++++++++++++++ validator/node/node.go | 18 ++++- 5 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 validator/keymanager/wallet.go diff --git a/WORKSPACE b/WORKSPACE index a9047664c8f..7032925d28d 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1486,3 +1486,81 @@ go_repository( sum = "h1:oq6BiN7v0MfWCRcJAxSV+hesVMAAV8COrQbTjYNnso4=", version = "v0.0.0-20190611015032-8a3d0352aa79", ) + +go_repository( + name = "com_github_wealdtech_go_eth2_wallet", + commit = "6970d62e60d86fdae3c3e510e800e8a60d755a7d", + importpath = "github.com/wealdtech/go-eth2-wallet", +) + +go_repository( + 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", + 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", +) + +go_repository( + name = "com_github_wealdtech_go_eth2_wallet_store_s3", + commit = "1c821b5161f7bb0b3efa2030eff687eea5e70e53", + importpath = "github.com/wealdtech/go-eth2-wallet-store-s3", +) + +go_repository( + name = "com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4", + commit = "0c11c07b9544eb662210fadded94f40f309d8c8f", + importpath = "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4", +) + +go_repository( + 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", + 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", +) + +go_repository( + name = "com_github_wealdtech_go_ecodec", + commit = "7473d835445a3490e61a5fcf48fe4e9755a37957", + importpath = "github.com/wealdtech/go-ecodec", +) + +go_repository( + name = "com_github_wealdtech_go_bytesutil", + commit = "e564d0ade555b9f97494f0f669196ddcc6bc531d", + importpath = "github.com/wealdtech/go-bytesutil", +) + +go_repository( + name = "com_github_wealdtech_go_indexer", + commit = "334862c32b1e3a5c6738a2618f5c0a8ebeb8cd51", + importpath = "github.com/wealdtech/go-indexer", +) + +go_repository( + name = "com_github_shibukawa_configdir", + commit = "e180dbdc8da04c4fa04272e875ce64949f38bd3e", + importpath = "github.com/shibukawa/configdir", +) diff --git a/validator/keymanager/BUILD.bazel b/validator/keymanager/BUILD.bazel index ad36cd38170..807a1dff4f3 100644 --- a/validator/keymanager/BUILD.bazel +++ b/validator/keymanager/BUILD.bazel @@ -10,6 +10,7 @@ go_library( "keymanager.go", "log.go", "opts.go", + "wallet.go", ], importpath = "github.com/prysmaticlabs/prysm/validator/keymanager", visibility = ["//validator:__subpackages__"], @@ -19,6 +20,8 @@ go_library( "//shared/interop:go_default_library", "//validator/accounts:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", + "@com_github_wealdtech_go_eth2_wallet//:go_default_library", + "@com_github_wealdtech_go_eth2_wallet_types//:go_default_library", "@org_golang_x_crypto//ssh/terminal:go_default_library", ], ) diff --git a/validator/keymanager/keymanager.go b/validator/keymanager/keymanager.go index 79fa18a9074..2b9b027fb50 100644 --- a/validator/keymanager/keymanager.go +++ b/validator/keymanager/keymanager.go @@ -9,6 +9,9 @@ import ( // ErrNoSuchKey is returned whenever a request is made for a key of which a key manager is unaware. var ErrNoSuchKey = errors.New("no such key") +// ErrCannotSign is returned whenever a signing attempt fails. +var ErrCannotSign = errors.New("cannot sign") + // KeyManager controls access to private keys by the validator. type KeyManager interface { // FetchValidatingKeys fetches the list of public keys that should be used to validate with. diff --git a/validator/keymanager/wallet.go b/validator/keymanager/wallet.go new file mode 100644 index 00000000000..b5f432730c9 --- /dev/null +++ b/validator/keymanager/wallet.go @@ -0,0 +1,110 @@ +package keymanager + +import ( + "encoding/json" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/bytesutil" + e2wallet "github.com/wealdtech/go-eth2-wallet" + e2wtypes "github.com/wealdtech/go-eth2-wallet-types" +) + +type walletOpts struct { + Accounts []string `json:"accounts"` + Passphrases []string `json:"passphrases"` +} + +var walletOptsHelp = `The wallet key manager stores keys in a local encrypted store. The options are: + - accounts This is a list of account specifiers. An account specifier is of + the form /[account name], where the account name can be a + regular expression. If the account specifier is just all + accounts in that wallet will be used. Multiple account specifiers can be + supplied if required. + - passphrase This is the passphrase used to encrypt the accounts when they + were created. Multiple passphrases can be supplied if required. + { + "accounts": ["Validators/Account.*"], // Use all accounts in the 'Validators' wallet starting with 'Account' + "passphrases": ["secret1","secret2"] // Use the passphrases 'secret1' and 'secret2' to decrypt accounts + }` + +// NewWallet creates a key manager populated with the keys from a wallet at the given path. +func NewWallet(input string) (KeyManager, string, error) { + opts := &walletOpts{} + err := json.Unmarshal([]byte(input), opts) + if err != nil { + return nil, walletOptsHelp, err + } + + if len(opts.Accounts) == 0 { + return nil, walletOptsHelp, errors.New("at least one account specifier is required") + } + + if len(opts.Passphrases) == 0 { + return nil, walletOptsHelp, errors.New("at least one passphrase is required to decrypt accounts") + } + + km := &Wallet{ + accounts: make(map[[48]byte]e2wtypes.Account), + } + + for _, path := range opts.Accounts { + parts := strings.Split(path, "/") + if len(parts[0]) == 0 { + return nil, walletOptsHelp, fmt.Errorf("did not understand account specifier %q", path) + } + wallet, err := e2wallet.OpenWallet(parts[0]) + if err != nil { + return nil, walletOptsHelp, err + } + accountSpecifier := "^.*$" + if len(parts) > 1 && len(parts[1]) > 0 { + accountSpecifier = fmt.Sprintf("^%s$", parts[1]) + } + re := regexp.MustCompile(accountSpecifier) + for account := range wallet.Accounts() { + if re.Match([]byte(account.Name())) { + pubKey := bytesutil.ToBytes48(account.PublicKey().Marshal()) + for _, passphrase := range opts.Passphrases { + if err := account.Unlock([]byte(passphrase)); err != nil { + log.WithError(err).WithField("pubKey", fmt.Sprintf("%#x", pubKey)).Warn("Failed to unlock account with supplied passphrases; cannot validate") + } else { + km.accounts[pubKey] = account + } + } + } + } + } + + return km, walletOptsHelp, nil +} + +// Wallet is a key manager that loads keys from a local Ethereum 2 wallet. +type Wallet struct { + accounts map[[48]byte]e2wtypes.Account +} + +// FetchValidatingKeys fetches the list of public keys that should be used to validate with. +func (km *Wallet) FetchValidatingKeys() ([][48]byte, error) { + res := make([][48]byte, 0, len(km.accounts)) + for pubKey := range km.accounts { + res = append(res, pubKey) + } + return res, nil +} + +// Sign signs a message for the validator to broadcast. +func (km *Wallet) Sign(pubKey [48]byte, root [32]byte, domain uint64) (*bls.Signature, error) { + account, exists := km.accounts[pubKey] + if !exists { + return nil, ErrNoSuchKey + } + sig, err := account.Sign(root[:], domain) + if err != nil { + return nil, err + } + return bls.SignatureFromBytes(sig.Marshal()) +} diff --git a/validator/node/node.go b/validator/node/node.go index 182845bbf93..c6785b40bb8 100644 --- a/validator/node/node.go +++ b/validator/node/node.go @@ -5,6 +5,7 @@ package node import ( "context" "fmt" + "io/ioutil" "os" "os/signal" "strings" @@ -88,8 +89,13 @@ func NewValidatorClient(ctx *cli.Context) (*ValidatorClient, error) { if err != nil { log.WithError(err).Error("Failed to obtain public keys for validation") } else { - for _, key := range pubKeys { - log.WithField("pubKey", fmt.Sprintf("%#x", key)).Info("Validating for public key") + if len(pubKeys) == 0 { + log.Warn("No keys found; nothing to validate") + } else { + log.WithField("validators", len(pubKeys)).Info("Found validator keys") + for _, key := range pubKeys { + log.WithField("pubKey", fmt.Sprintf("%#x", key)).Info("Validating for public key") + } } } @@ -199,6 +205,12 @@ func selectKeyManager(ctx *cli.Context) (keymanager.KeyManager, error) { opts := ctx.String(flags.KeyManagerOpts.Name) if opts == "" { opts = "{}" + } else if !strings.HasPrefix(opts, "{") { + fileopts, err := ioutil.ReadFile(opts) + if err != nil { + return nil, errors.Wrap(err, "Failed to read keymanager options file") + } + opts = string(fileopts) } if manager == "" { @@ -238,6 +250,8 @@ func selectKeyManager(ctx *cli.Context) (keymanager.KeyManager, error) { km, help, err = keymanager.NewUnencrypted(opts) case "keystore": km, help, err = keymanager.NewKeystore(opts) + case "wallet": + km, help, err = keymanager.NewWallet(opts) default: return nil, fmt.Errorf("unknown keymanager %q", manager) }