From ab76bdad150a5c38e394b213ecf584c30cf887ac Mon Sep 17 00:00:00 2001
From: Shay Zluf <thezluf@gmail.com>
Date: Fri, 23 Oct 2020 00:05:08 +0300
Subject: [PATCH] Use validator protection datadir (#7355)

* Add validator protection db flag

* fix nil handling

* reuse datadir

* add datadir default config

* Add handling for moving account dir datafile to new set dir

* naming conditionals

* add tests

* fix test

* fix logic to default to wallet dir

* raul feedback

* nishant feedback

* gaz

* revert site_data changes

* fix formatting

* fix formatting

Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
Co-authored-by: Nishant Das <nishdas93@gmail.com>
Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
---
 validator/accounts/prompt/prompt.go | 13 +++++
 validator/flags/flags.go            |  5 ++
 validator/main.go                   |  1 +
 validator/node/BUILD.bazel          |  4 ++
 validator/node/node.go              | 76 ++++++++++++++++++---------
 validator/node/node_test.go         | 80 +++++++++++++++++++++++++++++
 validator/usage.go                  |  1 +
 7 files changed, 157 insertions(+), 23 deletions(-)

diff --git a/validator/accounts/prompt/prompt.go b/validator/accounts/prompt/prompt.go
index 1f0332ebbb3f..425a584d9d33 100644
--- a/validator/accounts/prompt/prompt.go
+++ b/validator/accounts/prompt/prompt.go
@@ -62,6 +62,19 @@ func InputDirectory(cliCtx *cli.Context, promptText string, flag *cli.StringFlag
 	return fileutil.ExpandPath(inputtedDir)
 }
 
+// InputDir from the cli without exception.
+func InputDir(cliCtx *cli.Context, promptText string, dirFlag *cli.StringFlag) (string, error) {
+	directory := cliCtx.String(dirFlag.Name)
+	inputtedDir, err := promptutil.DefaultPrompt(au.Bold(promptText).String(), directory)
+	if err != nil {
+		return "", err
+	}
+	if inputtedDir == directory {
+		return directory, nil
+	}
+	return fileutil.ExpandPath(inputtedDir)
+}
+
 // InputRemoteKeymanagerConfig via the cli.
 func InputRemoteKeymanagerConfig(cliCtx *cli.Context) (*remote.KeymanagerOpts, error) {
 	addr := cliCtx.String(flags.GrpcRemoteAddressFlag.Name)
diff --git a/validator/flags/flags.go b/validator/flags/flags.go
index deadab8b1b9a..fc70d6f0491f 100644
--- a/validator/flags/flags.go
+++ b/validator/flags/flags.go
@@ -251,6 +251,11 @@ var (
 		Usage: "Enables the web portal for the validator client (work in progress)",
 		Value: false,
 	}
+	// AllowEmptyProtectionDB allow new protection db to be created without prompting the user.
+	AllowEmptyProtectionDB = &cli.BoolFlag{
+		Name:  "allow-new-protection-db",
+		Usage: "Use this flag allow new protection db to be created non-interactively",
+	}
 )
 
 // DefaultValidatorDir returns OS-specific default validator directory.
diff --git a/validator/main.go b/validator/main.go
index 0ff0166d9513..55829c369829 100644
--- a/validator/main.go
+++ b/validator/main.go
@@ -72,6 +72,7 @@ var appFlags = []cli.Flag{
 	flags.WalletPasswordFileFlag,
 	flags.WalletDirFlag,
 	flags.EnableWebFlag,
+	flags.AllowEmptyProtectionDB,
 	cmd.MinimalConfigFlag,
 	cmd.E2EConfigFlag,
 	cmd.VerbosityFlag,
diff --git a/validator/node/BUILD.bazel b/validator/node/BUILD.bazel
index e201d1dea276..4c01180ecf9b 100644
--- a/validator/node/BUILD.bazel
+++ b/validator/node/BUILD.bazel
@@ -7,11 +7,13 @@ go_test(
     srcs = ["node_test.go"],
     embed = [":go_default_library"],
     deps = [
+        "//shared/params:go_default_library",
         "//shared/testutil:go_default_library",
         "//shared/testutil/assert:go_default_library",
         "//shared/testutil/require:go_default_library",
         "//validator/accounts:go_default_library",
         "//validator/accounts/wallet:go_default_library",
+        "//validator/db/kv:go_default_library",
         "//validator/flags:go_default_library",
         "//validator/keymanager:go_default_library",
         "@com_github_sirupsen_logrus//hooks/test:go_default_library",
@@ -36,6 +38,7 @@ go_library(
         "//shared/prometheus:go_default_library",
         "//shared/tracing:go_default_library",
         "//shared/version:go_default_library",
+        "//validator/accounts/prompt:go_default_library",
         "//validator/accounts/wallet:go_default_library",
         "//validator/client:go_default_library",
         "//validator/db/kv:go_default_library",
@@ -45,6 +48,7 @@ go_library(
         "//validator/rpc:go_default_library",
         "//validator/rpc/gateway:go_default_library",
         "//validator/slashing-protection:go_default_library",
+        "@com_github_logrusorgru_aurora//:go_default_library",
         "@com_github_pkg_errors//:go_default_library",
         "@com_github_sirupsen_logrus//:go_default_library",
         "@com_github_urfave_cli_v2//:go_default_library",
diff --git a/validator/node/node.go b/validator/node/node.go
index ddcbd4531b6b..22c187fab53d 100644
--- a/validator/node/node.go
+++ b/validator/node/node.go
@@ -12,6 +12,7 @@ import (
 	"sync"
 	"syscall"
 
+	"github.com/logrusorgru/aurora"
 	"github.com/pkg/errors"
 	"github.com/prysmaticlabs/prysm/shared"
 	"github.com/prysmaticlabs/prysm/shared/cmd"
@@ -24,6 +25,7 @@ import (
 	"github.com/prysmaticlabs/prysm/shared/prometheus"
 	"github.com/prysmaticlabs/prysm/shared/tracing"
 	"github.com/prysmaticlabs/prysm/shared/version"
+	"github.com/prysmaticlabs/prysm/validator/accounts/prompt"
 	"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
 	"github.com/prysmaticlabs/prysm/validator/client"
 	"github.com/prysmaticlabs/prysm/validator/db/kv"
@@ -38,6 +40,17 @@ import (
 )
 
 var log = logrus.WithField("prefix", "node")
+var au = aurora.NewAurora(true)
+var calmFirstTimeUsers = "If this is the first time you are using these keys " +
+	"disregard this warning and hit the Enter key.\n"
+var warning = "Warning!!! protection db is the main method to prevent slashing. " +
+	"If it is not the first time you are running the validator with the current " +
+	"keys please locate the db file!!!\n"
+var defaultWarning = "hitting return will start an empty db file"
+var specifyProtectionDBPath = fmt.Sprintf(
+	"\n\n%s%sdb file name is %s please locate the latest version of it "+
+		"and paste the path here (%s)", au.BrightCyan(calmFirstTimeUsers), au.BrightMagenta(warning),
+	au.BrightMagenta(kv.ProtectionDbFileName), au.Red(defaultWarning))
 
 // ValidatorClient defines an instance of an eth2 validator that manages
 // the entire lifecycle of services attached to it participating in eth2.
@@ -159,7 +172,6 @@ func (s *ValidatorClient) Close() {
 func (s *ValidatorClient) initializeFromCLI(cliCtx *cli.Context) error {
 	var keyManager keymanager.IKeymanager
 	var err error
-	var accountsDir string
 	if cliCtx.IsSet(flags.InteropNumValidators.Name) {
 		numValidatorKeys := cliCtx.Uint64(flags.InteropNumValidators.Name)
 		offset := cliCtx.Uint64(flags.InteropStartIndex.Name)
@@ -189,15 +201,19 @@ func (s *ValidatorClient) initializeFromCLI(cliCtx *cli.Context) error {
 		if err := w.LockWalletConfigFile(cliCtx.Context); err != nil {
 			log.Fatalf("Could not get a lock on wallet file. Please check if you have another validator instance running and using the same wallet: %v", err)
 		}
-		accountsDir = s.wallet.AccountsDir()
 	}
 
-	dataDir := moveDb(cliCtx, accountsDir)
+	dataFlag := flags.WalletDirFlag
+	if cliCtx.String(cmd.DataDirFlag.Name) != cmd.DefaultDataDir() {
+		dataFlag = cmd.DataDirFlag
+	}
+	dataDir := cliCtx.String(dataFlag.Name)
+	moveSlashingProtectionDatabase(cliCtx, dataFlag)
 	clearFlag := cliCtx.Bool(cmd.ClearDB.Name)
 	forceClearFlag := cliCtx.Bool(cmd.ForceClearDB.Name)
 	if clearFlag || forceClearFlag {
-		if dataDir == "" {
-			dataDir = cmd.DefaultDataDir()
+		if dataDir == "" && s.wallet != nil {
+			dataDir = s.wallet.AccountsDir()
 			if dataDir == "" {
 				log.Fatal(
 					"Could not determine your system's HOME path, please specify a --datadir you wish " +
@@ -241,24 +257,33 @@ func (s *ValidatorClient) initializeFromCLI(cliCtx *cli.Context) error {
 	return nil
 }
 
-func moveDb(cliCtx *cli.Context, accountsDir string) string {
-	dataDir := cliCtx.String(cmd.DataDirFlag.Name)
-	if accountsDir != "" {
-		dataFile := filepath.Join(dataDir, kv.ProtectionDbFileName)
-		newDataFile := filepath.Join(accountsDir, kv.ProtectionDbFileName)
-		if fileutil.FileExists(dataFile) && !fileutil.FileExists(newDataFile) {
-			log.WithFields(logrus.Fields{
-				"oldDbPath": dataDir,
-				"walletDir": accountsDir,
-			}).Info("Moving validator protection db to wallet dir")
-			err := fileutil.CopyFile(dataFile, newDataFile)
-			if err != nil {
-				log.Fatal(err)
-			}
+func moveSlashingProtectionDatabase(cliCtx *cli.Context, defaultDir *cli.StringFlag) {
+	dataDir := cliCtx.String(defaultDir.Name)
+	dataFile := filepath.Join(dataDir, kv.ProtectionDbFileName)
+	clearFlag := cliCtx.Bool(cmd.ClearDB.Name)
+	forceClearFlag := cliCtx.Bool(cmd.ForceClearDB.Name)
+	if clearFlag || forceClearFlag || cliCtx.Bool(flags.AllowEmptyProtectionDB.Name) {
+		return
+	}
+	if !fileutil.FileExists(dataFile) {
+		// Input the directory where the old protection db resides.
+		protectionDbPath, err := prompt.InputDir(cliCtx, specifyProtectionDBPath, defaultDir)
+		if err != nil {
+			log.WithError(err).Fatal("could not parse protection db directory")
+		}
+		if protectionDbPath == dataDir {
+			return
+		}
+		oldDataFile := filepath.Join(protectionDbPath, kv.ProtectionDbFileName)
+		log.WithFields(logrus.Fields{
+			"oldDbPath":      oldDataFile,
+			"validatorDbDir": dataFile,
+		}).Info("Moving validator protection db")
+		err = fileutil.CopyFile(oldDataFile, dataFile)
+		if err != nil {
+			log.WithError(err).Fatal("could not copy old db file")
 		}
-		dataDir = accountsDir
 	}
-	return dataDir
 }
 
 func (s *ValidatorClient) initializeForWeb(cliCtx *cli.Context) error {
@@ -287,10 +312,15 @@ func (s *ValidatorClient) initializeForWeb(cliCtx *cli.Context) error {
 			log.Fatalf("Could not get a lock on wallet file. Please check if you have another validator instance running and using the same wallet: %v", err)
 		}
 	}
-
+	dataFlag := flags.WalletDirFlag
+	if cliCtx.String(cmd.DataDirFlag.Name) != cmd.DefaultDataDir() {
+		dataFlag = cmd.DataDirFlag
+	}
+	dataDir := cliCtx.String(dataFlag.Name)
+	moveSlashingProtectionDatabase(cliCtx, dataFlag)
 	clearFlag := cliCtx.Bool(cmd.ClearDB.Name)
 	forceClearFlag := cliCtx.Bool(cmd.ForceClearDB.Name)
-	dataDir := cliCtx.String(cmd.DataDirFlag.Name)
+
 	if clearFlag || forceClearFlag {
 		if dataDir == "" {
 			dataDir = cmd.DefaultDataDir()
diff --git a/validator/node/node_test.go b/validator/node/node_test.go
index 647316c7a9d5..bd32abc9eb0b 100644
--- a/validator/node/node_test.go
+++ b/validator/node/node_test.go
@@ -10,11 +10,13 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/prysmaticlabs/prysm/shared/params"
 	"github.com/prysmaticlabs/prysm/shared/testutil"
 	"github.com/prysmaticlabs/prysm/shared/testutil/assert"
 	"github.com/prysmaticlabs/prysm/shared/testutil/require"
 	"github.com/prysmaticlabs/prysm/validator/accounts"
 	"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
+	"github.com/prysmaticlabs/prysm/validator/db/kv"
 	"github.com/prysmaticlabs/prysm/validator/flags"
 	"github.com/prysmaticlabs/prysm/validator/keymanager"
 	logTest "github.com/sirupsen/logrus/hooks/test"
@@ -26,6 +28,7 @@ func TestNode_Builds(t *testing.T) {
 	app := cli.App{}
 	set := flag.NewFlagSet("test", 0)
 	set.String("datadir", testutil.TempDir()+"/datadir", "the node data directory")
+	set.Bool("allow-new-protection-db", true, "dont prompt")
 	dir := testutil.TempDir() + "/walletpath"
 	passwordDir := testutil.TempDir() + "/password"
 	require.NoError(t, os.MkdirAll(passwordDir, os.ModePerm))
@@ -73,3 +76,80 @@ func TestClearDB(t *testing.T) {
 	require.NoError(t, err)
 	require.LogsContain(t, hook, "Removing database")
 }
+
+func Test_moveSlashingProtectionDatabase_doesntPromptWithFlag(t *testing.T) {
+	hook := logTest.NewGlobal()
+	app := cli.App{}
+	set := flag.NewFlagSet("test", 0)
+	dataDir := testutil.TempDir() + "/datadir"
+	set.String("datadir", dataDir, "the node data directory")
+	set.Bool("allow-new-protection-db", true, "dont prompt")
+	context := cli.NewContext(&app, set, nil)
+	// dont prompt when non interactive flag is on.
+	moveSlashingProtectionDatabase(context, flags.WalletDirFlag)
+	require.LogsDoNotContain(t, hook, "protection db is empty.")
+	require.LogsDoNotContain(t, hook, "Moving validator protection db")
+}
+
+func Test_moveSlashingProtectionDatabaseDefaultValue(t *testing.T) {
+	tmpfile, err := ioutil.TempFile("", "content")
+	require.NoError(t, err)
+	defer func() {
+		err := os.Remove(tmpfile.Name())
+		require.NoError(t, err)
+	}()
+
+	_, err = tmpfile.Write([]byte("\n"))
+	require.NoError(t, err)
+
+	_, err = tmpfile.Seek(0, 0)
+	require.NoError(t, err)
+	oldStdin := os.Stdin
+	defer func() { os.Stdin = oldStdin }() // Restore original Stdin
+	os.Stdin = tmpfile
+
+	hook := logTest.NewGlobal()
+	app := cli.App{}
+	set := flag.NewFlagSet("test", 0)
+
+	// prompt when flag is not present and db is new.
+	context := cli.NewContext(&app, set, nil)
+	moveSlashingProtectionDatabase(context, flags.WalletDirFlag)
+	require.LogsDoNotContain(t, hook, "Moving validator protection db")
+}
+
+func Test_moveSlashingProtectionDatabaseToNewLocation(t *testing.T) {
+	tmpDBDir, err := ioutil.TempDir("", "dbdir")
+	require.NoError(t, err)
+	tmpfile, err := ioutil.TempFile("", "content")
+	require.NoError(t, err)
+	tmpDbFile := filepath.Join(tmpDBDir, kv.ProtectionDbFileName)
+	err = ioutil.WriteFile(tmpDbFile, []byte("test data"), params.BeaconIoConfig().ReadWritePermissions)
+	require.NoError(t, err)
+	defer func() {
+		err := os.Remove(tmpfile.Name())
+		require.NoError(t, err)
+		err = os.Remove(tmpDbFile)
+		require.NoError(t, err)
+		err = os.Remove(tmpDBDir)
+		require.NoError(t, err)
+	}()
+	_, err = tmpfile.Write([]byte(tmpDBDir))
+	require.NoError(t, err)
+
+	_, err = tmpfile.Seek(0, 0)
+	require.NoError(t, err)
+	oldStdin := os.Stdin
+	defer func() { os.Stdin = oldStdin }() // Restore original Stdin
+	os.Stdin = tmpfile
+
+	hook := logTest.NewGlobal()
+	app := cli.App{}
+	set := flag.NewFlagSet("test", 0)
+	set.String("datadir", testutil.TempDir(), "the node data directory")
+
+	// prompt when flag is not present and db is new.
+	context := cli.NewContext(&app, set, nil)
+	moveSlashingProtectionDatabase(context, flags.WalletDirFlag)
+	require.LogsContain(t, hook, "Moving validator protection db")
+}
diff --git a/validator/usage.go b/validator/usage.go
index d1b64ae65932..6f6e75397452 100644
--- a/validator/usage.go
+++ b/validator/usage.go
@@ -83,6 +83,7 @@ var appHelpFlagGroups = []flagGroup{
 			flags.BeaconRPCGatewayProviderFlag,
 			flags.CertFlag,
 			flags.EnableWebFlag,
+			flags.AllowEmptyProtectionDB,
 			flags.DisablePenaltyRewardLogFlag,
 			flags.GraffitiFlag,
 			flags.EnableRPCFlag,