Accounts V2: Resolve Remaining Keymanager Bugs (#6706)

* v2 fix bugs
* better doc
* include wallet build fix
* fixed broken list test
* add round trip recover seed unit test
* imports
* implement list with tests
* add altona flags
* tests for unicode
* added is valid unicode tests
* fixed up tests to ensure wallet is persisted after everything works
* resolve confs and integrate medalla testnet
* fix build
* add medalla
* fixed import spacing
This commit is contained in:
Raul Jordan
2020-07-23 19:43:01 -05:00
committed by GitHub
parent cc773a1641
commit a5b408769a
22 changed files with 408 additions and 46 deletions

View File

@@ -115,13 +115,13 @@ func ConfigureBeaconChain(ctx *cli.Context) {
if ctx.Bool(devModeFlag.Name) {
enableDevModeFlags(ctx)
}
if ctx.Bool(altonaTestnet.Name) {
if ctx.Bool(AltonaTestnet.Name) {
log.Warn("Running Node on Altona Testnet")
params.UseAltonaConfig()
params.UseAltonaNetworkConfig()
cfg.AltonaTestnet = true
}
if ctx.Bool(medallaTestnet.Name) {
if ctx.Bool(MedallaTestnet.Name) {
log.Warn("Running Node on Medalla Testnet")
params.UseMedallaConfig()
params.UseMedallaNetworkConfig()
@@ -264,13 +264,13 @@ func ConfigureSlasher(ctx *cli.Context) {
func ConfigureValidator(ctx *cli.Context) {
complainOnDeprecatedFlags(ctx)
cfg := &Flags{}
if ctx.Bool(altonaTestnet.Name) {
if ctx.Bool(AltonaTestnet.Name) {
log.Warn("Running Validator on Altona Testnet")
params.UseAltonaConfig()
params.UseAltonaNetworkConfig()
cfg.AltonaTestnet = true
}
if ctx.Bool(medallaTestnet.Name) {
if ctx.Bool(MedallaTestnet.Name) {
log.Warn("Running Validator on Medalla Testnet")
params.UseMedallaConfig()
params.UseMedallaNetworkConfig()

View File

@@ -5,6 +5,16 @@ import (
)
var (
// AltonaTestnet flag for the multiclient eth2 testnet configuration.
AltonaTestnet = &cli.BoolFlag{
Name: "altona",
Usage: "This defines the flag through which we can run on the Altona Multiclient Testnet",
}
// MedallaTestnet flag for the multiclient eth2 testnet configuration.
MedallaTestnet = &cli.BoolFlag{
Name: "medalla",
Usage: "This defines the flag through which we can run on the Medalla Multiclient Testnet",
}
devModeFlag = &cli.BoolFlag{
Name: "dev",
Usage: "Enable experimental features still in development. These features may not be stable.",
@@ -136,14 +146,6 @@ var (
Name: "attestation-aggregation-force-maxcover",
Usage: "When enabled, forces --attestation-aggregation-strategy=max_cover setting.",
}
altonaTestnet = &cli.BoolFlag{
Name: "altona",
Usage: "This defines the flag through which we can run on the Altona Multiclient Testnet",
}
medallaTestnet = &cli.BoolFlag{
Name: "medalla",
Usage: "This defines the flag through which we can run on the Medalla Multiclient Testnet",
}
enableAccountsV2 = &cli.BoolFlag{
Name: "enable-accounts-v2",
Usage: "Enables usage of v2 for Prysm validator accounts",
@@ -569,8 +571,8 @@ var ValidatorFlags = append(deprecatedFlags, []cli.Flag{
enableExternalSlasherProtectionFlag,
disableDomainDataCacheFlag,
waitForSyncedFlag,
altonaTestnet,
medallaTestnet,
AltonaTestnet,
MedallaTestnet,
enableAccountsV2,
}...)
@@ -612,8 +614,8 @@ var BeaconChainFlags = append(deprecatedFlags, []cli.Flag{
attestationAggregationStrategy,
newBeaconStateLocks,
forceMaxCoverAttestationAggregation,
altonaTestnet,
medallaTestnet,
AltonaTestnet,
MedallaTestnet,
batchBlockVerify,
initSyncVerbose,
enableFinalizedDepositsCache,

View File

@@ -23,7 +23,9 @@ go_library(
"//validator:__subpackages__",
],
deps = [
"//shared/featureconfig:go_default_library",
"//shared/params:go_default_library",
"//shared/petnames:go_default_library",
"//validator/flags:go_default_library",
"//validator/keymanager/v2:go_default_library",
"//validator/keymanager/v2/derived:go_default_library",
@@ -55,6 +57,10 @@ go_test(
],
embed = [":go_default_library"],
deps = [
"//proto/validator/accounts/v2:go_default_library",
"//shared/bls:go_default_library",
"//shared/bytesutil:go_default_library",
"//shared/petnames:go_default_library",
"//shared/testutil:go_default_library",
"//shared/testutil/assert:go_default_library",
"//shared/testutil/require:go_default_library",

View File

@@ -1,6 +1,7 @@
package v2
import (
"github.com/prysmaticlabs/prysm/shared/featureconfig"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/urfave/cli/v2"
)
@@ -22,6 +23,8 @@ this command outputs a deposit data string which is required to become a validat
flags.WalletPasswordsDirFlag,
flags.PasswordFileFlag,
flags.SkipMnemonicConfirmFlag,
featureconfig.AltonaTestnet,
featureconfig.MedallaTestnet,
},
Action: func(cliCtx *cli.Context) error {
if err := NewAccount(cliCtx); err != nil {
@@ -38,6 +41,8 @@ this command outputs a deposit data string which is required to become a validat
flags.WalletPasswordsDirFlag,
flags.PasswordFileFlag,
flags.ShowDepositDataFlag,
featureconfig.AltonaTestnet,
featureconfig.MedallaTestnet,
},
Action: func(cliCtx *cli.Context) error {
if err := ListAccounts(cliCtx); err != nil {
@@ -54,6 +59,8 @@ this command outputs a deposit data string which is required to become a validat
flags.WalletPasswordsDirFlag,
flags.BackupDirFlag,
flags.AccountsFlag,
featureconfig.AltonaTestnet,
featureconfig.MedallaTestnet,
},
Action: func(cliCtx *cli.Context) error {
if err := ExportAccount(cliCtx); err != nil {
@@ -70,6 +77,8 @@ this command outputs a deposit data string which is required to become a validat
flags.WalletPasswordsDirFlag,
flags.BackupDirFlag,
flags.PasswordFileFlag,
featureconfig.AltonaTestnet,
featureconfig.MedallaTestnet,
},
Action: func(cliCtx *cli.Context) error {
if err := ImportAccount(cliCtx); err != nil {

View File

@@ -1,6 +1,7 @@
package v2
import (
"github.com/prysmaticlabs/prysm/shared/featureconfig"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/urfave/cli/v2"
)
@@ -24,6 +25,8 @@ var WalletCommands = &cli.Command{
flags.RemoteSignerKeyPathFlag,
flags.RemoteSignerCACertPathFlag,
flags.PasswordFileFlag,
featureconfig.AltonaTestnet,
featureconfig.MedallaTestnet,
},
Action: func(cliCtx *cli.Context) error {
if err := CreateWallet(cliCtx); err != nil {
@@ -41,6 +44,8 @@ var WalletCommands = &cli.Command{
flags.RemoteSignerCertPathFlag,
flags.RemoteSignerKeyPathFlag,
flags.RemoteSignerCACertPathFlag,
featureconfig.AltonaTestnet,
featureconfig.MedallaTestnet,
},
Action: func(cliCtx *cli.Context) error {
if err := EditWalletConfiguration(cliCtx); err != nil {
@@ -57,6 +62,8 @@ var WalletCommands = &cli.Command{
flags.WalletPasswordsDirFlag,
flags.MnemonicFileFlag,
flags.PasswordFileFlag,
featureconfig.AltonaTestnet,
featureconfig.MedallaTestnet,
},
Action: func(cliCtx *cli.Context) error {
if err := RecoverWallet(cliCtx); err != nil {

View File

@@ -1,4 +1,5 @@
// Package v2 defines a new model for accounts management in Prysm, using best
// practices for user security, UX, and extensibility via different wallet types
// including derived, HD wallets and remote-signing capable configurations.
// including HD wallets, non-HD wallets, and remote-signing capable configurations. This model
// is compliant with the EIP-2333, EIP-2334, and EIP-2335 standards for key management in eth2.
package v2

View File

@@ -32,8 +32,9 @@ func TestZipAndUnzip(t *testing.T) {
exportDir: exportDir,
keymanagerKind: v2keymanager.Direct,
})
wallet, err := NewWallet(cliCtx)
wallet, err := NewWallet(cliCtx, v2keymanager.Direct)
require.NoError(t, err)
require.NoError(t, wallet.SaveWallet())
ctx := context.Background()
keymanager, err := direct.NewKeymanager(
ctx,
@@ -84,8 +85,9 @@ func TestExport_Noninteractive(t *testing.T) {
accountsToExport: accounts,
keymanagerKind: v2keymanager.Direct,
})
wallet, err := NewWallet(cliCtx)
wallet, err := NewWallet(cliCtx, v2keymanager.Direct)
require.NoError(t, err)
require.NoError(t, wallet.SaveWallet())
ctx := context.Background()
keymanagerCfg := direct.DefaultConfig()
encodedCfg, err := direct.MarshalConfigFile(ctx, keymanagerCfg)

View File

@@ -40,8 +40,9 @@ func TestImport_Noninteractive(t *testing.T) {
keymanagerKind: v2keymanager.Direct,
passwordFile: passwordFilePath,
})
wallet, err := NewWallet(cliCtx)
wallet, err := NewWallet(cliCtx, v2keymanager.Direct)
require.NoError(t, err)
require.NoError(t, wallet.SaveWallet())
ctx := context.Background()
keymanagerCfg := direct.DefaultConfig()
encodedCfg, err := direct.MarshalConfigFile(ctx, keymanagerCfg)

View File

@@ -10,10 +10,12 @@ import (
"github.com/dustin/go-humanize"
"github.com/logrusorgru/aurora"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/shared/petnames"
"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/urfave/cli/v2"
)
@@ -47,6 +49,14 @@ func ListAccounts(cliCtx *cli.Context) error {
if err := listDerivedKeymanagerAccounts(showDepositData, wallet, km); err != nil {
return errors.Wrap(err, "could not list validator accounts with derived keymanager")
}
case v2keymanager.Remote:
km, ok := keymanager.(*remote.Keymanager)
if !ok {
return errors.New("could not assert keymanager interface to concrete type")
}
if err := listRemoteKeymanagerAccounts(wallet, km, km.Config()); err != nil {
return errors.Wrap(err, "could not list validator accounts with remote keymanager")
}
default:
return fmt.Errorf("keymanager kind %s not yet supported", wallet.KeymanagerKind().String())
}
@@ -75,7 +85,6 @@ func listDirectKeymanagerAccounts(
au.BrightRed("View the eth1 deposit transaction data for your accounts " +
"by running `validator accounts-v2 list --show-deposit-data"),
)
fmt.Printf("Keymanager kind: %s\n", au.BrightGreen(wallet.KeymanagerKind().String()).Bold())
ctx := context.Background()
pubKeys, err := keymanager.FetchValidatingPublicKeys(ctx)
@@ -152,6 +161,14 @@ func listDerivedKeymanagerAccounts(
if err != nil {
return err
}
if len(accountNames) == 1 {
fmt.Print("Showing 1 validator account\n")
} else if len(accountNames) == 0 {
fmt.Print("No accounts found\n")
return nil
} else {
fmt.Printf("Showing %d validator accounts\n", len(accountNames))
}
for i := uint64(0); i <= currentAccountNumber; i++ {
fmt.Println("")
validatingKeyPath := fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, i)
@@ -197,3 +214,42 @@ func listDerivedKeymanagerAccounts(
}
return nil
}
func listRemoteKeymanagerAccounts(
wallet *Wallet,
keymanager v2keymanager.IKeymanager,
cfg *remote.Config,
) error {
au := aurora.NewAurora(true)
fmt.Printf("(keymanager kind) %s\n", au.BrightGreen("remote signer").Bold())
fmt.Printf(
"(configuration file path) %s\n",
au.BrightGreen(filepath.Join(wallet.AccountsDir(), KeymanagerConfigFileName)).Bold(),
)
ctx := context.Background()
fmt.Println(" ")
fmt.Printf("%s\n", au.BrightGreen("Configuration options").Bold())
fmt.Println(cfg)
validatingPubKeys, err := keymanager.FetchValidatingPublicKeys(ctx)
if err != nil {
return errors.Wrap(err, "could not fetch validating public keys")
}
if len(validatingPubKeys) == 1 {
fmt.Print("Showing 1 validator account\n")
} else if len(validatingPubKeys) == 0 {
fmt.Print("No accounts found\n")
return nil
} else {
fmt.Printf("Showing %d validator accounts\n", len(validatingPubKeys))
}
for i := 0; i < len(validatingPubKeys); i++ {
fmt.Println("")
fmt.Printf(
"%s\n", au.BrightGreen(petnames.DeterministicName(validatingPubKeys[i][:], "-")).Bold(),
)
// Retrieve the validating key account metadata.
fmt.Printf("%s %#x\n", au.BrightCyan("[validating public key]").Bold(), validatingPubKeys[i])
fmt.Println(" ")
}
return nil
}

View File

@@ -2,22 +2,43 @@ package v2
import (
"context"
"crypto/rand"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/dustin/go-humanize"
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/petnames"
"github.com/prysmaticlabs/prysm/shared/testutil"
"github.com/prysmaticlabs/prysm/shared/testutil/assert"
"github.com/prysmaticlabs/prysm/shared/testutil/require"
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"
)
type mockKeymanager struct {
publicKeys [][48]byte
}
func (m *mockKeymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) {
return m.publicKeys, nil
}
func (m *mockKeymanager) Sign(context.Context, *validatorpb.SignRequest) (bls.Signature, error) {
return nil, nil
}
func TestListAccounts_DirectKeymanager(t *testing.T) {
walletDir, passwordsDir := setupWalletAndPasswordsDir(t)
cliCtx := setupWalletCtx(t, &testWalletConfig{
@@ -25,8 +46,9 @@ func TestListAccounts_DirectKeymanager(t *testing.T) {
passwordsDir: passwordsDir,
keymanagerKind: v2keymanager.Direct,
})
wallet, err := NewWallet(cliCtx)
wallet, err := NewWallet(cliCtx, v2keymanager.Direct)
require.NoError(t, err)
require.NoError(t, wallet.SaveWallet())
ctx := context.Background()
keymanager, err := direct.NewKeymanager(
ctx,
@@ -106,15 +128,29 @@ func TestListAccounts_DirectKeymanager(t *testing.T) {
func TestListAccounts_DerivedKeymanager(t *testing.T) {
walletDir, passwordsDir := setupWalletAndPasswordsDir(t)
randPath, err := rand.Int(rand.Reader, big.NewInt(1000000))
require.NoError(t, err)
passwordFileDir := filepath.Join(testutil.TempDir(), fmt.Sprintf("/%d", randPath), "passwords-file")
require.NoError(t, os.MkdirAll(passwordFileDir, os.ModePerm))
t.Cleanup(func() {
require.NoError(t, os.RemoveAll(passwordFileDir), "Failed to remove directory")
})
passwordFilePath := filepath.Join(passwordFileDir, passwordFileName)
password := "PasszW0rdzzz2%"
require.NoError(
t,
ioutil.WriteFile(passwordFilePath, []byte(password), os.ModePerm),
)
cliCtx := setupWalletCtx(t, &testWalletConfig{
walletDir: walletDir,
passwordsDir: passwordsDir,
keymanagerKind: v2keymanager.Derived,
passwordFile: passwordFilePath,
})
wallet, err := NewWallet(cliCtx)
wallet, err := NewWallet(cliCtx, v2keymanager.Derived)
require.NoError(t, err)
require.NoError(t, wallet.SaveWallet())
ctx := context.Background()
password := "hello world"
seedConfig, err := derived.InitializeWalletSeedFile(ctx, password, true /* skip confirm */)
require.NoError(t, err)
@@ -167,7 +203,7 @@ func TestListAccounts_DerivedKeymanager(t *testing.T) {
t.Error("Did not find Keymanager kind in output")
}
// Assert the wallet and passwords paths are in stdout.
// Assert the wallet accounts path is in stdout.
if !strings.Contains(stringOutput, wallet.accountsPath) {
t.Errorf("Did not find accounts path %s in output", wallet.accountsPath)
}
@@ -203,3 +239,75 @@ func TestListAccounts_DerivedKeymanager(t *testing.T) {
assert.Equal(t, strings.Contains(stringOutput, humanize.Time(unixTimestamp)), true)
}
}
func TestListAccounts_RemoteKeymanager(t *testing.T) {
walletDir, _ := setupWalletAndPasswordsDir(t)
cliCtx := setupWalletCtx(t, &testWalletConfig{
walletDir: walletDir,
keymanagerKind: v2keymanager.Remote,
})
wallet, err := NewWallet(cliCtx, v2keymanager.Remote)
require.NoError(t, err)
require.NoError(t, wallet.SaveWallet())
rescueStdout := os.Stdout
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w
numAccounts := 3
pubKeys := make([][48]byte, numAccounts)
for i := 0; i < numAccounts; i++ {
key := make([]byte, 48)
copy(key, strconv.Itoa(i))
pubKeys[i] = bytesutil.ToBytes48(key)
}
km := &mockKeymanager{
publicKeys: pubKeys,
}
// We call the list remote keymanager accounts function.
cfg := &remote.Config{
RemoteCertificate: &remote.CertificateConfig{
ClientCertPath: "/tmp/client.crt",
ClientKeyPath: "/tmp/client.key",
CACertPath: "/tmp/ca.crt",
},
RemoteAddr: "localhost:4000",
}
require.NoError(t, listRemoteKeymanagerAccounts(wallet, km, cfg))
require.NoError(t, w.Close())
out, err := ioutil.ReadAll(r)
require.NoError(t, err)
os.Stdout = rescueStdout
// Assert the keymanager kind is printed to stdout.
stringOutput := string(out)
if !strings.Contains(stringOutput, wallet.KeymanagerKind().String()) {
t.Error("Did not find keymanager kind in output")
}
// Assert the keymanager configuration is printed to stdout.
if !strings.Contains(stringOutput, cfg.String()) {
t.Error("Did not find remote config in output")
}
// Assert the wallet accounts path is in stdout.
if !strings.Contains(stringOutput, wallet.accountsPath) {
t.Errorf("Did not find accounts path %s in output", wallet.accountsPath)
}
for i := 0; i < numAccounts; i++ {
accountName := petnames.DeterministicName(pubKeys[i][:], "-")
// Assert the account name is printed to stdout.
if !strings.Contains(stringOutput, accountName) {
t.Errorf("Did not find account %s in output", accountName)
}
key := pubKeys[i]
// Assert every public key is printed to stdout.
if !strings.Contains(stringOutput, fmt.Sprintf("%#x", key)) {
t.Errorf("Did not find pubkey %#x in output", key)
}
}
}

View File

@@ -35,6 +35,11 @@ func Test_validatePasswordInput(t *testing.T) {
input: "aaaaaaa1$",
wantErr: true,
},
{
name: "Unicode strings separated by a space character",
input: "x*329293@aAJSD i22903saj",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -44,3 +49,39 @@ func Test_validatePasswordInput(t *testing.T) {
})
}
}
func Test_isValidUnicode(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{
name: "Regular alphanumeric",
input: "Someone23xx",
want: true,
},
{
name: "Unicode strings separated by a space character",
input: "x*329293@aAJSD i22903saj",
want: false,
},
{
name: "Japanese",
input: "僕は絵お見るのが好きです",
want: true,
},
{
name: "Other foreign",
input: "Etérium",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isValidUnicode(tt.input); got != tt.want {
t.Errorf("isValidUnicode() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -168,6 +168,8 @@ func validatePasswordInput(input string) error {
}
for _, char := range input {
switch {
case !(unicode.IsLetter(char) || unicode.IsNumber(char) || unicode.IsPunct(char) || unicode.IsSymbol(char)):
return errors.New("password must only contain alphanumeric characters, punctuation, or symbols")
case unicode.IsLetter(char):
hasLetter = true
case unicode.IsNumber(char):
@@ -195,7 +197,11 @@ func inputRemoteKeymanagerConfig(cliCtx *cli.Context) (*remote.Config, error) {
crt := cliCtx.String(flags.RemoteSignerCertPathFlag.Name)
key := cliCtx.String(flags.RemoteSignerKeyPathFlag.Name)
ca := cliCtx.String(flags.RemoteSignerCACertPathFlag.Name)
if addr != "" && crt != "" && key != "" && ca != "" {
if !(isValidUnicode(addr) && isValidUnicode(crt) && isValidUnicode(key) && isValidUnicode(ca)) {
return nil, errors.New("flag inputs contain non-unicode characters")
}
newCfg := &remote.Config{
RemoteCertificate: &remote.CertificateConfig{
ClientCertPath: strings.TrimRight(crt, "\r\n"),
@@ -215,6 +221,9 @@ func inputRemoteKeymanagerConfig(cliCtx *cli.Context) (*remote.Config, error) {
if input == "" {
return errors.New("remote host address cannot be empty")
}
if !isValidUnicode(input) {
return errors.New("not valid unicode")
}
return nil
},
}
@@ -240,7 +249,7 @@ func inputRemoteKeymanagerConfig(cliCtx *cli.Context) (*remote.Config, error) {
}
prompt = promptui.Prompt{
Label: "(Optional) Path to certificate authority (CA) crt (such as /path/to/ca.crt)",
Validate: validateCertPath,
Validate: validateCACertPath,
}
caCrtPath, err := prompt.Run()
if err != nil {
@@ -262,12 +271,25 @@ func validateCertPath(input string) error {
if input == "" {
return errors.New("crt path cannot be empty")
}
if !isValidUnicode(input) {
return errors.New("not valid unicode")
}
if !fileExists(input) {
return fmt.Errorf("no crt found at path: %s", input)
}
return nil
}
func validateCACertPath(input string) error {
if input != "" && !fileExists(input) {
return fmt.Errorf("no crt found at path: %s", input)
}
if !isValidUnicode(input) {
return errors.New("not valid unicode")
}
return nil
}
func formatPromptError(err error) error {
switch err {
case promptui.ErrAbort:
@@ -280,3 +302,18 @@ func formatPromptError(err error) error {
return err
}
}
// Checks if an input string is a valid unicode string comprised of only
// letters, numbers, punctuation, or symbols.
func isValidUnicode(input string) bool {
for _, char := range input {
if !(unicode.IsLetter(char) ||
unicode.IsNumber(char) ||
unicode.IsPunct(char) ||
unicode.IsSymbol(char)) {
log.Info(char)
return false
}
}
return true
}

View File

@@ -14,6 +14,7 @@ import (
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"
)
@@ -60,6 +61,7 @@ func init() {
// create and write a new wallet to disk for a Prysm validator.
func NewWallet(
cliCtx *cli.Context,
keymanagerKind v2keymanager.Kind,
) (*Wallet, error) {
walletDir, err := inputDirectory(cliCtx, walletDirPromptText, flags.WalletDirFlag)
if err != nil && !errors.Is(err, ErrNoWalletFound) {
@@ -78,18 +80,18 @@ func NewWallet(
"edit your wallet configuration by running ./prysm.sh validator wallet-v2 edit",
)
}
keymanagerKind, err := inputKeymanagerKind(cliCtx)
if err != nil {
return nil, err
}
accountsPath := filepath.Join(walletDir, keymanagerKind.String())
if err := os.MkdirAll(accountsPath, DirectoryPermissions); err != nil {
return nil, errors.Wrap(err, "could not create wallet directory")
}
w := &Wallet{
accountsPath: accountsPath,
keymanagerKind: keymanagerKind,
}
if keymanagerKind == v2keymanager.Derived {
walletPassword, err := inputPassword(cliCtx, newWalletPasswordPromptText, confirmPass)
if err != nil {
return nil, err
}
w.walletPassword = walletPassword
}
if keymanagerKind == v2keymanager.Direct {
passwordsDir, err := inputDirectory(cliCtx, passwordsDirPromptText, flags.WalletPasswordsDirFlag)
if err != nil {
@@ -140,6 +142,19 @@ func OpenWallet(cliCtx *cli.Context) (*Wallet, error) {
return w, 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")
}
if w.keymanagerKind == v2keymanager.Direct {
if err := os.MkdirAll(w.passwordsDir, DirectoryPermissions); err != nil {
return errors.Wrap(err, "could not create passwords directory")
}
}
return nil
}
// KeymanagerKind used by the wallet.
func (w *Wallet) KeymanagerKind() v2keymanager.Kind {
return w.keymanagerKind
@@ -180,6 +195,15 @@ func (w *Wallet) InitializeKeymanager(
if err != nil {
return nil, errors.Wrap(err, "could not initialize derived keymanager")
}
case v2keymanager.Remote:
cfg, err := remote.UnmarshalConfigFile(configFile)
if err != nil {
return nil, errors.Wrap(err, "could not unmarshal keymanager config file")
}
keymanager, err = remote.NewKeymanager(ctx, 100000000, cfg)
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)
}

View File

@@ -16,7 +16,11 @@ import (
// wallet already exists in the path, it suggests the user alternatives
// such as how to edit their existing wallet configuration.
func CreateWallet(cliCtx *cli.Context) error {
w, err := NewWallet(cliCtx)
keymanagerKind, err := inputKeymanagerKind(cliCtx)
if err != nil {
return err
}
w, err := NewWallet(cliCtx, keymanagerKind)
if err != nil {
return errors.Wrap(err, "could not check if wallet directory exists")
}
@@ -51,6 +55,9 @@ func CreateWallet(cliCtx *cli.Context) error {
}
func createDirectKeymanagerWallet(cliCtx *cli.Context, wallet *Wallet) error {
if err := wallet.SaveWallet(); err != nil {
return errors.Wrap(err, "could not save wallet to disk")
}
keymanagerConfig, err := direct.MarshalConfigFile(context.Background(), direct.DefaultConfig())
if err != nil {
return errors.Wrap(err, "could not marshal keymanager config file")
@@ -64,11 +71,7 @@ func createDirectKeymanagerWallet(cliCtx *cli.Context, wallet *Wallet) error {
func createDerivedKeymanagerWallet(cliCtx *cli.Context, wallet *Wallet) error {
skipMnemonicConfirm := cliCtx.Bool(flags.SkipMnemonicConfirmFlag.Name)
ctx := context.Background()
walletPassword, err := inputPassword(cliCtx, newWalletPasswordPromptText, confirmPass)
if err != nil {
return err
}
seedConfig, err := derived.InitializeWalletSeedFile(ctx, walletPassword, skipMnemonicConfirm)
seedConfig, err := derived.InitializeWalletSeedFile(ctx, wallet.walletPassword, skipMnemonicConfirm)
if err != nil {
return errors.Wrap(err, "could not initialize new wallet seed file")
}
@@ -80,6 +83,9 @@ func createDerivedKeymanagerWallet(cliCtx *cli.Context, wallet *Wallet) error {
if err != nil {
return errors.Wrap(err, "could not marshal keymanager config file")
}
if err := wallet.SaveWallet(); err != nil {
return errors.Wrap(err, "could not save wallet to disk")
}
if err := wallet.WriteKeymanagerConfigToDisk(ctx, keymanagerConfig); err != nil {
return errors.Wrap(err, "could not write keymanager config to disk")
}
@@ -99,6 +105,9 @@ func createRemoteKeymanagerWallet(cliCtx *cli.Context, wallet *Wallet) error {
if err != nil {
return errors.Wrap(err, "could not marshal config file")
}
if err := wallet.SaveWallet(); err != nil {
return errors.Wrap(err, "could not save wallet to disk")
}
if err := wallet.WriteKeymanagerConfigToDisk(ctx, keymanagerConfig); err != nil {
return errors.Wrap(err, "could not write keymanager config to disk")
}

View File

@@ -19,8 +19,9 @@ func TestEditWalletConfiguration(t *testing.T) {
walletDir: walletDir,
keymanagerKind: v2keymanager.Remote,
})
wallet, err := NewWallet(cliCtx)
wallet, err := NewWallet(cliCtx, v2keymanager.Remote)
require.NoError(t, err)
require.NoError(t, wallet.SaveWallet())
ctx := context.Background()
originalCfg := &remote.Config{

View File

@@ -9,6 +9,7 @@ import (
"github.com/manifoldco/promptui"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/validator/flags"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/derived"
"github.com/urfave/cli/v2"
)
@@ -21,7 +22,7 @@ func RecoverWallet(cliCtx *cli.Context) error {
if err != nil {
return errors.Wrap(err, "could not get mnemonic phrase")
}
wallet, err := NewWallet(cliCtx)
wallet, err := NewWallet(cliCtx, v2keymanager.Derived)
if err != nil {
return errors.Wrap(err, "could not create new wallet")
}
@@ -38,6 +39,9 @@ func RecoverWallet(cliCtx *cli.Context) error {
if err != nil {
return errors.Wrap(err, "could not marshal keymanager config file")
}
if err := wallet.SaveWallet(); err != nil {
return errors.Wrap(err, "could not save wallet to disk")
}
if err := wallet.WriteKeymanagerConfigToDisk(ctx, keymanagerConfig); err != nil {
return errors.Wrap(err, "could not write keymanager config to disk")
}

View File

@@ -75,7 +75,8 @@ func TestCreateAndReadWallet(t *testing.T) {
passwordsDir: passwordsDir,
keymanagerKind: v2keymanager.Direct,
})
_, err := NewWallet(cliCtx)
wallet, err := NewWallet(cliCtx, v2keymanager.Direct)
require.NoError(t, wallet.SaveWallet())
require.NoError(t, err)
// We should be able to now read the wallet as well.
_, err = OpenWallet(cliCtx)

View File

@@ -44,11 +44,13 @@ go_test(
"//proto/validator/accounts/v2:go_default_library",
"//shared/bls:go_default_library",
"//shared/bytesutil:go_default_library",
"//shared/rand:go_default_library",
"//shared/testutil:go_default_library",
"//shared/testutil/assert:go_default_library",
"//shared/testutil/require:go_default_library",
"//validator/accounts/v2/testing:go_default_library",
"//validator/keymanager/v2:go_default_library",
"@com_github_google_uuid//:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
"@com_github_tyler_smith_go_bip39//:go_default_library",
"@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library",

View File

@@ -207,7 +207,7 @@ func InitializeWalletSeedFile(ctx context.Context, password string, skipMnemonic
// SeedFileFromMnemonic uses the provided mnemonic seed phrase to generate the
// appropriate seed file for recovering a derived wallets.
func SeedFileFromMnemonic(ctx context.Context, mnemonic string, password string) (*SeedConfig, error) {
walletSeed, err := bip39.MnemonicToByteArray(mnemonic)
walletSeed, err := bip39.EntropyFromMnemonic(mnemonic)
if err != nil {
return nil, errors.Wrap(err, "could not convert mnemonic to wallet seed")
}

View File

@@ -9,18 +9,68 @@ import (
"strings"
"testing"
"github.com/google/uuid"
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/rand"
"github.com/prysmaticlabs/prysm/shared/testutil"
"github.com/prysmaticlabs/prysm/shared/testutil/assert"
"github.com/prysmaticlabs/prysm/shared/testutil/require"
mock "github.com/prysmaticlabs/prysm/validator/accounts/v2/testing"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
logTest "github.com/sirupsen/logrus/hooks/test"
"github.com/tyler-smith/go-bip39"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
func TestDerivedKeymanager_RecoverSeedRoundTrip(t *testing.T) {
walletSeed := make([]byte, 32)
n, err := rand.NewGenerator().Read(walletSeed)
require.NoError(t, err)
require.Equal(t, n, len(walletSeed))
encryptor := keystorev4.New()
password := "Passwz0rdz2020%"
cryptoFields, err := encryptor.Encrypt(walletSeed, []byte(password))
require.NoError(t, err)
id, err := uuid.NewRandom()
require.NoError(t, err)
cfg := &SeedConfig{
Crypto: cryptoFields,
ID: id.String(),
NextAccount: 0,
Version: encryptor.Version(),
Name: encryptor.Name(),
}
phrase, err := bip39.NewMnemonic(walletSeed)
require.NoError(t, err)
recoveredSeed, err := bip39.EntropyFromMnemonic(phrase)
require.NoError(t, err)
// Ensure the recovered seed matches the old wallet seed.
assert.DeepEqual(t, walletSeed, recoveredSeed)
cryptoFields, err = encryptor.Encrypt(recoveredSeed, []byte(password))
require.NoError(t, err)
newCfg := &SeedConfig{
Crypto: cryptoFields,
ID: cfg.ID,
NextAccount: 0,
Version: encryptor.Version(),
Name: encryptor.Name(),
}
// Ensure we can decrypt the newly recovered config.
decryptor := keystorev4.New()
seed, err := decryptor.Decrypt(newCfg.Crypto, []byte(password))
assert.NoError(t, err)
// Ensure the decrypted seed matches the old wallet seed and the new wallet seed.
assert.DeepEqual(t, walletSeed, seed)
assert.DeepEqual(t, recoveredSeed, seed)
}
func TestDerivedKeymanager_CreateAccount(t *testing.T) {
hook := logTest.NewGlobal()
wallet := &mock.Wallet{

View File

@@ -151,6 +151,7 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, password string) (strin
===================================================================
`, withdrawalKey.Marshal())
fmt.Println(" ")
// Upon confirmation of the withdrawal key, proceed to display
// and write associated deposit data to disk.

View File

@@ -164,9 +164,9 @@ func (c *Config) String() string {
return b.String()
}
// CreateAccount based on the keymanager's logic. Returns the account name.
func (k *Keymanager) CreateAccount(ctx context.Context, password string) (string, error) {
return "", errors.New("a remote validator account cannot be created from the client")
// Config for the remote keymanager.
func (k *Keymanager) Config() *Config {
return k.cfg
}
// FetchValidatingPublicKeys fetches the list of public keys that should be used to validate with.