Accounts V2: Simplify Wallet Save/Read To and From Disk Functions (#6686)

* simplify wallet functions

* fix build

* futher simplify wallet

* simplify read/write methods

* move direct functions to direct keymanager

* further move direct km specific funcs

* cleanup

* simplify the direct tests

* fixed tests

* lint

* further simplify

* tidy

* fix config write

* fixed test

Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
This commit is contained in:
Raul Jordan
2020-07-22 21:12:51 -05:00
committed by GitHub
parent 0006377aec
commit 7c52ef8c2b
15 changed files with 304 additions and 369 deletions

View File

@@ -22,7 +22,6 @@ go_library(
"//validator:__subpackages__",
],
deps = [
"//shared/bytesutil:go_default_library",
"//shared/params:go_default_library",
"//validator/flags:go_default_library",
"//validator/keymanager/v2:go_default_library",
@@ -37,7 +36,6 @@ go_library(
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
"@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library",
],
)

View File

@@ -2,6 +2,7 @@ package v2
import (
"archive/zip"
"context"
"fmt"
"io"
"os"
@@ -14,6 +15,7 @@ import (
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct"
"github.com/urfave/cli/v2"
)
@@ -31,8 +33,15 @@ func ExportAccount(cliCtx *cli.Context) error {
if err != nil {
return errors.Wrap(err, "could not open wallet")
}
allAccounts, err := wallet.AccountNames()
keymanager, err := wallet.InitializeKeymanager(context.Background(), true /* skip mnemonic confirm */)
if err != nil {
return errors.Wrap(err, "could not initialize keymanager")
}
km, ok := keymanager.(*direct.Keymanager)
if !ok {
return errors.New("can only export accounts for a non-HD wallet")
}
allAccounts, err := km.ValidatingAccountNames()
if err != nil {
return errors.Wrap(err, "could not get account names")
}
@@ -48,7 +57,7 @@ func ExportAccount(cliCtx *cli.Context) error {
return errors.Wrap(err, "could not export accounts")
}
if err := logAccountsExported(wallet, accounts); err != nil {
if err := logAccountsExported(wallet, km, accounts); err != nil {
return errors.Wrap(err, "could not log out exported accounts")
}
@@ -196,7 +205,7 @@ func copyFileFromZip(archive *zip.Writer, sourcePath string, info os.FileInfo, p
return err
}
func logAccountsExported(wallet *Wallet, accountNames []string) error {
func logAccountsExported(wallet *Wallet, keymanager *direct.Keymanager, accountNames []string) error {
au := aurora.NewAurora(true)
numAccounts := au.BrightYellow(len(accountNames))
@@ -210,7 +219,7 @@ func logAccountsExported(wallet *Wallet, accountNames []string) error {
fmt.Println("")
fmt.Printf("%s\n", au.BrightGreen(accountName).Bold())
publicKey, err := wallet.publicKeyForAccount(accountName)
publicKey, err := keymanager.PublicKeyForAccount(accountName)
if err != nil {
return errors.Wrap(err, "could not get public key")
}

View File

@@ -45,7 +45,7 @@ func TestZipAndUnzip(t *testing.T) {
_, err = keymanager.CreateAccount(ctx, password)
require.NoError(t, err)
accounts, err := wallet.AccountNames()
accounts, err := keymanager.ValidatingAccountNames()
require.NoError(t, err)
if len(accounts) == 0 {
@@ -88,10 +88,14 @@ func TestExport_Noninteractive(t *testing.T) {
wallet, err := NewWallet(cliCtx)
require.NoError(t, err)
ctx := context.Background()
keymanagerCfg := direct.DefaultConfig()
encodedCfg, err := direct.MarshalConfigFile(ctx, keymanagerCfg)
require.NoError(t, err)
require.NoError(t, wallet.WriteKeymanagerConfigToDisk(ctx, encodedCfg))
keymanager, err := direct.NewKeymanager(
ctx,
wallet,
direct.DefaultConfig(),
keymanagerCfg,
)
require.NoError(t, err)
_, err = keymanager.CreateAccount(ctx, password)

View File

@@ -10,16 +10,14 @@ import (
// Useful for keymanagers to have persistent capabilities for accounts on-disk.
type Wallet interface {
// Methods to retrieve wallet and accounts metadata.
AccountNames() ([]string, error)
AccountsDir() string
ListDirs() ([]string, error)
// Read methods for important wallet and accounts-related files.
ReadEncryptedSeedFromDisk(ctx context.Context) (io.ReadCloser, error)
ReadPasswordForAccount(accountName string) (string, error)
ReadFileForAccount(accountName string, fileName string) ([]byte, error)
ReadFileAtPath(ctx context.Context, filePath string, fileName string) ([]byte, error)
ReadPasswordFromDisk(ctx context.Context, passwordFileName string) (string, error)
// Write methods to persist important wallet and accounts-related files to disk.
WriteFileAtPath(ctx context.Context, pathName string, fileName string, data []byte) error
WritePasswordToDisk(ctx context.Context, passwordFileName string, password string) error
WriteEncryptedSeedToDisk(ctx context.Context, encoded []byte) error
WriteAccountToDisk(ctx context.Context, password string) (string, error)
WriteFileForAccount(ctx context.Context, accountName string, fileName string, data []byte) error
}

View File

@@ -2,6 +2,7 @@ package v2
import (
"archive/zip"
"context"
"fmt"
"io"
"os"
@@ -10,10 +11,10 @@ import (
"strings"
"github.com/logrusorgru/aurora"
"github.com/manifoldco/promptui"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct"
"github.com/urfave/cli/v2"
)
@@ -24,6 +25,14 @@ func ImportAccount(cliCtx *cli.Context) error {
if err != nil {
return errors.Wrap(err, "could not open wallet")
}
keymanager, err := wallet.InitializeKeymanager(context.Background(), true /* skip mnemonic confirm */)
if err != nil {
return errors.Wrap(err, "could not initialize keymanager")
}
km, ok := keymanager.(*direct.Keymanager)
if !ok {
return errors.New("can only export accounts for a non-HD wallet")
}
backupDir, err := inputImportDir(cliCtx)
if err != nil {
@@ -43,11 +52,11 @@ func ImportAccount(cliCtx *cli.Context) error {
fmt.Printf("Importing accounts: %s\n", strings.Join(loggedAccounts, ", "))
for _, accountName := range accountsImported {
if err := wallet.enterPasswordForAccount(cliCtx, accountName); err != nil {
if err := km.EnterPasswordForAccount(cliCtx, accountName); err != nil {
return errors.Wrap(err, "could not set account password")
}
}
if err := logAccountsImported(wallet, accountsImported); err != nil {
if err := logAccountsImported(wallet, km, accountsImported); err != nil {
return errors.Wrap(err, "could not log accounts imported")
}
@@ -136,7 +145,7 @@ func copyFileFromZipToPath(file *zip.File, path string) error {
return nil
}
func logAccountsImported(wallet *Wallet, accountNames []string) error {
func logAccountsImported(wallet *Wallet, keymanager *direct.Keymanager, accountNames []string) error {
au := aurora.NewAurora(true)
numAccounts := au.BrightYellow(len(accountNames))
@@ -150,7 +159,7 @@ func logAccountsImported(wallet *Wallet, accountNames []string) error {
fmt.Println("")
fmt.Printf("%s\n", au.BrightGreen(accountName).Bold())
publicKey, err := wallet.publicKeyForAccount(accountName)
publicKey, err := keymanager.PublicKeyForAccount(accountName)
if err != nil {
return errors.Wrap(err, "could not get public key")
}

View File

@@ -43,18 +43,21 @@ func TestImport_Noninteractive(t *testing.T) {
})
wallet, err := NewWallet(cliCtx)
require.NoError(t, err)
ctx := context.Background()
keymanagerCfg := direct.DefaultConfig()
encodedCfg, err := direct.MarshalConfigFile(ctx, keymanagerCfg)
require.NoError(t, err)
require.NoError(t, wallet.WriteKeymanagerConfigToDisk(ctx, encodedCfg))
keymanager, err := direct.NewKeymanager(
ctx,
wallet,
direct.DefaultConfig(),
keymanagerCfg,
)
require.NoError(t, err)
_, err = keymanager.CreateAccount(ctx, password)
require.NoError(t, err)
accounts, err := wallet.AccountNames()
accounts, err := keymanager.ValidatingAccountNames()
require.NoError(t, err)
assert.Equal(t, len(accounts), 1)

View File

@@ -59,7 +59,7 @@ func listDirectKeymanagerAccounts(
keymanager *direct.Keymanager,
) error {
// We initialize the wallet's keymanager.
accountNames, err := wallet.AccountNames()
accountNames, err := keymanager.ValidatingAccountNames()
if err != nil {
return errors.Wrap(err, "could not fetch account names")
}
@@ -77,7 +77,8 @@ func listDirectKeymanagerAccounts(
)
fmt.Printf("Keymanager kind: %s\n", au.BrightGreen(wallet.KeymanagerKind().String()).Bold())
pubKeys, err := keymanager.FetchValidatingPublicKeys(context.Background())
ctx := context.Background()
pubKeys, err := keymanager.FetchValidatingPublicKeys(ctx)
if err != nil {
return errors.Wrap(err, "could not fetch validating public keys")
}
@@ -87,7 +88,7 @@ func listDirectKeymanagerAccounts(
fmt.Printf("%s %#x\n", au.BrightMagenta("[public key]").Bold(), pubKeys[i])
// Retrieve the account creation timestamp.
createdAtBytes, err := wallet.ReadFileForAccount(accountNames[i], direct.TimestampFileName)
createdAtBytes, err := wallet.ReadFileAtPath(ctx, accountNames[i], direct.TimestampFileName)
if err != nil {
return errors.Wrapf(err, "could not read file for account: %s", direct.TimestampFileName)
}
@@ -100,7 +101,7 @@ func listDirectKeymanagerAccounts(
if !showDepositData {
continue
}
enc, err := wallet.ReadFileForAccount(accountNames[i], direct.DepositTransactionFileName)
enc, err := wallet.ReadFileAtPath(ctx, accountNames[i], direct.DepositTransactionFileName)
if err != nil {
return errors.Wrapf(err, "could not read file for account: %s", direct.DepositTransactionFileName)
}

View File

@@ -41,10 +41,10 @@ func TestListAccounts_DirectKeymanager(t *testing.T) {
for i := 0; i < numAccounts; i++ {
accountName, err := keymanager.CreateAccount(ctx, "hello world")
require.NoError(t, err)
depositData, err := wallet.ReadFileForAccount(accountName, direct.DepositTransactionFileName)
depositData, err := wallet.ReadFileAtPath(ctx, accountName, direct.DepositTransactionFileName)
require.NoError(t, err)
depositDataForAccounts[i] = depositData
unixTimestamp, err := wallet.ReadFileForAccount(accountName, direct.TimestampFileName)
unixTimestamp, err := wallet.ReadFileAtPath(ctx, accountName, direct.TimestampFileName)
require.NoError(t, err)
accountCreationTimestamps[i] = unixTimestamp
}
@@ -72,7 +72,7 @@ func TestListAccounts_DirectKeymanager(t *testing.T) {
t.Errorf("Did not find accounts path %s in output", wallet.accountsPath)
}
accountNames, err := wallet.AccountNames()
accountNames, err := keymanager.ValidatingAccountNames()
require.NoError(t, err)
pubKeys, err := keymanager.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
@@ -122,7 +122,7 @@ func TestListAccounts_DerivedKeymanager(t *testing.T) {
// Create a new wallet seed file and write it to disk.
seedConfigFile, err := derived.MarshalEncryptedSeedFile(ctx, seedConfig)
require.NoError(t, err)
require.NoError(t, wallet.WriteEncryptedSeedToDisk(ctx, seedConfigFile))
require.NoError(t, wallet.WriteFileAtPath(ctx, "", derived.EncryptedSeedFileName, seedConfigFile))
keymanager, err := derived.NewKeymanager(
ctx,

View File

@@ -9,5 +9,4 @@ go_library(
"//validator:__pkg__",
"//validator:__subpackages__",
],
deps = ["@com_github_dustinkirkland_golang_petname//:go_default_library"],
)

View File

@@ -7,12 +7,12 @@ import (
"io"
"io/ioutil"
"sync"
petname "github.com/dustinkirkland/golang-petname"
)
// Wallet contains an in-memory, simulated wallet implementation.
type Wallet struct {
InnerAccountsDir string
Directories []string
Files map[string]map[string][]byte
EncryptedSeedFile []byte
AccountPasswords map[string]string
@@ -33,58 +33,34 @@ func (m *Wallet) AccountNames() ([]string, error) {
// AccountsDir --
func (m *Wallet) AccountsDir() string {
return ""
return m.InnerAccountsDir
}
// WriteAccountToDisk --
func (m *Wallet) WriteAccountToDisk(ctx context.Context, password string) (string, error) {
m.lock.Lock()
defer m.lock.Unlock()
accountName := petname.Generate(3, "-")
m.AccountPasswords[accountName] = password
return accountName, nil
// ListDirs --
func (m *Wallet) ListDirs() ([]string, error) {
return m.Directories, nil
}
// WriteFileForAccount --
func (m *Wallet) WriteFileForAccount(
ctx context.Context,
accountName string,
fileName string,
data []byte,
) error {
// WritePasswordToDisk --
func (m *Wallet) WritePasswordToDisk(ctx context.Context, passwordFileName string, password string) error {
m.lock.Lock()
defer m.lock.Unlock()
if m.Files[accountName] == nil {
m.Files[accountName] = make(map[string][]byte)
}
m.Files[accountName][fileName] = data
m.AccountPasswords[passwordFileName] = password
return nil
}
// ReadPasswordForAccount --
func (m *Wallet) ReadPasswordForAccount(accountName string) (string, error) {
// ReadPasswordFromDisk --
func (m *Wallet) ReadPasswordFromDisk(ctx context.Context, passwordFileName string) (string, error) {
m.lock.RLock()
defer m.lock.RUnlock()
for name, password := range m.AccountPasswords {
if name == accountName {
if name == passwordFileName {
return password, nil
}
}
return "", errors.New("account not found")
}
// ReadFileForAccount --
func (m *Wallet) ReadFileForAccount(accountName string, fileName string) ([]byte, error) {
m.lock.RLock()
defer m.lock.RUnlock()
for f, v := range m.Files[accountName] {
if f == fileName {
return v, nil
}
}
return nil, errors.New("file not found")
}
// WriteFileAtPath --
func (m *Wallet) WriteFileAtPath(ctx context.Context, pathName string, fileName string, data []byte) error {
m.lock.Lock()

View File

@@ -2,27 +2,20 @@ package v2
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
"path/filepath"
petname "github.com/dustinkirkland/golang-petname"
"github.com/logrusorgru/aurora"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/params"
"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/sirupsen/logrus"
"github.com/urfave/cli/v2"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
const (
@@ -32,14 +25,6 @@ const (
PasswordsDefaultDirName = ".prysm-wallet-v2-passwords"
// KeymanagerConfigFileName for the keymanager used by the wallet: direct, derived, or remote.
KeymanagerConfigFileName = "keymanageropts.json"
// EncryptedSeedFileName for persisting a wallet's seed when using a derived keymanager.
EncryptedSeedFileName = "seed.encrypted.json"
// PasswordFileSuffix for passwords persisted as text to disk.
PasswordFileSuffix = ".pass"
// NumAccountWords for human-readable names in wallets using a direct keymanager.
NumAccountWords = 3 // Number of words in account human-readable names.
// AccountFilePermissions for accounts saved to disk.
AccountFilePermissions = os.O_CREATE | os.O_RDWR
// DirectoryPermissions for directories created under the wallet path.
DirectoryPermissions = os.ModePerm
)
@@ -155,16 +140,6 @@ func OpenWallet(cliCtx *cli.Context) (*Wallet, error) {
return w, 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 := path.Join(w.accountsPath, KeymanagerConfigFileName)
if !fileExists(configFilePath) {
return nil, fmt.Errorf("no keymanager config file found at path: %s", w.accountsPath)
}
return os.Open(configFilePath)
}
// KeymanagerKind used by the wallet.
func (w *Wallet) KeymanagerKind() v2keymanager.Kind {
return w.keymanagerKind
@@ -175,37 +150,6 @@ func (w *Wallet) AccountsDir() string {
return w.accountsPath
}
// AccountNames reads all account names at the wallet's path.
func (w *Wallet) AccountNames() ([]string, error) {
accountsDir, err := os.Open(w.accountsPath)
if err != nil {
return nil, err
}
defer func() {
if err := accountsDir.Close(); err != nil {
log.WithField(
"directory", w.accountsPath,
).Errorf("Could not close accounts directory: %v", err)
}
}()
list, err := accountsDir.Readdirnames(0) // 0 to read all files and folders.
if err != nil {
return nil, errors.Wrapf(err, "could not read files in directory: %s", w.accountsPath)
}
accountNames := make([]string, 0)
for _, item := range list {
ok, err := hasDir(path.Join(w.accountsPath, item))
if err != nil {
return nil, errors.Wrapf(err, "could not parse directory: %v", err)
}
if ok {
accountNames = append(accountNames, item)
}
}
return accountNames, err
}
// 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(
@@ -242,24 +186,35 @@ func (w *Wallet) InitializeKeymanager(
return keymanager, nil
}
// WriteAccountToDisk creates an account directory under a unique namespace
// within the wallet's path. It additionally writes the account's password to the
// wallet's passwords directory. Returns the unique account name.
func (w *Wallet) WriteAccountToDisk(ctx context.Context, password string) (string, error) {
accountName, err := w.generateAccountName()
// ListDirs in wallet accounts path.
func (w *Wallet) ListDirs() ([]string, error) {
accountsDir, err := os.Open(w.AccountsDir())
if err != nil {
return "", errors.Wrap(err, "could not generate unique account name")
return nil, err
}
// Generate a directory for the new account name and
// write its associated password to disk.
accountPath := path.Join(w.accountsPath, accountName)
if err := os.MkdirAll(accountPath, DirectoryPermissions); err != nil {
return "", errors.Wrap(err, "could not create account directory")
defer func() {
if err := accountsDir.Close(); err != nil {
log.WithField(
"directory", w.AccountsDir(),
).Errorf("Could not close accounts directory: %v", err)
}
}()
list, err := accountsDir.Readdirnames(0) // 0 to read all files and folders.
if err != nil {
return nil, errors.Wrapf(err, "could not read files in directory: %s", w.AccountsDir())
}
if err := w.writePasswordToFile(accountName, password); err != nil {
return "", errors.Wrap(err, "could not write password to disk")
dirNames := make([]string, 0)
for _, item := range list {
ok, err := hasDir(filepath.Join(w.AccountsDir(), item))
if err != nil {
return nil, errors.Wrapf(err, "could not parse directory: %v", err)
}
if ok {
dirNames = append(dirNames, item)
}
}
return accountName, nil
return dirNames, nil
}
// WriteFileAtPath within the wallet directory given the desired path, filename, and raw data.
@@ -282,9 +237,6 @@ func (w *Wallet) WriteFileAtPath(ctx context.Context, filePath string, fileName
// 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 := path.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 := path.Join(accountPath, fileName)
rawData, err := ioutil.ReadFile(fullPath)
if err != nil {
@@ -293,27 +245,14 @@ func (w *Wallet) ReadFileAtPath(ctx context.Context, filePath string, fileName s
return rawData, nil
}
// WriteFileForAccount stores a unique file and its data under an account namespace
// in the wallet's directory on-disk. Creates the file if it does not exist
// and writes over it otherwise.
func (w *Wallet) WriteFileForAccount(ctx context.Context, accountName string, fileName string, data []byte) error {
accountPath := path.Join(w.accountsPath, accountName)
exists, err := hasDir(accountPath)
if err != nil {
return errors.Wrapf(err, "could not check if account exists in directory: %s", w.accountsPath)
// 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 := path.Join(w.accountsPath, KeymanagerConfigFileName)
if !fileExists(configFilePath) {
return nil, fmt.Errorf("no keymanager config file found at path: %s", w.accountsPath)
}
if !exists {
return errors.Wrapf(err, "account does not exist in wallet directory: %s", w.accountsPath)
}
filePath := path.Join(accountPath, fileName)
if err := ioutil.WriteFile(filePath, data, os.ModePerm); err != nil {
return errors.Wrapf(err, "could not write %s", filePath)
}
log.WithFields(logrus.Fields{
"name": accountName,
"path": filePath,
}).Debug("Wrote new file for account")
return nil
return os.Open(configFilePath)
}
// WriteKeymanagerConfigToDisk takes an encoded keymanager config file
@@ -328,10 +267,20 @@ func (w *Wallet) WriteKeymanagerConfigToDisk(ctx context.Context, encoded []byte
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 := path.Join(w.accountsPath, derived.EncryptedSeedFileName)
if !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 := path.Join(w.accountsPath, EncryptedSeedFileName)
seedFilePath := path.Join(w.accountsPath, derived.EncryptedSeedFileName)
// Write the config file to disk.
if err := ioutil.WriteFile(seedFilePath, encoded, os.ModePerm); err != nil {
return errors.Wrapf(err, "could not write %s", seedFilePath)
@@ -340,190 +289,25 @@ func (w *Wallet) WriteEncryptedSeedToDisk(ctx context.Context, encoded []byte) e
return nil
}
// ReadEncryptedSeedFromDisk reads the encrypted wallet seed configuration from
// within the wallet path.
func (w *Wallet) ReadEncryptedSeedFromDisk(ctx context.Context) (io.ReadCloser, error) {
if !fileExists(path.Join(w.accountsPath, EncryptedSeedFileName)) {
return nil, fmt.Errorf("no encrypted seed file found at path: %s", w.accountsPath)
// ReadPasswordFromDisk --
func (w *Wallet) ReadPasswordFromDisk(ctx context.Context, passwordFileName string) (string, error) {
fullPath := filepath.Join(w.passwordsDir, passwordFileName)
rawData, err := ioutil.ReadFile(fullPath)
if err != nil {
return "", errors.Wrapf(err, "could not read %s", fullPath)
}
configFilePath := path.Join(w.accountsPath, EncryptedSeedFileName)
return os.Open(configFilePath)
return string(rawData), nil
}
// ReadPasswordForAccount when given an account name from the wallet's passwords' path.
func (w *Wallet) ReadPasswordForAccount(accountName string) (string, error) {
passwordFilePath := path.Join(w.passwordsDir, accountName+PasswordFileSuffix)
passwordFile, err := os.Open(passwordFilePath)
if err != nil {
return "", errors.Wrapf(err, "could not read password file from directory: %s", w.passwordsDir)
}
defer func() {
if err := passwordFile.Close(); err != nil {
log.Errorf("Could not close password file: %s", passwordFilePath)
}
}()
password, err := ioutil.ReadAll(passwordFile)
if err != nil {
return "", errors.Wrapf(err, "could not read data from password file: %s", passwordFilePath)
}
return string(password), nil
}
// ReadFileForAccount from the wallet's accounts directory.
func (w *Wallet) ReadFileForAccount(accountName string, fileName string) ([]byte, error) {
accountPath := path.Join(w.accountsPath, accountName)
exists, err := hasDir(accountPath)
if err != nil {
return nil, errors.Wrapf(err, "could not check if account exists in directory: %s", w.accountsPath)
}
if !exists {
return nil, errors.Wrapf(err, "account does not exist in wallet directory: %s", w.accountsPath)
}
filePath := path.Join(accountPath, fileName)
f, err := os.Open(filePath)
if err != nil {
return nil, errors.Wrapf(err, "could not read file for account: %s", filePath)
}
defer func() {
if err := f.Close(); err != nil {
log.Errorf("Could not close file after writing: %s", filePath)
}
}()
return ioutil.ReadAll(f)
}
func (w *Wallet) enterPasswordForAccount(cliCtx *cli.Context, accountName string) error {
au := aurora.NewAurora(true)
var password string
var err error
if cliCtx.IsSet(flags.PasswordFileFlag.Name) {
passwordFilePath := cliCtx.String(flags.PasswordFileFlag.Name)
data, err := ioutil.ReadFile(passwordFilePath)
if err != nil {
return err
}
password = string(data)
err = w.checkPasswordForAccount(accountName, password)
if err != nil && strings.Contains(err.Error(), "invalid checksum") {
return fmt.Errorf("invalid password entered for account %s", accountName)
}
if err != nil {
return err
}
} else {
attemptingPassword := true
// Loop asking for the password until the user enters it correctly.
for attemptingPassword {
// Ask the user for the password to their account.
password, err = inputPasswordForAccount(cliCtx, accountName)
if err != nil {
return errors.Wrap(err, "could not input password")
}
err = w.checkPasswordForAccount(accountName, password)
if err != nil && strings.Contains(err.Error(), "invalid checksum") {
fmt.Println(au.Red("Incorrect password entered, please try again"))
continue
}
if err != nil {
return err
}
attemptingPassword = false
}
}
if err := os.MkdirAll(w.passwordsDir, params.BeaconIoConfig().ReadWriteExecutePermissions); err != nil {
return err
}
if err := w.writePasswordToFile(accountName, password); err != nil {
return errors.Wrap(err, "could not write password to disk")
// WritePasswordToDisk --
func (w *Wallet) WritePasswordToDisk(ctx context.Context, passwordFileName string, password string) error {
passwordPath := filepath.Join(w.passwordsDir, passwordFileName)
if err := ioutil.WriteFile(passwordPath, []byte(password), os.ModePerm); err != nil {
return errors.Wrapf(err, "could not write %s", passwordPath)
}
return nil
}
func (w *Wallet) checkPasswordForAccount(accountName string, password string) error {
accountKeystore, err := w.keystoreForAccount(accountName)
if err != nil {
return errors.Wrap(err, "could not get keystore")
}
decryptor := keystorev4.New()
_, err = decryptor.Decrypt(accountKeystore.Crypto, []byte(password))
if err != nil {
return errors.Wrap(err, "could not decrypt keystore")
}
return nil
}
func (w *Wallet) publicKeyForAccount(accountName string) ([48]byte, error) {
accountKeystore, err := w.keystoreForAccount(accountName)
if err != nil {
return [48]byte{}, errors.Wrap(err, "could not get keystore")
}
pubKey, err := hex.DecodeString(accountKeystore.Pubkey)
if err != nil {
return [48]byte{}, errors.Wrap(err, "could decode pubkey string")
}
return bytesutil.ToBytes48(pubKey), nil
}
func (w *Wallet) keystoreForAccount(accountName string) (*v2keymanager.Keystore, error) {
encoded, err := w.ReadFileForAccount(accountName, direct.KeystoreFileName)
if err != nil {
return nil, errors.Wrap(err, "could not read keystore file")
}
keystoreJSON := &v2keymanager.Keystore{}
if err := json.Unmarshal(encoded, &keystoreJSON); err != nil {
return nil, errors.Wrap(err, "could not decode json")
}
return keystoreJSON, nil
}
// Writes the password file for an account namespace in the wallet's passwords directory.
func (w *Wallet) writePasswordToFile(accountName string, password string) error {
passwordFilePath := path.Join(w.passwordsDir, accountName+PasswordFileSuffix)
// Removing any file that exists to make sure the existing is overwritten.
if _, err := os.Stat(passwordFilePath); os.IsExist(err) {
if err := os.Remove(passwordFilePath); err != nil {
return errors.Wrap(err, "could not rewrite password file")
}
}
passwordFile, err := os.Create(passwordFilePath)
if err != nil {
return errors.Wrapf(err, "could not create password file in directory: %s", w.passwordsDir)
}
defer func() {
if err := passwordFile.Close(); err != nil {
log.WithError(err).Error("Could not close password file")
}
}()
n, err := passwordFile.WriteString(password)
if err != nil {
return errors.Wrap(err, "could not write account password to disk")
}
if n != len(password) {
return fmt.Errorf("could only write %d/%d password bytes to disk", n, len(password))
}
return nil
}
// Generates a human-readable name for an account. Checks for uniqueness in the accounts path.
func (w *Wallet) generateAccountName() (string, error) {
var accountExists bool
var accountName string
for !accountExists {
accountName = petname.Generate(NumAccountWords, "-" /* separator */)
exists, err := hasDir(path.Join(w.accountsPath, accountName))
if err != nil {
return "", errors.Wrapf(err, "could not check if account exists in dir: %s", w.accountsPath)
}
if !exists {
break
}
}
return accountName, nil
}
func readKeymanagerKindFromWalletPath(walletPath string) (v2keymanager.Kind, error) {
walletItem, err := os.Open(walletPath)
if err != nil {

View File

@@ -52,6 +52,8 @@ const (
DepositTransactionFileName = "deposit_transaction.rlp"
// DepositDataFileName for the raw, ssz-encoded deposit data object.
DepositDataFileName = "deposit_data.ssz"
// EncryptedSeedFileName for persisting a wallet's seed when using a derived keymanager.
EncryptedSeedFileName = "seed.encrypted.json"
)
// Config for a derived keymanager.
@@ -99,7 +101,7 @@ func NewKeymanager(
) (*Keymanager, error) {
seedConfigFile, err := wallet.ReadEncryptedSeedFromDisk(ctx)
if err != nil {
return nil, errors.Wrap(err, "could not read encrypted seed configuration file from disk")
return nil, errors.Wrap(err, "could not read encrypted seed file from disk")
}
enc, err := ioutil.ReadAll(seedConfigFile)
if err != nil {

View File

@@ -17,13 +17,18 @@ go_library(
"//shared/bls:go_default_library",
"//shared/bytesutil:go_default_library",
"//shared/depositutil:go_default_library",
"//shared/petnames:go_default_library",
"//shared/roughtime:go_default_library",
"//validator/accounts/v2/iface:go_default_library",
"//validator/flags:go_default_library",
"//validator/keymanager/v2:go_default_library",
"@com_github_google_uuid//:go_default_library",
"@com_github_logrusorgru_aurora//:go_default_library",
"@com_github_manifoldco_promptui//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_go_ssz//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
"@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library",
],
)

View File

@@ -7,20 +7,28 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/google/uuid"
"github.com/logrusorgru/aurora"
"github.com/manifoldco/promptui"
"github.com/pkg/errors"
"github.com/prysmaticlabs/go-ssz"
validatorpb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/bytesutil"
"github.com/prysmaticlabs/prysm/shared/depositutil"
"github.com/prysmaticlabs/prysm/shared/petnames"
"github.com/prysmaticlabs/prysm/shared/roughtime"
"github.com/prysmaticlabs/prysm/validator/accounts/v2/iface"
"github.com/prysmaticlabs/prysm/validator/flags"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
@@ -34,7 +42,9 @@ const (
// file for a direct keymanager account.
TimestampFileName = "created_at.txt"
// KeystoreFileName exposes the expected filename for the keystore file for an account.
KeystoreFileName = "keystore.json"
KeystoreFileName = "keystore.json"
// PasswordFileSuffix for passwords persisted as text to disk.
PasswordFileSuffix = ".pass"
depositDataFileName = "deposit_data.ssz"
eipVersion = "EIP-2335"
)
@@ -70,7 +80,7 @@ func NewKeymanager(ctx context.Context, wallet iface.Wallet, cfg *Config) (*Keym
// passphrases, then we initialize a cache of public key -> secret keys
// used to retrieve secrets keys for the accounts via password unlock.
// This cache is needed to process Sign requests using a public key.
if err := k.initializeSecretKeysCache(); err != nil {
if err := k.initializeSecretKeysCache(ctx); err != nil {
return nil, errors.Wrap(err, "could not initialize keys cache")
}
return k, nil
@@ -100,20 +110,28 @@ func MarshalConfigFile(ctx context.Context, cfg *Config) ([]byte, error) {
return json.MarshalIndent(cfg, "", "\t")
}
// ValidatingAccountNames for a direct keymanager.
func (dr *Keymanager) ValidatingAccountNames() ([]string, error) {
return dr.wallet.ListDirs()
}
// CreateAccount for a direct keymanager implementation. This utilizes
// the EIP-2335 keystore standard for BLS12-381 keystores. It
// stores the generated keystore.json file in the wallet and additionally
// generates withdrawal credentials. At the end, it logs
// the raw deposit data hex string for users to copy.
func (dr *Keymanager) CreateAccount(ctx context.Context, password string) (string, error) {
// Create a new, unique account name and write its password + directory to disk.
accountName, err := dr.wallet.WriteAccountToDisk(ctx, password)
// Create a petname for an account from its public key and write its password to disk.
validatingKey := bls.RandKey()
accountName, err := dr.generateAccountName(validatingKey.PublicKey().Marshal())
if err != nil {
return "", errors.Wrap(err, "could not write account to disk")
return "", errors.Wrap(err, "could not generate unique account name")
}
if err := dr.wallet.WritePasswordToDisk(ctx, accountName+".pass", password); err != nil {
return "", errors.Wrap(err, "could not write password to disk")
}
// Generates a new EIP-2335 compliant keystore file
// from a BLS private key and marshals it as JSON.
validatingKey := bls.RandKey()
encoded, err := dr.generateKeystoreFile(validatingKey, password)
if err != nil {
return "", err
@@ -145,7 +163,7 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, password string) (strin
depositutil.LogDepositTransaction(log, tx)
// We write the raw deposit transaction as an .rlp encoded file.
if err := dr.wallet.WriteFileForAccount(ctx, accountName, DepositTransactionFileName, tx.Data()); err != nil {
if err := dr.wallet.WriteFileAtPath(ctx, accountName, DepositTransactionFileName, tx.Data()); err != nil {
return "", errors.Wrapf(err, "could not write for account %s: %s", accountName, DepositTransactionFileName)
}
@@ -154,19 +172,19 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, password string) (strin
if err != nil {
return "", errors.Wrap(err, "could not marshal deposit data")
}
if err := dr.wallet.WriteFileForAccount(ctx, accountName, depositDataFileName, encodedDepositData); err != nil {
if err := dr.wallet.WriteFileAtPath(ctx, accountName, depositDataFileName, encodedDepositData); err != nil {
return "", errors.Wrapf(err, "could not write for account %s: %s", accountName, encodedDepositData)
}
// Write the encoded keystore to disk.
if err := dr.wallet.WriteFileForAccount(ctx, accountName, KeystoreFileName, encoded); err != nil {
if err := dr.wallet.WriteFileAtPath(ctx, accountName, KeystoreFileName, encoded); err != nil {
return "", errors.Wrapf(err, "could not write keystore file for account %s", accountName)
}
// Finally, write the account creation timestamp as a file.
createdAt := roughtime.Now().Unix()
createdAtStr := strconv.FormatInt(createdAt, 10)
if err := dr.wallet.WriteFileForAccount(ctx, accountName, TimestampFileName, []byte(createdAtStr)); err != nil {
if err := dr.wallet.WriteFileAtPath(ctx, accountName, TimestampFileName, []byte(createdAtStr)); err != nil {
return "", errors.Wrapf(err, "could not write timestamp file for account %s", accountName)
}
@@ -179,7 +197,7 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, password string) (strin
// FetchValidatingPublicKeys fetches the list of public keys from the direct account keystores.
func (dr *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) {
accountNames, err := dr.wallet.AccountNames()
accountNames, err := dr.ValidatingAccountNames()
if err != nil {
return nil, err
}
@@ -199,7 +217,7 @@ func (dr *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte
}
for i, name := range accountNames {
encoded, err := dr.wallet.ReadFileForAccount(name, KeystoreFileName)
encoded, err := dr.wallet.ReadFileAtPath(ctx, name, KeystoreFileName)
if err != nil {
return nil, errors.Wrapf(err, "could not read keystore file for account %s", name)
}
@@ -231,18 +249,91 @@ func (dr *Keymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (b
return secretKey.Sign(req.SigningRoot), nil
}
func (dr *Keymanager) initializeSecretKeysCache() error {
accountNames, err := dr.wallet.AccountNames()
// EnterPasswordForAccount checks if a user has a password specified for the new account
// either from a file or from stdin. Then, it saves the password to the wallet.
func (dr *Keymanager) EnterPasswordForAccount(cliCtx *cli.Context, accountName string) error {
au := aurora.NewAurora(true)
var password string
var err error
if cliCtx.IsSet(flags.PasswordFileFlag.Name) {
passwordFilePath := cliCtx.String(flags.PasswordFileFlag.Name)
data, err := ioutil.ReadFile(passwordFilePath)
if err != nil {
return err
}
password = string(data)
err = dr.checkPasswordForAccount(accountName, password)
if err != nil && strings.Contains(err.Error(), "invalid checksum") {
return fmt.Errorf("invalid password entered for account %s", accountName)
}
if err != nil {
return err
}
} else {
attemptingPassword := true
// Loop asking for the password until the user enters it correctly.
for attemptingPassword {
// Ask the user for the password to their account.
password, err = inputPasswordForAccount(cliCtx, accountName)
if err != nil {
return errors.Wrap(err, "could not input password")
}
err = dr.checkPasswordForAccount(accountName, password)
if err != nil && strings.Contains(err.Error(), "invalid checksum") {
fmt.Println(au.Red("Incorrect password entered, please try again"))
continue
}
if err != nil {
return err
}
attemptingPassword = false
}
}
ctx := context.Background()
if err := dr.wallet.WritePasswordToDisk(ctx, accountName+PasswordFileSuffix, password); err != nil {
return errors.Wrap(err, "could not write password to disk")
}
return nil
}
// PublicKeyForAccount returns the associated public key for an account name.
func (dr *Keymanager) PublicKeyForAccount(accountName string) ([48]byte, error) {
accountKeystore, err := dr.keystoreForAccount(accountName)
if err != nil {
return [48]byte{}, errors.Wrap(err, "could not get keystore")
}
pubKey, err := hex.DecodeString(accountKeystore.Pubkey)
if err != nil {
return [48]byte{}, errors.Wrap(err, "could decode pubkey string")
}
return bytesutil.ToBytes48(pubKey), nil
}
func (dr *Keymanager) keystoreForAccount(accountName string) (*v2keymanager.Keystore, error) {
encoded, err := dr.wallet.ReadFileAtPath(context.Background(), accountName, KeystoreFileName)
if err != nil {
return nil, errors.Wrap(err, "could not read keystore file")
}
keystoreJSON := &v2keymanager.Keystore{}
if err := json.Unmarshal(encoded, &keystoreJSON); err != nil {
return nil, errors.Wrap(err, "could not decode json")
}
return keystoreJSON, nil
}
func (dr *Keymanager) initializeSecretKeysCache(ctx context.Context) error {
accountNames, err := dr.ValidatingAccountNames()
if err != nil {
return err
}
for _, name := range accountNames {
password, err := dr.wallet.ReadPasswordForAccount(name)
password, err := dr.wallet.ReadPasswordFromDisk(ctx, name+PasswordFileSuffix)
if err != nil {
return errors.Wrapf(err, "could not read password for account %s", name)
}
encoded, err := dr.wallet.ReadFileForAccount(name, KeystoreFileName)
encoded, err := dr.wallet.ReadFileAtPath(ctx, name, KeystoreFileName)
if err != nil {
return errors.Wrapf(err, "could not read keystore file for account %s", name)
}
@@ -289,3 +380,53 @@ func (dr *Keymanager) generateKeystoreFile(validatingKey bls.SecretKey, password
}
return json.MarshalIndent(keystoreFile, "", "\t")
}
func (dr *Keymanager) generateAccountName(pubKey []byte) (string, error) {
var accountExists bool
var accountName string
for !accountExists {
accountName = petnames.DeterministicName(pubKey, "-")
exists, err := hasDir(filepath.Join(dr.wallet.AccountsDir(), accountName))
if err != nil {
return "", errors.Wrapf(err, "could not check if account exists in dir: %s", dr.wallet.AccountsDir())
}
if !exists {
break
}
}
return accountName, nil
}
func (dr *Keymanager) checkPasswordForAccount(accountName string, password string) error {
accountKeystore, err := dr.keystoreForAccount(accountName)
if err != nil {
return errors.Wrap(err, "could not get keystore")
}
decryptor := keystorev4.New()
_, err = decryptor.Decrypt(accountKeystore.Crypto, []byte(password))
if err != nil {
return errors.Wrap(err, "could not decrypt keystore")
}
return nil
}
// Checks if a directory indeed exists at the specified path.
func hasDir(dirPath string) (bool, error) {
info, err := os.Stat(dirPath)
if os.IsNotExist(err) {
return false, nil
}
return info.IsDir(), err
}
func inputPasswordForAccount(_ *cli.Context, accountName string) (string, error) {
prompt := promptui.Prompt{
Label: fmt.Sprintf("Enter password for account %s", accountName),
Mask: '*',
}
walletPassword, err := prompt.Run()
if err != nil {
return "", fmt.Errorf("could not read wallet password: %v", err)
}
return walletPassword, nil
}

View File

@@ -21,7 +21,7 @@ import (
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
func TestKeymanager_CreateAccount(t *testing.T) {
func TestDirectKeymanager_CreateAccount(t *testing.T) {
hook := logTest.NewGlobal()
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
@@ -72,7 +72,7 @@ func TestKeymanager_CreateAccount(t *testing.T) {
testutil.AssertLogsContain(t, hook, "Successfully created new validator account")
}
func TestKeymanager_FetchValidatingPublicKeys(t *testing.T) {
func TestDirectKeymanager_FetchValidatingPublicKeys(t *testing.T) {
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
@@ -83,8 +83,9 @@ func TestKeymanager_FetchValidatingPublicKeys(t *testing.T) {
}
// First, generate accounts and their keystore.json files.
ctx := context.Background()
numAccounts := 20
wantedPublicKeys := generateAccounts(t, numAccounts, dr)
numAccounts := 1
accountNames, wantedPublicKeys := generateAccounts(t, numAccounts, dr)
wallet.Directories = accountNames
publicKeys, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
// The results are not guaranteed to be ordered, so we ensure each
@@ -100,7 +101,7 @@ func TestKeymanager_FetchValidatingPublicKeys(t *testing.T) {
}
}
func TestKeymanager_Sign(t *testing.T) {
func TestDirectKeymanager_Sign(t *testing.T) {
wallet := &mock.Wallet{
Files: make(map[string]map[string][]byte),
AccountPasswords: make(map[string]string),
@@ -112,9 +113,11 @@ func TestKeymanager_Sign(t *testing.T) {
// First, generate accounts and their keystore.json files.
numAccounts := 2
generateAccounts(t, numAccounts, dr)
accountNames, _ := generateAccounts(t, numAccounts, dr)
wallet.Directories = accountNames
ctx := context.Background()
require.NoError(t, dr.initializeSecretKeysCache())
require.NoError(t, dr.initializeSecretKeysCache(ctx))
publicKeys, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
@@ -137,7 +140,7 @@ func TestKeymanager_Sign(t *testing.T) {
t.Fatalf("Expected sig not to verify for pubkey %#x and data %v", wrongPubKey.Marshal(), data)
}
}
func TestKeymanager_Sign_NoPublicKeySpecified(t *testing.T) {
func TestDirectKeymanager_Sign_NoPublicKeySpecified(t *testing.T) {
req := &validatorpb.SignRequest{
PublicKey: nil,
}
@@ -146,7 +149,7 @@ func TestKeymanager_Sign_NoPublicKeySpecified(t *testing.T) {
assert.ErrorContains(t, "nil public key", err)
}
func TestKeymanager_Sign_NoPublicKeyInCache(t *testing.T) {
func TestDirectKeymanager_Sign_NoPublicKeyInCache(t *testing.T) {
req := &validatorpb.SignRequest{
PublicKey: []byte("hello world"),
}
@@ -178,8 +181,9 @@ func BenchmarkKeymanager_FetchValidatingPublicKeys(b *testing.B) {
}
}
func generateAccounts(t testing.TB, numAccounts int, dr *Keymanager) [][48]byte {
func generateAccounts(t testing.TB, numAccounts int, dr *Keymanager) ([]string, [][48]byte) {
ctx := context.Background()
accountNames := make([]string, numAccounts)
wantedPublicKeys := make([][48]byte, numAccounts)
for i := 0; i < numAccounts; i++ {
validatingKey := bls.RandKey()
@@ -187,9 +191,11 @@ func generateAccounts(t testing.TB, numAccounts int, dr *Keymanager) [][48]byte
password := strconv.Itoa(i)
encoded, err := dr.generateKeystoreFile(validatingKey, password)
require.NoError(t, err)
accountName, err := dr.wallet.WriteAccountToDisk(ctx, password)
accountName, err := dr.generateAccountName(validatingKey.PublicKey().Marshal())
require.NoError(t, err)
require.NoError(t, dr.wallet.WriteFileForAccount(ctx, accountName, KeystoreFileName, encoded))
assert.NoError(t, err, dr.wallet.WriteFileAtPath(ctx, accountName, KeystoreFileName, encoded))
assert.NoError(t, err, dr.wallet.WritePasswordToDisk(ctx, accountName+PasswordFileSuffix, password))
accountNames[i] = accountName
}
return wantedPublicKeys
return accountNames, wantedPublicKeys
}