package accounts import ( "encoding/hex" "encoding/json" "flag" "fmt" "os" "path/filepath" "strconv" "strings" "testing" "time" "github.com/google/uuid" "github.com/prysmaticlabs/prysm/cmd/validator/flags" "github.com/prysmaticlabs/prysm/config/params" "github.com/prysmaticlabs/prysm/crypto/bls" "github.com/prysmaticlabs/prysm/encoding/bytesutil" "github.com/prysmaticlabs/prysm/testing/assert" "github.com/prysmaticlabs/prysm/testing/require" prysmTime "github.com/prysmaticlabs/prysm/time" "github.com/prysmaticlabs/prysm/validator/accounts" "github.com/prysmaticlabs/prysm/validator/accounts/wallet" "github.com/prysmaticlabs/prysm/validator/keymanager" "github.com/prysmaticlabs/prysm/validator/keymanager/local" "github.com/urfave/cli/v2" keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" ) const ( passwordFileName = "password.txt" password = "OhWOWthisisatest42!$" ) func setupWalletAndPasswordsDir(t testing.TB) (string, string, string) { walletDir := filepath.Join(t.TempDir(), "wallet") passwordsDir := filepath.Join(t.TempDir(), "passwords") passwordFileDir := filepath.Join(t.TempDir(), "passwordFile") require.NoError(t, os.MkdirAll(passwordFileDir, params.BeaconIoConfig().ReadWriteExecutePermissions)) passwordFilePath := filepath.Join(passwordFileDir, passwordFileName) require.NoError(t, os.WriteFile(passwordFilePath, []byte(password), os.ModePerm)) return walletDir, passwordsDir, passwordFilePath } // Returns the fullPath to the newly created keystore file. func createKeystore(t *testing.T, path string) (*keymanager.Keystore, string) { validatingKey, err := bls.RandKey() require.NoError(t, err) encryptor := keystorev4.New() cryptoFields, err := encryptor.Encrypt(validatingKey.Marshal(), password) require.NoError(t, err) id, err := uuid.NewRandom() require.NoError(t, err) keystoreFile := &keymanager.Keystore{ Crypto: cryptoFields, ID: id.String(), Pubkey: fmt.Sprintf("%x", validatingKey.PublicKey().Marshal()), Version: encryptor.Version(), Name: encryptor.Name(), } encoded, err := json.MarshalIndent(keystoreFile, "", "\t") require.NoError(t, err) // Write the encoded keystore to disk with the timestamp appended createdAt := prysmTime.Now().Unix() fullPath := filepath.Join(path, fmt.Sprintf(local.KeystoreFileNameFormat, createdAt)) require.NoError(t, os.WriteFile(fullPath, encoded, os.ModePerm)) return keystoreFile, fullPath } type testWalletConfig struct { exitAll bool skipDepositConfirm bool keymanagerKind keymanager.Kind numAccounts int64 grpcHeaders string privateKeyFile string accountPasswordFile string walletPasswordFile string backupPasswordFile string backupPublicKeys string voluntaryExitPublicKeys string deletePublicKeys string keysDir string backupDir string walletDir string } func setupWalletCtx( tb testing.TB, cfg *testWalletConfig, ) *cli.Context { app := cli.App{} set := flag.NewFlagSet("test", 0) set.String(flags.WalletDirFlag.Name, cfg.walletDir, "") set.String(flags.KeysDirFlag.Name, cfg.keysDir, "") set.String(flags.KeymanagerKindFlag.Name, cfg.keymanagerKind.String(), "") set.String(flags.DeletePublicKeysFlag.Name, cfg.deletePublicKeys, "") set.String(flags.VoluntaryExitPublicKeysFlag.Name, cfg.voluntaryExitPublicKeys, "") set.String(flags.BackupDirFlag.Name, cfg.backupDir, "") set.String(flags.BackupPasswordFile.Name, cfg.backupPasswordFile, "") set.String(flags.BackupPublicKeysFlag.Name, cfg.backupPublicKeys, "") set.String(flags.WalletPasswordFileFlag.Name, cfg.walletPasswordFile, "") set.String(flags.AccountPasswordFileFlag.Name, cfg.accountPasswordFile, "") set.Int64(flags.NumAccountsFlag.Name, cfg.numAccounts, "") set.Bool(flags.SkipDepositConfirmationFlag.Name, cfg.skipDepositConfirm, "") set.Bool(flags.SkipMnemonic25thWordCheckFlag.Name, true, "") set.Bool(flags.ExitAllFlag.Name, cfg.exitAll, "") set.String(flags.GrpcHeadersFlag.Name, cfg.grpcHeaders, "") if cfg.privateKeyFile != "" { set.String(flags.ImportPrivateKeyFileFlag.Name, cfg.privateKeyFile, "") assert.NoError(tb, set.Set(flags.ImportPrivateKeyFileFlag.Name, cfg.privateKeyFile)) } assert.NoError(tb, set.Set(flags.WalletDirFlag.Name, cfg.walletDir)) assert.NoError(tb, set.Set(flags.SkipMnemonic25thWordCheckFlag.Name, "true")) assert.NoError(tb, set.Set(flags.KeysDirFlag.Name, cfg.keysDir)) assert.NoError(tb, set.Set(flags.KeymanagerKindFlag.Name, cfg.keymanagerKind.String())) assert.NoError(tb, set.Set(flags.DeletePublicKeysFlag.Name, cfg.deletePublicKeys)) assert.NoError(tb, set.Set(flags.VoluntaryExitPublicKeysFlag.Name, cfg.voluntaryExitPublicKeys)) assert.NoError(tb, set.Set(flags.BackupDirFlag.Name, cfg.backupDir)) assert.NoError(tb, set.Set(flags.BackupPublicKeysFlag.Name, cfg.backupPublicKeys)) assert.NoError(tb, set.Set(flags.BackupPasswordFile.Name, cfg.backupPasswordFile)) assert.NoError(tb, set.Set(flags.WalletPasswordFileFlag.Name, cfg.walletPasswordFile)) assert.NoError(tb, set.Set(flags.AccountPasswordFileFlag.Name, cfg.accountPasswordFile)) assert.NoError(tb, set.Set(flags.NumAccountsFlag.Name, strconv.Itoa(int(cfg.numAccounts)))) assert.NoError(tb, set.Set(flags.SkipDepositConfirmationFlag.Name, strconv.FormatBool(cfg.skipDepositConfirm))) assert.NoError(tb, set.Set(flags.ExitAllFlag.Name, strconv.FormatBool(cfg.exitAll))) assert.NoError(tb, set.Set(flags.GrpcHeadersFlag.Name, cfg.grpcHeaders)) return cli.NewContext(&app, set, nil) } func TestDeleteAccounts_Noninteractive(t *testing.T) { walletDir, _, passwordFilePath := setupWalletAndPasswordsDir(t) // Write a directory where we will import keys from. keysDir := filepath.Join(t.TempDir(), "keysDir") require.NoError(t, os.MkdirAll(keysDir, os.ModePerm)) // Create 3 keystore files in the keys directory we can then // import from in our wallet. k1, _ := createKeystore(t, keysDir) time.Sleep(time.Second) k2, _ := createKeystore(t, keysDir) time.Sleep(time.Second) k3, _ := createKeystore(t, keysDir) generatedPubKeys := []string{k1.Pubkey, k2.Pubkey, k3.Pubkey} // Only delete keys 0 and 1. deletePublicKeys := strings.Join(generatedPubKeys[0:2], ",") // We initialize a wallet with a local keymanager. cliCtx := setupWalletCtx(t, &testWalletConfig{ // Wallet configuration flags. walletDir: walletDir, keymanagerKind: keymanager.Local, walletPasswordFile: passwordFilePath, accountPasswordFile: passwordFilePath, // Flags required for ImportAccounts to work. keysDir: keysDir, // Flags required for DeleteAccounts to work. deletePublicKeys: deletePublicKeys, }) w, err := accounts.CreateWalletWithKeymanager(cliCtx.Context, &accounts.CreateWalletConfig{ WalletCfg: &wallet.Config{ WalletDir: walletDir, KeymanagerKind: keymanager.Local, WalletPassword: password, }, }) require.NoError(t, err) // We attempt to import accounts. require.NoError(t, accounts.ImportAccountsCli(cliCtx)) // We attempt to delete the accounts specified. require.NoError(t, accountsDelete(cliCtx)) keymanager, err := local.NewKeymanager( cliCtx.Context, &local.SetupConfig{ Wallet: w, ListenForChanges: false, }, ) require.NoError(t, err) remainingAccounts, err := keymanager.FetchValidatingPublicKeys(cliCtx.Context) require.NoError(t, err) require.Equal(t, len(remainingAccounts), 1) remainingPublicKey, err := hex.DecodeString(k3.Pubkey) require.NoError(t, err) assert.DeepEqual(t, remainingAccounts[0], bytesutil.ToBytes48(remainingPublicKey)) }