Przeglądaj źródła

accounts, cmd, internal, node: implement HD wallet self-derivation

Péter Szilágyi 8 lat temu
rodzic
commit
205ea95802

+ 14 - 1
accounts/accounts.go

@@ -20,6 +20,7 @@ package accounts
 import (
 	"math/big"
 
+	ethereum "github.com/ethereum/go-ethereum"
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core/types"
 	"github.com/ethereum/go-ethereum/event"
@@ -71,7 +72,19 @@ type Wallet interface {
 	// Derive attempts to explicitly derive a hierarchical deterministic account at
 	// the specified derivation path. If requested, the derived account will be added
 	// to the wallet's tracked account list.
-	Derive(path string, pin bool) (Account, error)
+	Derive(path DerivationPath, pin bool) (Account, error)
+
+	// SelfDerive sets a base account derivation path from which the wallet attempts
+	// to discover non zero accounts and automatically add them to list of tracked
+	// accounts.
+	//
+	// Note, self derivaton will increment the last component of the specified path
+	// opposed to decending into a child path to allow discovering accounts starting
+	// from non zero components.
+	//
+	// You can disable automatic account discovery by calling SelfDerive with a nil
+	// chain state reader.
+	SelfDerive(base DerivationPath, chain ethereum.ChainStateReader)
 
 	// SignHash requests the wallet to sign the given hash.
 	//

+ 130 - 0
accounts/hd.go

@@ -0,0 +1,130 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+	"errors"
+	"fmt"
+	"math"
+	"math/big"
+	"strings"
+)
+
+// DefaultRootDerivationPath is the root path to which custom derivation endpoints
+// are appended. As such, the first account will be at m/44'/60'/0'/0, the second
+// at m/44'/60'/0'/1, etc.
+var DefaultRootDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0}
+
+// DefaultBaseDerivationPath is the base path from which custom derivation endpoints
+// are incremented. As such, the first account will be at m/44'/60'/0'/0, the second
+// at m/44'/60'/0'/1, etc.
+var DefaultBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}
+
+// DerivationPath represents the computer friendly version of a hierarchical
+// deterministic wallet account derivaion path.
+//
+// The BIP-32 spec https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
+// defines derivation paths to be of the form:
+//
+//   m / purpose' / coin_type' / account' / change / address_index
+//
+// The BIP-44 spec https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
+// defines that the `purpose` be 44' (or 0x8000002C) for crypto currencies, and
+// SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns
+// the `coin_type` 60' (or 0x8000003C) to Ethereum.
+//
+// The root path for Ethereum is m/44'/60'/0'/0 according to the specification
+// from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone
+// yet whether accounts should increment the last component or the children of
+// that. We will go with the simpler approach of incrementing the last component.
+type DerivationPath []uint32
+
+// ParseDerivationPath converts a user specified derivation path string to the
+// internal binary representation.
+//
+// Full derivation paths need to start with the `m/` prefix, relative derivation
+// paths (which will get appended to the default root path) must not have prefixes
+// in front of the first element. Whitespace is ignored.
+func ParseDerivationPath(path string) (DerivationPath, error) {
+	var result DerivationPath
+
+	// Handle absolute or relative paths
+	components := strings.Split(path, "/")
+	switch {
+	case len(components) == 0:
+		return nil, errors.New("empty derivation path")
+
+	case strings.TrimSpace(components[0]) == "":
+		return nil, errors.New("ambiguous path: use 'm/' prefix for absolute paths, or no leading '/' for relative ones")
+
+	case strings.TrimSpace(components[0]) == "m":
+		components = components[1:]
+
+	default:
+		result = append(result, DefaultRootDerivationPath...)
+	}
+	// All remaining components are relative, append one by one
+	if len(components) == 0 {
+		return nil, errors.New("empty derivation path") // Empty relative paths
+	}
+	for _, component := range components {
+		// Ignore any user added whitespace
+		component = strings.TrimSpace(component)
+		var value uint32
+
+		// Handle hardened paths
+		if strings.HasSuffix(component, "'") {
+			value = 0x80000000
+			component = strings.TrimSpace(strings.TrimSuffix(component, "'"))
+		}
+		// Handle the non hardened component
+		bigval, ok := new(big.Int).SetString(component, 0)
+		if !ok {
+			return nil, fmt.Errorf("invalid component: %s", component)
+		}
+		max := math.MaxUint32 - value
+		if bigval.Sign() < 0 || bigval.Cmp(big.NewInt(int64(max))) > 0 {
+			if value == 0 {
+				return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigval, max)
+			}
+			return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigval, max)
+		}
+		value += uint32(bigval.Uint64())
+
+		// Append and repeat
+		result = append(result, value)
+	}
+	return result, nil
+}
+
+// String implements the stringer interface, converting a binary derivation path
+// to its canonical representation.
+func (path DerivationPath) String() string {
+	result := "m"
+	for _, component := range path {
+		var hardened bool
+		if component >= 0x80000000 {
+			component -= 0x80000000
+			hardened = true
+		}
+		result = fmt.Sprintf("%s/%d", result, component)
+		if hardened {
+			result += "'"
+		}
+	}
+	return result
+}

+ 79 - 0
accounts/hd_test.go

@@ -0,0 +1,79 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+	"reflect"
+	"testing"
+)
+
+// Tests that HD derivation paths can be correctly parsed into our internal binary
+// representation.
+func TestHDPathParsing(t *testing.T) {
+	tests := []struct {
+		input  string
+		output DerivationPath
+	}{
+		// Plain absolute derivation paths
+		{"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+		{"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
+		{"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+		{"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
+		{"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+		{"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+
+		// Plain relative derivation paths
+		{"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+		{"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
+		{"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+		{"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
+		{"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+
+		// Hexadecimal absolute derivation paths
+		{"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+		{"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
+		{"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+		{"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
+		{"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+		{"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+
+		// Hexadecimal relative derivation paths
+		{"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+		{"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
+		{"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+		{"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
+		{"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+
+		// Weird inputs just to ensure they work
+		{"	m  /   44			'\n/\n   60	\n\n\t'   /\n0 ' /\t\t	0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+
+		// Invaid derivation paths
+		{"", nil},              // Empty relative derivation path
+		{"m", nil},             // Empty absolute derivation path
+		{"m/", nil},            // Missing last derivation component
+		{"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error
+		{"m/2147483648'", nil}, // Overflows 32 bit integer
+		{"m/-1'", nil},         // Cannot contain negative number
+	}
+	for i, tt := range tests {
+		if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) {
+			t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output)
+		} else if path == nil && err == nil {
+			t.Errorf("test %d: nil path and error: %v", i, err)
+		}
+	}
+}

+ 6 - 1
accounts/keystore/keystore_wallet.go

@@ -19,6 +19,7 @@ package keystore
 import (
 	"math/big"
 
+	ethereum "github.com/ethereum/go-ethereum"
 	"github.com/ethereum/go-ethereum/accounts"
 	"github.com/ethereum/go-ethereum/core/types"
 )
@@ -69,10 +70,14 @@ func (w *keystoreWallet) Contains(account accounts.Account) bool {
 
 // Derive implements accounts.Wallet, but is a noop for plain wallets since there
 // is no notion of hierarchical account derivation for plain keystore accounts.
-func (w *keystoreWallet) Derive(path string, pin bool) (accounts.Account, error) {
+func (w *keystoreWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
 	return accounts.Account{}, accounts.ErrNotSupported
 }
 
+// SelfDerive implements accounts.Wallet, but is a noop for plain wallets since
+// there is no notion of hierarchical account derivation for plain keystore accounts.
+func (w *keystoreWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {}
+
 // SignHash implements accounts.Wallet, attempting to sign the given hash with
 // the given account. If the wallet does not wrap this particular account, an
 // error is returned to avoid account leakage (even though in theory we may be

+ 0 - 77
accounts/usbwallet/ledger_test.go

@@ -1,77 +0,0 @@
-// Copyright 2017 The go-ethereum Authors
-// This file is part of the go-ethereum library.
-//
-// The go-ethereum library is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// The go-ethereum library is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Lesser General Public License for more details.
-//
-// You should have received a copy of the GNU Lesser General Public License
-// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
-
-// +build !ios
-
-package usbwallet
-
-/*
-func TestLedgerHub(t *testing.T) {
-	glog.SetV(6)
-	glog.SetToStderr(true)
-
-	// Create a USB hub watching for Ledger devices
-	hub, err := NewLedgerHub()
-	if err != nil {
-		t.Fatalf("Failed to create Ledger hub: %v", err)
-	}
-	defer hub.Close()
-
-	// Wait for events :P
-	time.Sleep(time.Minute)
-}
-*/
-/*
-func TestLedger(t *testing.T) {
-	// Create a USB context to access devices through
-	ctx, err := usb.NewContext()
-	defer ctx.Close()
-	ctx.Debug(6)
-
-	// List all of the Ledger wallets
-	wallets, err := findLedgerWallets(ctx)
-	if err != nil {
-		t.Fatalf("Failed to list Ledger wallets: %v", err)
-	}
-	// Retrieve the address from every one of them
-	for _, wallet := range wallets {
-		// Retrieve the version of the wallet app
-		ver, err := wallet.Version()
-		if err != nil {
-			t.Fatalf("Failed to retrieve wallet version: %v", err)
-		}
-		fmt.Printf("Ledger version: %s\n", ver)
-
-		// Retrieve the address of the wallet
-		addr, err := wallet.Address()
-		if err != nil {
-			t.Fatalf("Failed to retrieve wallet address: %v", err)
-		}
-		fmt.Printf("Ledger address: %x\n", addr)
-
-		// Try to sign a transaction with the wallet
-		unsigned := types.NewTransaction(1, common.HexToAddress("0xbabababababababababababababababababababa"), common.Ether, big.NewInt(20000), common.Shannon, nil)
-		signed, err := wallet.Sign(unsigned)
-		if err != nil {
-			t.Fatalf("Failed to sign transactions: %v", err)
-		}
-		signer, err := types.Sender(types.NewEIP155Signer(big.NewInt(1)), signed)
-		if err != nil {
-			t.Fatalf("Failed to recover signer: %v", err)
-		}
-		fmt.Printf("Ledger signature by: %x\n", signer)
-	}
-}*/

+ 118 - 36
accounts/usbwallet/ledger_wallet.go

@@ -29,11 +29,10 @@ import (
 	"fmt"
 	"io"
 	"math/big"
-	"strconv"
-	"strings"
 	"sync"
 	"time"
 
+	ethereum "github.com/ethereum/go-ethereum"
 	"github.com/ethereum/go-ethereum/accounts"
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core/types"
@@ -41,10 +40,15 @@ import (
 	"github.com/ethereum/go-ethereum/logger/glog"
 	"github.com/ethereum/go-ethereum/rlp"
 	"github.com/karalabe/gousb/usb"
+	"golang.org/x/net/context"
 )
 
-// ledgerDerivationPath is the base derivation parameters used by the wallet.
-var ledgerDerivationPath = []uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}
+// Maximum time between wallet health checks to detect USB unplugs.
+const ledgerHeartbeatCycle = time.Second
+
+// Minimum time to wait between self derivation attempts, even it the user is
+// requesting accounts like crazy.
+const ledgerSelfDeriveThrottling = time.Second
 
 // ledgerOpcode is an enumeration encoding the supported Ledger opcodes.
 type ledgerOpcode byte
@@ -82,9 +86,15 @@ type ledgerWallet struct {
 	output  usb.Endpoint // Output endpoint to receive data from this device
 	failure error        // Any failure that would make the device unusable
 
-	version  [3]byte                     // Current version of the Ledger Ethereum app (zero if app is offline)
-	accounts []accounts.Account          // List of derive accounts pinned on the Ledger
-	paths    map[common.Address][]uint32 // Known derivation paths for signing operations
+	version  [3]byte                                    // Current version of the Ledger Ethereum app (zero if app is offline)
+	accounts []accounts.Account                         // List of derive accounts pinned on the Ledger
+	paths    map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations
+
+	selfDeriveNextPath accounts.DerivationPath   // Next derivation path for account auto-discovery
+	selfDeriveNextAddr common.Address            // Next derived account address for auto-discovery
+	selfDerivePrevZero common.Address            // Last zero-address where auto-discovery stopped
+	selfDeriveChain    ethereum.ChainStateReader // Blockchain state reader to discover used account with
+	selfDeriveTime     time.Time                 // Timestamp of the last self-derivation to avoid thrashing
 
 	quit chan chan error
 	lock sync.RWMutex
@@ -107,12 +117,17 @@ func (w *ledgerWallet) Status() string {
 	if w.device == nil {
 		return "Closed"
 	}
-	if w.version == [3]byte{0, 0, 0} {
+	if w.offline() {
 		return "Ethereum app offline"
 	}
 	return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2])
 }
 
+// offline returns whether the wallet and the Ethereum app is offline or not.
+func (w *ledgerWallet) offline() bool {
+	return w.version == [3]byte{0, 0, 0}
+}
+
 // Open implements accounts.Wallet, attempting to open a USB connection to the
 // Ledger hardware wallet. The Ledger does not require a user passphrase so that
 // is silently discarded.
@@ -176,13 +191,13 @@ func (w *ledgerWallet) Open(passphrase string) error {
 	// Wallet seems to be successfully opened, guess if the Ethereum app is running
 	w.device, w.input, w.output = device, input, output
 
-	w.paths = make(map[common.Address][]uint32)
+	w.paths = make(map[common.Address]accounts.DerivationPath)
 	w.quit = make(chan chan error)
 	defer func() {
 		go w.heartbeat()
 	}()
 
-	if _, err := w.deriveAddress(ledgerDerivationPath); err != nil {
+	if _, err := w.deriveAddress(accounts.DefaultBaseDerivationPath); err != nil {
 		// Ethereum app is not running, nothing more to do, return
 		return nil
 	}
@@ -209,7 +224,7 @@ func (w *ledgerWallet) heartbeat() {
 		case errc = <-w.quit:
 			// Termination requested
 			continue
-		case <-time.After(time.Second):
+		case <-time.After(ledgerHeartbeatCycle):
 			// Heartbeat time
 		}
 		// Execute a tiny data exchange to see responsiveness
@@ -242,16 +257,86 @@ func (w *ledgerWallet) Close() error {
 		return err
 	}
 	w.device, w.input, w.output, w.paths, w.quit = nil, nil, nil, nil, nil
+	w.version = [3]byte{}
 
 	return herr // If all went well, return any health-check errors
 }
 
 // Accounts implements accounts.Wallet, returning the list of accounts pinned to
-// the Ledger hardware wallet.
+// the Ledger hardware wallet. If self derivation was enabled, the account list
+// is periodically expanded based on current chain state.
 func (w *ledgerWallet) Accounts() []accounts.Account {
-	w.lock.RLock()
-	defer w.lock.RUnlock()
+	w.lock.Lock()
+	defer w.lock.Unlock()
+
+	// If the wallet is offline, there are no accounts to return
+	if w.offline() {
+		return nil
+	}
+	// If no self derivation is done (or throttled), return the current accounts
+	if w.selfDeriveChain == nil || time.Since(w.selfDeriveTime) < ledgerSelfDeriveThrottling {
+		cpy := make([]accounts.Account, len(w.accounts))
+		copy(cpy, w.accounts)
+		return cpy
+	}
+	// Self derivation requested, try to expand our account list
+	ctx := context.Background()
+	for empty := false; !empty; {
+		// Retrieve the next derived Ethereum account
+		var err error
+		if w.selfDeriveNextAddr == (common.Address{}) {
+			w.selfDeriveNextAddr, err = w.deriveAddress(w.selfDeriveNextPath)
+			if err != nil {
+				// Derivation failed, disable auto discovery
+				glog.V(logger.Warn).Infof("self-derivation failed: %v", err)
+				w.selfDeriveChain = nil
+				break
+			}
+		}
+		// Check the account's status against the current chain state
+		balance, err := w.selfDeriveChain.BalanceAt(ctx, w.selfDeriveNextAddr, nil)
+		if err != nil {
+			glog.V(logger.Warn).Infof("self-derivation balance retrieval failed: %v", err)
+			w.selfDeriveChain = nil
+			break
+		}
+		nonce, err := w.selfDeriveChain.NonceAt(ctx, w.selfDeriveNextAddr, nil)
+		if err != nil {
+			glog.V(logger.Warn).Infof("self-derivation nonce retrieval failed: %v", err)
+			w.selfDeriveChain = nil
+			break
+		}
+		// If the next account is empty, stop self-derivation, but add it nonetheless
+		if balance.BitLen() == 0 && nonce == 0 {
+			w.selfDerivePrevZero = w.selfDeriveNextAddr
+			empty = true
+		}
+		// We've just self-derived a new non-zero account, start tracking it
+		path := make(accounts.DerivationPath, len(w.selfDeriveNextPath))
+		copy(path[:], w.selfDeriveNextPath[:])
 
+		account := accounts.Account{
+			Address: w.selfDeriveNextAddr,
+			URL:     accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)},
+		}
+		_, known := w.paths[w.selfDeriveNextAddr]
+		if !known || (!empty && w.selfDeriveNextAddr == w.selfDerivePrevZero) {
+			// Either fully new account, or previous zero. Report discovery either way
+			glog.V(logger.Info).Infof("%s discovered %s (balance %d, nonce %d) at %s", w.url.String(), w.selfDeriveNextAddr.Hex(), balance, nonce, path)
+		}
+		if !known {
+			w.accounts = append(w.accounts, account)
+			w.paths[w.selfDeriveNextAddr] = path
+		}
+		// Fetch the next potential account
+		if !empty {
+			w.selfDeriveNextAddr = common.Address{}
+			w.selfDeriveNextPath[len(w.selfDeriveNextPath)-1]++
+		}
+	}
+	w.selfDeriveTime = time.Now()
+
+	// Return whatever account list we ended up with
 	cpy := make([]accounts.Account, len(w.accounts))
 	copy(cpy, w.accounts)
 	return cpy
@@ -271,34 +356,16 @@ func (w *ledgerWallet) Contains(account accounts.Account) bool {
 // Derive implements accounts.Wallet, deriving a new account at the specific
 // derivation path. If pin is set to true, the account will be added to the list
 // of tracked accounts.
-func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) {
+func (w *ledgerWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) {
 	w.lock.Lock()
 	defer w.lock.Unlock()
 
 	// If the wallet is closed, or the Ethereum app doesn't run, abort
-	if w.device == nil || w.version == [3]byte{0, 0, 0} {
+	if w.device == nil || w.offline() {
 		return accounts.Account{}, accounts.ErrWalletClosed
 	}
-	// All seems fine, convert the user derivation path to Ledger representation
-	path = strings.TrimPrefix(path, "/")
-
-	parts := strings.Split(path, "/")
-	lpath := make([]uint32, len(parts))
-	for i, part := range parts {
-		// Handle hardened paths
-		if strings.HasSuffix(part, "'") {
-			lpath[i] = 0x80000000
-			part = strings.TrimSuffix(part, "'")
-		}
-		// Handle the non hardened component
-		val, err := strconv.Atoi(part)
-		if err != nil {
-			return accounts.Account{}, fmt.Errorf("path element %d: %v", i, err)
-		}
-		lpath[i] += uint32(val)
-	}
 	// Try to derive the actual account and update it's URL if succeeful
-	address, err := w.deriveAddress(lpath)
+	address, err := w.deriveAddress(path)
 	if err != nil {
 		return accounts.Account{}, err
 	}
@@ -310,12 +377,27 @@ func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) {
 	if pin {
 		if _, ok := w.paths[address]; !ok {
 			w.accounts = append(w.accounts, account)
-			w.paths[address] = lpath
+			w.paths[address] = path
 		}
 	}
 	return account, nil
 }
 
+// SelfDerive implements accounts.Wallet, trying to discover accounts that the
+// user used previously (based on the chain state), but ones that he/she did not
+// explicitly pin to the wallet manually. To avoid chain head monitoring, self
+// derivation only runs during account listing (and even then throttled).
+func (w *ledgerWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {
+	w.lock.Lock()
+	defer w.lock.Unlock()
+
+	w.selfDeriveNextPath = make(accounts.DerivationPath, len(base))
+	copy(w.selfDeriveNextPath[:], base[:])
+
+	w.selfDeriveNextAddr = common.Address{}
+	w.selfDeriveChain = chain
+}
+
 // SignHash implements accounts.Wallet, however signing arbitrary data is not
 // supported for Ledger wallets, so this method will always return an error.
 func (w *ledgerWallet) SignHash(acc accounts.Account, hash []byte) ([]byte, error) {

+ 30 - 2
cmd/geth/main.go

@@ -25,12 +25,14 @@ import (
 	"strings"
 	"time"
 
+	"github.com/ethereum/go-ethereum/accounts"
 	"github.com/ethereum/go-ethereum/accounts/keystore"
 	"github.com/ethereum/go-ethereum/cmd/utils"
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/console"
 	"github.com/ethereum/go-ethereum/contracts/release"
 	"github.com/ethereum/go-ethereum/eth"
+	"github.com/ethereum/go-ethereum/ethclient"
 	"github.com/ethereum/go-ethereum/internal/debug"
 	"github.com/ethereum/go-ethereum/logger"
 	"github.com/ethereum/go-ethereum/logger/glog"
@@ -249,12 +251,38 @@ func startNode(ctx *cli.Context, stack *node.Node) {
 	ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
 
 	passwords := utils.MakePasswordList(ctx)
-	accounts := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
-	for i, account := range accounts {
+	unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
+	for i, account := range unlocks {
 		if trimmed := strings.TrimSpace(account); trimmed != "" {
 			unlockAccount(ctx, ks, trimmed, i, passwords)
 		}
 	}
+	// Register wallet event handlers to open and auto-derive wallets
+	events := make(chan accounts.WalletEvent, 16)
+	stack.AccountManager().Subscribe(events)
+
+	go func() {
+		// Create an chain state reader for self-derivation
+		rpcClient, err := stack.Attach()
+		if err != nil {
+			utils.Fatalf("Failed to attach to self: %v", err)
+		}
+		stateReader := ethclient.NewClient(rpcClient)
+
+		// Listen for wallet event till termination
+		for event := range events {
+			if event.Arrive {
+				if err := event.Wallet.Open(""); err != nil {
+					glog.V(logger.Info).Infof("New wallet appeared: %s, failed to open: %s", event.Wallet.URL(), err)
+				} else {
+					glog.V(logger.Info).Infof("New wallet appeared: %s, %s", event.Wallet.URL(), event.Wallet.Status())
+				}
+				event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
+			} else {
+				glog.V(logger.Info).Infof("Old wallet dropped:  %s", event.Wallet.URL())
+			}
+		}
+	}()
 	// Start auxiliary services if enabled
 	if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
 		var ethereum *eth.Ethereum

+ 5 - 1
internal/ethapi/api.go

@@ -253,10 +253,14 @@ func (s *PrivateAccountAPI) DeriveAccount(url string, path string, pin *bool) (a
 	if err != nil {
 		return accounts.Account{}, err
 	}
+	derivPath, err := accounts.ParseDerivationPath(path)
+	if err != nil {
+		return accounts.Account{}, err
+	}
 	if pin == nil {
 		pin = new(bool)
 	}
-	return wallet.Derive(path, *pin)
+	return wallet.Derive(derivPath, *pin)
 }
 
 // NewAccount will create a new account and returns the address for the new account.

+ 1 - 18
node/config.go

@@ -446,22 +446,5 @@ func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
 	} else {
 		backends = append(backends, ledgerhub)
 	}
-	am := accounts.NewManager(backends...)
-
-	// Start some logging for the user
-	changes := make(chan accounts.WalletEvent, 16)
-	am.Subscribe(changes)
-	go func() {
-		for event := range changes {
-			if event.Arrive {
-				glog.V(logger.Info).Infof("New wallet appeared: %s", event.Wallet.URL())
-				if err := event.Wallet.Open(""); err != nil {
-					glog.V(logger.Warn).Infof("Failed to open wallet %s: %v", event.Wallet.URL(), err)
-				}
-			} else {
-				glog.V(logger.Info).Infof("Old wallet disappeared: %s", event.Wallet.URL())
-			}
-		}
-	}()
-	return am, ephemeral, nil
+	return accounts.NewManager(backends...), ephemeral, nil
 }