From 205ea9580215cca4093dff22ec61222bc3a6ff96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Wed, 8 Feb 2017 20:25:52 +0200 Subject: [PATCH] accounts, cmd, internal, node: implement HD wallet self-derivation --- accounts/accounts.go | 15 ++- accounts/hd.go | 130 ++++++++++++++++++++++ accounts/hd_test.go | 79 ++++++++++++++ accounts/keystore/keystore_wallet.go | 7 +- accounts/usbwallet/ledger_test.go | 77 -------------- accounts/usbwallet/ledger_wallet.go | 154 ++++++++++++++++++++------- cmd/geth/main.go | 32 +++++- internal/ethapi/api.go | 6 +- node/config.go | 19 +--- 9 files changed, 383 insertions(+), 136 deletions(-) create mode 100644 accounts/hd.go create mode 100644 accounts/hd_test.go delete mode 100644 accounts/usbwallet/ledger_test.go diff --git a/accounts/accounts.go b/accounts/accounts.go index 6c9e0bbce..640de5220 100644 --- a/accounts/accounts.go +++ b/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. // diff --git a/accounts/hd.go b/accounts/hd.go new file mode 100644 index 000000000..e8bc191af --- /dev/null +++ b/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 . + +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 +} diff --git a/accounts/hd_test.go b/accounts/hd_test.go new file mode 100644 index 000000000..83ec34adb --- /dev/null +++ b/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 . + +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) + } + } +} diff --git a/accounts/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go index 7d5507a4f..7165d2821 100644 --- a/accounts/keystore/keystore_wallet.go +++ b/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 diff --git a/accounts/usbwallet/ledger_test.go b/accounts/usbwallet/ledger_test.go deleted file mode 100644 index 16a1e0b3f..000000000 --- a/accounts/usbwallet/ledger_test.go +++ /dev/null @@ -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 . - -// +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) - } -}*/ diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go index f712a503f..0481a8990 100644 --- a/accounts/usbwallet/ledger_wallet.go +++ b/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) { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index e324802b5..06dc55ba8 100644 --- a/cmd/geth/main.go +++ b/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 diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 85bb10f17..f49434e17 100644 --- a/internal/ethapi/api.go +++ b/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. diff --git a/node/config.go b/node/config.go index 47ecd22a3..c09f51747 100644 --- a/node/config.go +++ b/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 }