github.com/prysmaticlabs/prysm@v1.4.4/validator/accounts/accounts_import_test.go (about)

     1  package accounts
     2  
     3  import (
     4  	"crypto/rand"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"math/big"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/google/uuid"
    16  	"github.com/prysmaticlabs/prysm/shared/bls"
    17  	"github.com/prysmaticlabs/prysm/shared/bytesutil"
    18  	"github.com/prysmaticlabs/prysm/shared/params"
    19  	"github.com/prysmaticlabs/prysm/shared/testutil/assert"
    20  	"github.com/prysmaticlabs/prysm/shared/testutil/require"
    21  	"github.com/prysmaticlabs/prysm/shared/timeutils"
    22  	"github.com/prysmaticlabs/prysm/validator/accounts/iface"
    23  	"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
    24  	"github.com/prysmaticlabs/prysm/validator/keymanager"
    25  	"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
    26  	keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
    27  )
    28  
    29  func TestImport_Noninteractive(t *testing.T) {
    30  	imported.ResetCaches()
    31  	walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t)
    32  	keysDir := filepath.Join(t.TempDir(), "keysDir")
    33  	require.NoError(t, os.MkdirAll(keysDir, os.ModePerm))
    34  
    35  	cliCtx := setupWalletCtx(t, &testWalletConfig{
    36  		walletDir:           walletDir,
    37  		passwordsDir:        passwordsDir,
    38  		keysDir:             keysDir,
    39  		keymanagerKind:      keymanager.Imported,
    40  		walletPasswordFile:  passwordFilePath,
    41  		accountPasswordFile: passwordFilePath,
    42  	})
    43  	w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{
    44  		WalletCfg: &wallet.Config{
    45  			WalletDir:      walletDir,
    46  			KeymanagerKind: keymanager.Imported,
    47  			WalletPassword: password,
    48  		},
    49  	})
    50  	require.NoError(t, err)
    51  	keymanager, err := imported.NewKeymanager(
    52  		cliCtx.Context,
    53  		&imported.SetupConfig{
    54  			Wallet:           w,
    55  			ListenForChanges: false,
    56  		},
    57  	)
    58  	require.NoError(t, err)
    59  
    60  	// Make sure there are no accounts at the start.
    61  	accounts, err := keymanager.ValidatingAccountNames()
    62  	require.NoError(t, err)
    63  	assert.Equal(t, len(accounts), 0)
    64  
    65  	// Create 2 keys.
    66  	createKeystore(t, keysDir)
    67  	time.Sleep(time.Second)
    68  	createKeystore(t, keysDir)
    69  
    70  	require.NoError(t, ImportAccountsCli(cliCtx))
    71  
    72  	w, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{
    73  		WalletDir:      walletDir,
    74  		WalletPassword: password,
    75  	})
    76  	require.NoError(t, err)
    77  	km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false})
    78  	require.NoError(t, err)
    79  	keys, err := km.FetchValidatingPublicKeys(cliCtx.Context)
    80  	require.NoError(t, err)
    81  
    82  	assert.Equal(t, 2, len(keys))
    83  }
    84  
    85  // TestImport_DuplicateKeys is a regression test that ensures correction function if duplicate keys are being imported
    86  func TestImport_DuplicateKeys(t *testing.T) {
    87  	imported.ResetCaches()
    88  	walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t)
    89  	keysDir := filepath.Join(t.TempDir(), "keysDir")
    90  	require.NoError(t, os.MkdirAll(keysDir, os.ModePerm))
    91  
    92  	cliCtx := setupWalletCtx(t, &testWalletConfig{
    93  		walletDir:           walletDir,
    94  		passwordsDir:        passwordsDir,
    95  		keysDir:             keysDir,
    96  		keymanagerKind:      keymanager.Imported,
    97  		walletPasswordFile:  passwordFilePath,
    98  		accountPasswordFile: passwordFilePath,
    99  	})
   100  	w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{
   101  		WalletCfg: &wallet.Config{
   102  			WalletDir:      walletDir,
   103  			KeymanagerKind: keymanager.Imported,
   104  			WalletPassword: password,
   105  		},
   106  	})
   107  	require.NoError(t, err)
   108  
   109  	// Create a key and then copy it to create a duplicate
   110  	_, keystorePath := createKeystore(t, keysDir)
   111  	time.Sleep(time.Second)
   112  	input, err := ioutil.ReadFile(keystorePath)
   113  	require.NoError(t, err)
   114  	keystorePath2 := filepath.Join(keysDir, "copyOfKeystore.json")
   115  	err = ioutil.WriteFile(keystorePath2, input, os.ModePerm)
   116  	require.NoError(t, err)
   117  
   118  	require.NoError(t, ImportAccountsCli(cliCtx))
   119  
   120  	_, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{
   121  		WalletDir:      walletDir,
   122  		WalletPassword: password,
   123  	})
   124  	require.NoError(t, err)
   125  	km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false})
   126  	require.NoError(t, err)
   127  	keys, err := km.FetchValidatingPublicKeys(cliCtx.Context)
   128  	require.NoError(t, err)
   129  
   130  	// There should only be 1 account as the duplicate keystore was ignored
   131  	assert.Equal(t, 1, len(keys))
   132  }
   133  
   134  // TestImport_NonImportedWallet is a regression test that ensures non-silent failure when importing to non-imported wallets
   135  func TestImport_NonImportedWallet(t *testing.T) {
   136  	walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t)
   137  	keysDir := filepath.Join(t.TempDir(), "keysDir")
   138  	require.NoError(t, os.MkdirAll(keysDir, os.ModePerm))
   139  
   140  	cliCtx := setupWalletCtx(t, &testWalletConfig{
   141  		walletDir:          walletDir,
   142  		passwordsDir:       passwordsDir,
   143  		keysDir:            keysDir,
   144  		keymanagerKind:     keymanager.Derived,
   145  		walletPasswordFile: passwordFilePath,
   146  	})
   147  	_, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{
   148  		WalletCfg: &wallet.Config{
   149  			WalletDir:      walletDir,
   150  			KeymanagerKind: keymanager.Derived,
   151  			WalletPassword: password,
   152  		},
   153  	})
   154  	require.NoError(t, err)
   155  
   156  	// Create a key
   157  	createKeystore(t, keysDir)
   158  	require.ErrorContains(t, "only imported wallets", ImportAccountsCli(cliCtx))
   159  }
   160  
   161  func TestImport_Noninteractive_RandomName(t *testing.T) {
   162  	imported.ResetCaches()
   163  	walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t)
   164  	keysDir := filepath.Join(t.TempDir(), "keysDir")
   165  	require.NoError(t, os.MkdirAll(keysDir, os.ModePerm))
   166  
   167  	cliCtx := setupWalletCtx(t, &testWalletConfig{
   168  		walletDir:           walletDir,
   169  		passwordsDir:        passwordsDir,
   170  		keysDir:             keysDir,
   171  		keymanagerKind:      keymanager.Imported,
   172  		walletPasswordFile:  passwordFilePath,
   173  		accountPasswordFile: passwordFilePath,
   174  	})
   175  	w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{
   176  		WalletCfg: &wallet.Config{
   177  			WalletDir:      walletDir,
   178  			KeymanagerKind: keymanager.Imported,
   179  			WalletPassword: password,
   180  		},
   181  	})
   182  	require.NoError(t, err)
   183  	keymanager, err := imported.NewKeymanager(
   184  		cliCtx.Context,
   185  		&imported.SetupConfig{
   186  			Wallet:           w,
   187  			ListenForChanges: false,
   188  		},
   189  	)
   190  	require.NoError(t, err)
   191  
   192  	// Make sure there are no accounts at the start.
   193  	accounts, err := keymanager.ValidatingAccountNames()
   194  	require.NoError(t, err)
   195  	assert.Equal(t, len(accounts), 0)
   196  
   197  	// Create 2 keys.
   198  	createRandomNameKeystore(t, keysDir)
   199  	time.Sleep(time.Second)
   200  	createRandomNameKeystore(t, keysDir)
   201  
   202  	require.NoError(t, ImportAccountsCli(cliCtx))
   203  
   204  	w, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{
   205  		WalletDir:      walletDir,
   206  		WalletPassword: password,
   207  	})
   208  	require.NoError(t, err)
   209  	km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false})
   210  	require.NoError(t, err)
   211  	keys, err := km.FetchValidatingPublicKeys(cliCtx.Context)
   212  	require.NoError(t, err)
   213  
   214  	assert.Equal(t, 2, len(keys))
   215  }
   216  
   217  func TestImport_Noninteractive_Filepath(t *testing.T) {
   218  	imported.ResetCaches()
   219  	walletDir, passwordsDir, passwordFilePath := setupWalletAndPasswordsDir(t)
   220  	keysDir := filepath.Join(t.TempDir(), "keysDir")
   221  	require.NoError(t, os.MkdirAll(keysDir, os.ModePerm))
   222  
   223  	_, keystorePath := createKeystore(t, keysDir)
   224  	cliCtx := setupWalletCtx(t, &testWalletConfig{
   225  		walletDir:           walletDir,
   226  		passwordsDir:        passwordsDir,
   227  		keysDir:             keystorePath,
   228  		keymanagerKind:      keymanager.Imported,
   229  		walletPasswordFile:  passwordFilePath,
   230  		accountPasswordFile: passwordFilePath,
   231  	})
   232  	w, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{
   233  		WalletCfg: &wallet.Config{
   234  			WalletDir:      walletDir,
   235  			KeymanagerKind: keymanager.Imported,
   236  			WalletPassword: password,
   237  		},
   238  	})
   239  	require.NoError(t, err)
   240  	keymanager, err := imported.NewKeymanager(
   241  		cliCtx.Context,
   242  		&imported.SetupConfig{
   243  			Wallet:           w,
   244  			ListenForChanges: false,
   245  		},
   246  	)
   247  	require.NoError(t, err)
   248  
   249  	// Make sure there are no accounts at the start.
   250  	accounts, err := keymanager.ValidatingAccountNames()
   251  	require.NoError(t, err)
   252  	assert.Equal(t, len(accounts), 0)
   253  
   254  	require.NoError(t, ImportAccountsCli(cliCtx))
   255  
   256  	w, err = wallet.OpenWallet(cliCtx.Context, &wallet.Config{
   257  		WalletDir:      walletDir,
   258  		WalletPassword: password,
   259  	})
   260  	require.NoError(t, err)
   261  	km, err := w.InitializeKeymanager(cliCtx.Context, iface.InitKeymanagerConfig{ListenForChanges: false})
   262  	require.NoError(t, err)
   263  	keys, err := km.FetchValidatingPublicKeys(cliCtx.Context)
   264  	require.NoError(t, err)
   265  
   266  	assert.Equal(t, 1, len(keys))
   267  }
   268  
   269  func TestImport_SortByDerivationPath(t *testing.T) {
   270  	imported.ResetCaches()
   271  	type test struct {
   272  		name  string
   273  		input []string
   274  		want  []string
   275  	}
   276  	tests := []test{
   277  		{
   278  			name: "Basic sort",
   279  			input: []string{
   280  				"keystore_m_12381_3600_2_0_0.json",
   281  				"keystore_m_12381_3600_1_0_0.json",
   282  				"keystore_m_12381_3600_0_0_0.json",
   283  			},
   284  			want: []string{
   285  				"keystore_m_12381_3600_0_0_0.json",
   286  				"keystore_m_12381_3600_1_0_0.json",
   287  				"keystore_m_12381_3600_2_0_0.json",
   288  			},
   289  		},
   290  		{
   291  			name: "Large digit accounts",
   292  			input: []string{
   293  				"keystore_m_12381_3600_30020330_0_0.json",
   294  				"keystore_m_12381_3600_430490934_0_0.json",
   295  				"keystore_m_12381_3600_0_0_0.json",
   296  				"keystore_m_12381_3600_333_0_0.json",
   297  			},
   298  			want: []string{
   299  				"keystore_m_12381_3600_0_0_0.json",
   300  				"keystore_m_12381_3600_333_0_0.json",
   301  				"keystore_m_12381_3600_30020330_0_0.json",
   302  				"keystore_m_12381_3600_430490934_0_0.json",
   303  			},
   304  		},
   305  		{
   306  			name: "Some filenames with derivation path, others without",
   307  			input: []string{
   308  				"keystore_m_12381_3600_4_0_0.json",
   309  				"keystore.json",
   310  				"keystore-2309023.json",
   311  				"keystore_m_12381_3600_1_0_0.json",
   312  				"keystore_m_12381_3600_3_0_0.json",
   313  			},
   314  			want: []string{
   315  				"keystore_m_12381_3600_1_0_0.json",
   316  				"keystore_m_12381_3600_3_0_0.json",
   317  				"keystore_m_12381_3600_4_0_0.json",
   318  				"keystore.json",
   319  				"keystore-2309023.json",
   320  			},
   321  		},
   322  	}
   323  	for _, tt := range tests {
   324  		t.Run(tt.name, func(t *testing.T) {
   325  			sort.Sort(byDerivationPath(tt.input))
   326  			assert.DeepEqual(t, tt.want, tt.input)
   327  		})
   328  	}
   329  }
   330  
   331  func Test_importPrivateKeyAsAccount(t *testing.T) {
   332  	walletDir, _, passwordFilePath := setupWalletAndPasswordsDir(t)
   333  	privKeyDir := filepath.Join(t.TempDir(), "privKeys")
   334  	require.NoError(t, os.MkdirAll(privKeyDir, os.ModePerm))
   335  	privKeyFileName := filepath.Join(privKeyDir, "privatekey.txt")
   336  
   337  	// We create a new private key and save it to a file on disk.
   338  	privKey, err := bls.RandKey()
   339  	require.NoError(t, err)
   340  	privKeyHex := fmt.Sprintf("%x", privKey.Marshal())
   341  	require.NoError(
   342  		t,
   343  		ioutil.WriteFile(privKeyFileName, []byte(privKeyHex), params.BeaconIoConfig().ReadWritePermissions),
   344  	)
   345  
   346  	// We instantiate a new wallet from a cli context.
   347  	cliCtx := setupWalletCtx(t, &testWalletConfig{
   348  		walletDir:          walletDir,
   349  		keymanagerKind:     keymanager.Imported,
   350  		walletPasswordFile: passwordFilePath,
   351  		privateKeyFile:     privKeyFileName,
   352  	})
   353  	walletPass := "Passwordz0320$"
   354  	wallet, err := CreateWalletWithKeymanager(cliCtx.Context, &CreateWalletConfig{
   355  		WalletCfg: &wallet.Config{
   356  			WalletDir:      walletDir,
   357  			KeymanagerKind: keymanager.Imported,
   358  			WalletPassword: walletPass,
   359  		},
   360  	})
   361  	require.NoError(t, err)
   362  	keymanager, err := imported.NewKeymanager(
   363  		cliCtx.Context,
   364  		&imported.SetupConfig{
   365  			Wallet:           wallet,
   366  			ListenForChanges: false,
   367  		},
   368  	)
   369  	require.NoError(t, err)
   370  	assert.NoError(t, importPrivateKeyAsAccount(cliCtx, wallet, keymanager))
   371  
   372  	// We re-instantiate the keymanager and check we now have 1 public key.
   373  	keymanager, err = imported.NewKeymanager(
   374  		cliCtx.Context,
   375  		&imported.SetupConfig{
   376  			Wallet:           wallet,
   377  			ListenForChanges: false,
   378  		},
   379  	)
   380  	require.NoError(t, err)
   381  	pubKeys, err := keymanager.FetchValidatingPublicKeys(cliCtx.Context)
   382  	require.NoError(t, err)
   383  	require.Equal(t, 1, len(pubKeys))
   384  	assert.DeepEqual(t, pubKeys[0], bytesutil.ToBytes48(privKey.PublicKey().Marshal()))
   385  }
   386  
   387  // Returns the fullPath to the newly created keystore file.
   388  func createKeystore(t *testing.T, path string) (*keymanager.Keystore, string) {
   389  	validatingKey, err := bls.RandKey()
   390  	require.NoError(t, err)
   391  	encryptor := keystorev4.New()
   392  	cryptoFields, err := encryptor.Encrypt(validatingKey.Marshal(), password)
   393  	require.NoError(t, err)
   394  	id, err := uuid.NewRandom()
   395  	require.NoError(t, err)
   396  	keystoreFile := &keymanager.Keystore{
   397  		Crypto:  cryptoFields,
   398  		ID:      id.String(),
   399  		Pubkey:  fmt.Sprintf("%x", validatingKey.PublicKey().Marshal()),
   400  		Version: encryptor.Version(),
   401  		Name:    encryptor.Name(),
   402  	}
   403  	encoded, err := json.MarshalIndent(keystoreFile, "", "\t")
   404  	require.NoError(t, err)
   405  	// Write the encoded keystore to disk with the timestamp appended
   406  	createdAt := timeutils.Now().Unix()
   407  	fullPath := filepath.Join(path, fmt.Sprintf(imported.KeystoreFileNameFormat, createdAt))
   408  	require.NoError(t, ioutil.WriteFile(fullPath, encoded, os.ModePerm))
   409  	return keystoreFile, fullPath
   410  }
   411  
   412  // Returns the fullPath to the newly created keystore file.
   413  func createRandomNameKeystore(t *testing.T, path string) (*keymanager.Keystore, string) {
   414  	validatingKey, err := bls.RandKey()
   415  	require.NoError(t, err)
   416  	encryptor := keystorev4.New()
   417  	cryptoFields, err := encryptor.Encrypt(validatingKey.Marshal(), password)
   418  	require.NoError(t, err)
   419  	id, err := uuid.NewRandom()
   420  	require.NoError(t, err)
   421  	keystoreFile := &keymanager.Keystore{
   422  		Crypto:  cryptoFields,
   423  		ID:      id.String(),
   424  		Pubkey:  fmt.Sprintf("%x", validatingKey.PublicKey().Marshal()),
   425  		Version: encryptor.Version(),
   426  		Name:    encryptor.Name(),
   427  	}
   428  	encoded, err := json.MarshalIndent(keystoreFile, "", "\t")
   429  	require.NoError(t, err)
   430  	// Write the encoded keystore to disk with the timestamp appended
   431  	random, err := rand.Int(rand.Reader, big.NewInt(1000000))
   432  	require.NoError(t, err)
   433  	fullPath := filepath.Join(path, fmt.Sprintf("test-%d-keystore", random.Int64()))
   434  	require.NoError(t, ioutil.WriteFile(fullPath, encoded, os.ModePerm))
   435  	return keystoreFile, fullPath
   436  }