From 91aaddaeb38ff25118896fb436a938d14636760b Mon Sep 17 00:00:00 2001
From: Felix Lange <fjl@twurst.com>
Date: Fri, 1 Apr 2016 18:10:58 +0200
Subject: [PATCH] cmd/geth: add recovery procedure for AmbiguousAddrError

---
 accounts/testdata/dupes/1   |  1 +
 accounts/testdata/dupes/2   |  1 +
 accounts/testdata/dupes/foo |  1 +
 cmd/geth/accountcmd.go      | 37 +++++++++++++++++++++-
 cmd/geth/accountcmd_test.go | 62 +++++++++++++++++++++++++++++++++++++
 cmd/geth/run_test.go        | 10 +++++-
 6 files changed, 110 insertions(+), 2 deletions(-)
 create mode 100644 accounts/testdata/dupes/1
 create mode 100644 accounts/testdata/dupes/2
 create mode 100644 accounts/testdata/dupes/foo

diff --git a/accounts/testdata/dupes/1 b/accounts/testdata/dupes/1
new file mode 100644
index 000000000..a3868ec6d
--- /dev/null
+++ b/accounts/testdata/dupes/1
@@ -0,0 +1 @@
+{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3}
\ No newline at end of file
diff --git a/accounts/testdata/dupes/2 b/accounts/testdata/dupes/2
new file mode 100644
index 000000000..a3868ec6d
--- /dev/null
+++ b/accounts/testdata/dupes/2
@@ -0,0 +1 @@
+{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3}
\ No newline at end of file
diff --git a/accounts/testdata/dupes/foo b/accounts/testdata/dupes/foo
new file mode 100644
index 000000000..c57060aea
--- /dev/null
+++ b/accounts/testdata/dupes/foo
@@ -0,0 +1 @@
+{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3}
\ No newline at end of file
diff --git a/cmd/geth/accountcmd.go b/cmd/geth/accountcmd.go
index 86175d05f..0bd60d701 100644
--- a/cmd/geth/accountcmd.go
+++ b/cmd/geth/accountcmd.go
@@ -181,10 +181,19 @@ func unlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i
 	for trials := 0; trials < 3; trials++ {
 		prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3)
 		password := getPassPhrase(prompt, false, i, passwords)
-		if err = accman.Unlock(account, password); err == nil {
+		err = accman.Unlock(account, password)
+		if err == nil {
 			glog.V(logger.Info).Infof("Unlocked account %x", account.Address)
 			return account, password
 		}
+		if err, ok := err.(*accounts.AmbiguousAddrError); ok {
+			glog.V(logger.Info).Infof("Unlocked account %x", account.Address)
+			return ambiguousAddrRecovery(accman, err, password), password
+		}
+		if err != accounts.ErrDecrypt {
+			// No need to prompt again if the error is not decryption-related.
+			break
+		}
 	}
 	// All trials expended to unlock account, bail out
 	utils.Fatalf("Failed to unlock account %s (%v)", address, err)
@@ -221,6 +230,32 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string)
 	return password
 }
 
+func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrError, auth string) accounts.Account {
+	fmt.Printf("Multiple key files exist for address %x:\n", err.Addr)
+	for _, a := range err.Matches {
+		fmt.Println("  ", a.File)
+	}
+	fmt.Println("Testing your passphrase against all of them...")
+	var match *accounts.Account
+	for _, a := range err.Matches {
+		if err := am.Unlock(a, auth); err == nil {
+			match = &a
+			break
+		}
+	}
+	if match == nil {
+		utils.Fatalf("None of the listed files could be unlocked.")
+	}
+	fmt.Printf("Your passphrase unlocked %s\n", match.File)
+	fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:")
+	for _, a := range err.Matches {
+		if a != *match {
+			fmt.Println("  ", a.File)
+		}
+	}
+	return *match
+}
+
 // accountCreate creates a new account into the keystore defined by the CLI flags.
 func accountCreate(ctx *cli.Context) {
 	accman := utils.MakeAccountManager(ctx)
diff --git a/cmd/geth/accountcmd_test.go b/cmd/geth/accountcmd_test.go
index 440a0cb0b..b6abde6d8 100644
--- a/cmd/geth/accountcmd_test.go
+++ b/cmd/geth/accountcmd_test.go
@@ -228,3 +228,65 @@ func TestUnlockFlagPasswordFileWrongPassword(t *testing.T) {
 Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase)
 `)
 }
+
+func TestUnlockFlagAmbiguous(t *testing.T) {
+	store := filepath.Join("..", "..", "accounts", "testdata", "dupes")
+	geth := runGeth(t,
+		"--keystore", store, "--nat", "none", "--nodiscover", "--dev",
+		"--unlock", "f466859ead1932d743d622cb74fc058882e8648a",
+		"js", "testdata/empty.js")
+	defer geth.expectExit()
+
+	// Helper for the expect template, returns absolute keystore path.
+	geth.setTemplateFunc("keypath", func(file string) string {
+		abs, _ := filepath.Abs(filepath.Join(store, file))
+		return abs
+	})
+	geth.expect(`
+Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
+!! Unsupported terminal, password will be echoed.
+Passphrase: {{.InputLine "foobar"}}
+Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
+   {{keypath "1"}}
+   {{keypath "2"}}
+Testing your passphrase against all of them...
+Your passphrase unlocked {{keypath "1"}}
+In order to avoid this warning, you need to remove the following duplicate key files:
+   {{keypath "2"}}
+`)
+	geth.expectExit()
+
+	wantMessages := []string{
+		"Unlocked account f466859ead1932d743d622cb74fc058882e8648a",
+	}
+	for _, m := range wantMessages {
+		if strings.Index(geth.stderrText(), m) == -1 {
+			t.Errorf("stderr text does not contain %q", m)
+		}
+	}
+}
+
+func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) {
+	store := filepath.Join("..", "..", "accounts", "testdata", "dupes")
+	geth := runGeth(t,
+		"--keystore", store, "--nat", "none", "--nodiscover", "--dev",
+		"--unlock", "f466859ead1932d743d622cb74fc058882e8648a")
+	defer geth.expectExit()
+
+	// Helper for the expect template, returns absolute keystore path.
+	geth.setTemplateFunc("keypath", func(file string) string {
+		abs, _ := filepath.Abs(filepath.Join(store, file))
+		return abs
+	})
+	geth.expect(`
+Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
+!! Unsupported terminal, password will be echoed.
+Passphrase: {{.InputLine "wrong"}}
+Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
+   {{keypath "1"}}
+   {{keypath "2"}}
+Testing your passphrase against all of them...
+Fatal: None of the listed files could be unlocked.
+`)
+	geth.expectExit()
+}
diff --git a/cmd/geth/run_test.go b/cmd/geth/run_test.go
index a15d0bf28..a82eb9d68 100644
--- a/cmd/geth/run_test.go
+++ b/cmd/geth/run_test.go
@@ -45,6 +45,7 @@ type testgeth struct {
 	// template variables for expect
 	Datadir    string
 	Executable string
+	Func       template.FuncMap
 
 	removeDatadir bool
 	cmd           *exec.Cmd
@@ -114,6 +115,13 @@ func (tt *testgeth) InputLine(s string) string {
 	return ""
 }
 
+func (tt *testgeth) setTemplateFunc(name string, fn interface{}) {
+	if tt.Func == nil {
+		tt.Func = make(map[string]interface{})
+	}
+	tt.Func[name] = fn
+}
+
 // expect runs its argument as a template, then expects the
 // child process to output the result of the template within 5s.
 //
@@ -121,7 +129,7 @@ func (tt *testgeth) InputLine(s string) string {
 // before matching.
 func (tt *testgeth) expect(tplsource string) {
 	// Generate the expected output by running the template.
-	tpl := template.Must(template.New("").Parse(tplsource))
+	tpl := template.Must(template.New("").Funcs(tt.Func).Parse(tplsource))
 	wantbuf := new(bytes.Buffer)
 	if err := tpl.Execute(wantbuf, tt); err != nil {
 		panic(err)
-- 
GitLab