prysm-pulse/validator/accounts/v2/wallet.go
Raul Jordan ecbab20bad
Begin Dynamic Rescan of Validating Keys (#6963)
* begin on dynamic key rescan
* Merge branch 'master' into dynamic-rescan
* begin dynamic rescan
* fsnotify to listen for rescan dir changes
* recheck for slashing protection
* lint
* Merge branch 'master' into dynamic-rescan
* less aggressive recheck interval
* imports
* Merge branch 'dynamic-rescan' of github.com:prysmaticlabs/prysm into dynamic-rescan
* resolve confs
* listen for file changes for accounts file
* reload accounts from keystore
* begin fixing rescan test
* add event feed
* fix confs
* fix conf
* fix broken tests
* Merge branch 'master' into dynamic-rescan
* simplify lines
* do nothing if no subscribers
* Merge branch 'dynamic-rescan' of github.com:prysmaticlabs/prysm into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* fix tests
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* gaz
* Merge branch 'dynamic-rescan' of github.com:prysmaticlabs/prysm into dynamic-rescan
* ident
* Update WORKSPACE
* gaz
* Merge branch 'dynamic-rescan' of github.com:prysmaticlabs/prysm into dynamic-rescan
* add keys on service start
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* ensure debounce util works
* Merge branch 'dynamic-rescan' of github.com:prysmaticlabs/prysm into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* complete refresh, debounce test, and ensure works at runtime
* Merge branch 'dynamic-rescan' of github.com:prysmaticlabs/prysm into dynamic-rescan
* imports and remove log
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* Merge refs/heads/master into dynamic-rescan
* resolve confs
* fix up e2e tests
* Merge branch 'dynamic-rescan' of github.com:prysmaticlabs/prysm into dynamic-rescan
* fix up e2e
* Merge refs/heads/master into dynamic-rescan
2020-08-31 23:38:20 +00:00

455 lines
15 KiB
Go

package v2
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/gofrs/flock"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/shared/event"
"github.com/prysmaticlabs/prysm/shared/fileutil"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/promptutil"
"github.com/prysmaticlabs/prysm/validator/flags"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/derived"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/remote"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
const (
// KeymanagerConfigFileName for the keymanager used by the wallet: direct, derived, or remote.
KeymanagerConfigFileName = "keymanageropts.json"
// DirectoryPermissions for directories created under the wallet path.
DirectoryPermissions = os.ModePerm
newWalletPasswordPromptText = "New wallet password"
walletPasswordPromptText = "Wallet password"
confirmPasswordPromptText = "Confirm password"
)
var (
// ErrNoWalletFound signifies there was no wallet directory found on-disk.
ErrNoWalletFound = errors.New(
"no wallet found at path, please create a new wallet using `./prysm.sh validator wallet-v2 create`",
)
// ErrWalletExists is an error returned when a wallet already exists in the path provided.
ErrWalletExists = errors.New("you already have a wallet at the specified path. You can " +
"edit your wallet configuration by running ./prysm.sh validator wallet-v2 edit-config",
)
keymanagerKindSelections = map[v2keymanager.Kind]string{
v2keymanager.Derived: "HD Wallet (Recommended)",
v2keymanager.Direct: "Non-HD Wallet (Most Basic)",
v2keymanager.Remote: "Remote Signing Wallet (Advanced)",
}
validateExistingPass = func(input string) error {
if input == "" {
return errors.New("password input cannot be empty")
}
return nil
}
)
// WalletConfig to open a wallet programmatically.
type WalletConfig struct {
WalletDir string
KeymanagerKind v2keymanager.Kind
WalletPassword string
}
// Wallet is a primitive in Prysm's v2 account management which
// has the capability of creating new accounts, reading existing accounts,
// and providing secure access to eth2 secrets depending on an
// associated keymanager (either direct, derived, or remote signing enabled).
type Wallet struct {
walletDir string
accountsPath string
configFilePath string
walletPassword string
walletFileLock *flock.Flock
keymanagerKind v2keymanager.Kind
accountsChangedFeed *event.Feed
}
// WalletExists check if a wallet at the specified directory
// exists and has valid information in it.
func WalletExists(walletDir string) error {
ok, err := fileutil.HasDir(walletDir)
if err != nil {
return errors.Wrap(err, "could not parse wallet directory")
}
if ok {
isEmptyWallet, err := isEmptyWallet(walletDir)
if err != nil {
return errors.Wrap(err, "could not check if wallet has files")
}
if isEmptyWallet {
return ErrNoWalletFound
}
return nil
}
return ErrNoWalletFound
}
// OpenWalletOrElseCli tries to open the wallet and if it fails or no wallet
// is found, invokes a callback function.
func OpenWalletOrElseCli(cliCtx *cli.Context, otherwise func(cliCtx *cli.Context) (*Wallet, error)) (*Wallet, error) {
if err := WalletExists(cliCtx.String(flags.WalletDirFlag.Name)); err != nil {
if errors.Is(err, ErrNoWalletFound) {
return otherwise(cliCtx)
}
return nil, errors.Wrap(err, "could not check if wallet exists")
}
walletDir, err := inputDirectory(cliCtx, walletDirPromptText, flags.WalletDirFlag)
if err != nil {
return nil, err
}
walletPassword, err := inputPassword(
cliCtx,
flags.WalletPasswordFileFlag,
walletPasswordPromptText,
false, /* Do not confirm password */
validateExistingPass,
)
return OpenWallet(cliCtx.Context, &WalletConfig{
WalletDir: walletDir,
WalletPassword: walletPassword,
})
}
// OpenWallet instantiates a wallet from a specified path. It checks the
// type of keymanager associated with the wallet by reading files in the wallet
// path, if applicable. If a wallet does not exist, returns an appropriate error.
func OpenWallet(ctx context.Context, cfg *WalletConfig) (*Wallet, error) {
if err := WalletExists(cfg.WalletDir); err != nil {
if errors.Is(err, ErrNoWalletFound) {
return nil, ErrNoWalletFound
}
return nil, errors.Wrap(err, "could not check if wallet exists")
}
keymanagerKind, err := readKeymanagerKindFromWalletPath(cfg.WalletDir)
if err != nil {
return nil, errors.Wrap(err, "could not read keymanager kind for wallet")
}
walletPath := filepath.Join(cfg.WalletDir, keymanagerKind.String())
return &Wallet{
walletDir: cfg.WalletDir,
accountsPath: walletPath,
keymanagerKind: keymanagerKind,
walletPassword: cfg.WalletPassword,
}, nil
}
// SaveWallet persists the wallet's directories to disk.
func (w *Wallet) SaveWallet() error {
if err := os.MkdirAll(w.accountsPath, DirectoryPermissions); err != nil {
return errors.Wrap(err, "could not create wallet directory")
}
return nil
}
// KeymanagerKind used by the wallet.
func (w *Wallet) KeymanagerKind() v2keymanager.Kind {
return w.keymanagerKind
}
// AccountsDir for the wallet.
func (w *Wallet) AccountsDir() string {
return w.accountsPath
}
// Password for the wallet.
func (w *Wallet) Password() string {
return w.walletPassword
}
// InitializeKeymanager reads a keymanager config from disk at the wallet path,
// unmarshals it based on the wallet's keymanager kind, and returns its value.
func (w *Wallet) InitializeKeymanager(
ctx context.Context,
skipMnemonicConfirm bool,
) (v2keymanager.IKeymanager, error) {
configFile, err := w.ReadKeymanagerConfigFromDisk(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not read keymanager config")
}
var keymanager v2keymanager.IKeymanager
switch w.KeymanagerKind() {
case v2keymanager.Direct:
opts, err := direct.UnmarshalOptionsFile(configFile)
if err != nil {
return nil, errors.Wrap(err, "could not unmarshal keymanageropts file")
}
keymanager, err = direct.NewKeymanager(ctx, &direct.SetupConfig{
Wallet: w,
Opts: opts,
})
if err != nil {
return nil, errors.Wrap(err, "could not initialize direct keymanager")
}
case v2keymanager.Derived:
opts, err := derived.UnmarshalOptionsFile(configFile)
if err != nil {
return nil, errors.Wrap(err, "could not unmarshal keymanager config file")
}
keymanager, err = derived.NewKeymanager(ctx, &derived.SetupConfig{
Opts: opts,
Wallet: w,
SkipMnemonicConfirm: skipMnemonicConfirm,
})
if err != nil {
return nil, errors.Wrap(err, "could not initialize derived keymanager")
}
case v2keymanager.Remote:
opts, err := remote.UnmarshalOptionsFile(configFile)
if err != nil {
return nil, errors.Wrap(err, "could not unmarshal keymanager config file")
}
keymanager, err = remote.NewKeymanager(ctx, &remote.SetupConfig{
Opts: opts,
MaxMessageSize: 100000000,
})
if err != nil {
return nil, errors.Wrap(err, "could not initialize remote keymanager")
}
default:
return nil, fmt.Errorf("keymanager kind not supported: %s", w.keymanagerKind)
}
return keymanager, nil
}
// WriteFileAtPath within the wallet directory given the desired path, filename, and raw data.
func (w *Wallet) WriteFileAtPath(ctx context.Context, filePath string, fileName string, data []byte) error {
accountPath := filepath.Join(w.accountsPath, filePath)
if err := os.MkdirAll(accountPath, os.ModePerm); err != nil {
return errors.Wrapf(err, "could not create path: %s", accountPath)
}
fullPath := filepath.Join(accountPath, fileName)
if err := ioutil.WriteFile(fullPath, data, params.BeaconIoConfig().ReadWritePermissions); err != nil {
return errors.Wrapf(err, "could not write %s", filePath)
}
log.WithFields(logrus.Fields{
"path": fullPath,
"fileName": fileName,
}).Debug("Wrote new file at path")
return nil
}
// ReadFileAtPath within the wallet directory given the desired path and filename.
func (w *Wallet) ReadFileAtPath(ctx context.Context, filePath string, fileName string) ([]byte, error) {
accountPath := filepath.Join(w.accountsPath, filePath)
if err := os.MkdirAll(accountPath, os.ModePerm); err != nil {
return nil, errors.Wrapf(err, "could not create path: %s", accountPath)
}
fullPath := filepath.Join(accountPath, fileName)
matches, err := filepath.Glob(fullPath)
if err != nil {
return []byte{}, errors.Wrap(err, "could not find file")
}
if len(matches) == 0 {
return []byte{}, fmt.Errorf("no files found %s", fullPath)
}
rawData, err := ioutil.ReadFile(matches[0])
if err != nil {
return nil, errors.Wrapf(err, "could not read %s", filePath)
}
return rawData, nil
}
// FileNameAtPath return the full file name for the requested file. It allows for finding the file
// with a regex pattern.
func (w *Wallet) FileNameAtPath(ctx context.Context, filePath string, fileName string) (string, error) {
accountPath := filepath.Join(w.accountsPath, filePath)
if err := os.MkdirAll(accountPath, os.ModePerm); err != nil {
return "", errors.Wrapf(err, "could not create path: %s", accountPath)
}
fullPath := filepath.Join(accountPath, fileName)
matches, err := filepath.Glob(fullPath)
if err != nil {
return "", errors.Wrap(err, "could not find file")
}
if len(matches) == 0 {
return "", fmt.Errorf("no files found %s", fullPath)
}
fullFileName := filepath.Base(matches[0])
return fullFileName, nil
}
// ReadKeymanagerConfigFromDisk opens a keymanager config file
// for reading if it exists at the wallet path.
func (w *Wallet) ReadKeymanagerConfigFromDisk(ctx context.Context) (io.ReadCloser, error) {
configFilePath := filepath.Join(w.accountsPath, KeymanagerConfigFileName)
if !fileutil.FileExists(configFilePath) {
return nil, fmt.Errorf("no keymanager config file found at path: %s", w.accountsPath)
}
w.configFilePath = configFilePath
return os.Open(configFilePath)
}
// LockWalletConfigFile lock read and write to wallet file in order to prevent
// two validators from using the same keys.
func (w *Wallet) LockWalletConfigFile(ctx context.Context) error {
fileLock := flock.New(w.configFilePath)
locked, err := fileLock.TryLock()
if err != nil {
return errors.Wrapf(err, "failed to lock wallet config file: %s", w.configFilePath)
}
if !locked {
return fmt.Errorf("failed to lock wallet config file: %s", w.configFilePath)
}
w.walletFileLock = fileLock
return nil
}
// UnlockWalletConfigFile unlock wallet file.
// should be called before client is closing in order to remove the file lock.
func (w *Wallet) UnlockWalletConfigFile() error {
if w.walletFileLock == nil {
return errors.New("trying to unlock a nil lock")
}
return w.walletFileLock.Unlock()
}
// WriteKeymanagerConfigToDisk takes an encoded keymanager config file
// and writes it to the wallet path.
func (w *Wallet) WriteKeymanagerConfigToDisk(ctx context.Context, encoded []byte) error {
configFilePath := filepath.Join(w.accountsPath, KeymanagerConfigFileName)
// Write the config file to disk.
if err := ioutil.WriteFile(configFilePath, encoded, params.BeaconIoConfig().ReadWritePermissions); err != nil {
return errors.Wrapf(err, "could not write %s", configFilePath)
}
log.WithField("configFilePath", configFilePath).Debug("Wrote keymanager config file to disk")
return nil
}
// ReadEncryptedSeedFromDisk reads the encrypted wallet seed configuration from
// within the wallet path.
func (w *Wallet) ReadEncryptedSeedFromDisk(ctx context.Context) (io.ReadCloser, error) {
configFilePath := filepath.Join(w.accountsPath, derived.EncryptedSeedFileName)
if !fileutil.FileExists(configFilePath) {
return nil, fmt.Errorf("no encrypted seed file found at path: %s", w.accountsPath)
}
return os.Open(configFilePath)
}
// WriteEncryptedSeedToDisk writes the encrypted wallet seed configuration
// within the wallet path.
func (w *Wallet) WriteEncryptedSeedToDisk(ctx context.Context, encoded []byte) error {
seedFilePath := filepath.Join(w.accountsPath, derived.EncryptedSeedFileName)
// Write the config file to disk.
if err := ioutil.WriteFile(seedFilePath, encoded, params.BeaconIoConfig().ReadWritePermissions); err != nil {
return errors.Wrapf(err, "could not write %s", seedFilePath)
}
log.WithField("seedFilePath", seedFilePath).Debug("Wrote wallet encrypted seed file to disk")
return nil
}
func readKeymanagerKindFromWalletPath(walletPath string) (v2keymanager.Kind, error) {
walletItem, err := os.Open(walletPath)
if err != nil {
return 0, err
}
defer func() {
if err := walletItem.Close(); err != nil {
log.WithField(
"path", walletPath,
).Errorf("Could not close wallet directory: %v", err)
}
}()
list, err := walletItem.Readdirnames(0) // 0 to read all files and folders.
if err != nil {
return 0, fmt.Errorf("could not read files in directory: %s", walletPath)
}
for _, n := range list {
keymanagerKind, err := v2keymanager.ParseKind(n)
if err == nil {
return keymanagerKind, nil
}
}
return 0, errors.New("no keymanager folder, 'direct', 'remote', nor 'derived' found in wallet path")
}
// isEmptyWallet checks if a folder consists key directory such as `derived`, `remote` or `direct`.
// Returns true if exists, false otherwise.
func isEmptyWallet(name string) (bool, error) {
expanded, err := fileutil.ExpandPath(name)
if err != nil {
return false, err
}
f, err := os.Open(expanded)
if err != nil {
return false, err
}
defer func() {
if err := f.Close(); err != nil {
log.Debugf("Could not close directory: %s", expanded)
}
}()
names, err := f.Readdirnames(-1)
if err == io.EOF {
return true, nil
}
for _, n := range names {
// Nil error means input name is `derived`, `remote` or `direct`, the wallet is not empty.
_, err := v2keymanager.ParseKind(n)
if err == nil {
return false, nil
}
}
return true, err
}
func inputPassword(
cliCtx *cli.Context,
passwordFileFlag *cli.StringFlag,
promptText string,
confirmPassword bool,
passwordValidator func(input string) error,
) (string, error) {
if cliCtx.IsSet(passwordFileFlag.Name) {
passwordFilePathInput := cliCtx.String(passwordFileFlag.Name)
data, err := fileutil.ReadFileAsBytes(passwordFilePathInput)
if err != nil {
return "", errors.Wrap(err, "could not read file as bytes")
}
enteredPassword := strings.TrimRight(string(data), "\r\n")
if err := passwordValidator(enteredPassword); err != nil {
return "", errors.Wrap(err, "password did not pass validation")
}
return enteredPassword, nil
}
var hasValidPassword bool
var walletPassword string
var err error
for !hasValidPassword {
walletPassword, err = promptutil.PasswordPrompt(promptText, passwordValidator)
if err != nil {
return "", fmt.Errorf("could not read account password: %v", err)
}
if confirmPassword {
passwordConfirmation, err := promptutil.PasswordPrompt(confirmPasswordPromptText, passwordValidator)
if err != nil {
return "", fmt.Errorf("could not read password confirmation: %v", err)
}
if walletPassword != passwordConfirmation {
log.Error("Passwords do not match")
continue
}
hasValidPassword = true
} else {
return walletPassword, nil
}
}
return walletPassword, nil
}