diff options
author | Péter Szilágyi <peterke@gmail.com> | 2017-02-07 18:47:34 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2017-02-13 20:00:07 +0800 |
commit | fad5eb0a87abfc12812647344a26de8a43830182 (patch) | |
tree | a679f37933a85edade15ee69b385b6705cc94043 /accounts | |
parent | b3c0e9d3ccb0bb326646aea47dda391a9552b122 (diff) | |
download | dexon-fad5eb0a87abfc12812647344a26de8a43830182.tar dexon-fad5eb0a87abfc12812647344a26de8a43830182.tar.gz dexon-fad5eb0a87abfc12812647344a26de8a43830182.tar.bz2 dexon-fad5eb0a87abfc12812647344a26de8a43830182.tar.lz dexon-fad5eb0a87abfc12812647344a26de8a43830182.tar.xz dexon-fad5eb0a87abfc12812647344a26de8a43830182.tar.zst dexon-fad5eb0a87abfc12812647344a26de8a43830182.zip |
accounts, cmd, eth, internal, miner, node: wallets and HD APIs
Diffstat (limited to 'accounts')
-rw-r--r-- | accounts/accounts.go | 256 | ||||
-rw-r--r-- | accounts/backend.go | 88 | ||||
-rw-r--r-- | accounts/errors.go | 29 | ||||
-rw-r--r-- | accounts/keystore/account_cache.go (renamed from accounts/keystore/address_cache.go) | 59 | ||||
-rw-r--r-- | accounts/keystore/account_cache_test.go (renamed from accounts/keystore/address_cache_test.go) | 53 | ||||
-rw-r--r-- | accounts/keystore/keystore.go | 178 | ||||
-rw-r--r-- | accounts/keystore/keystore_test.go | 156 | ||||
-rw-r--r-- | accounts/keystore/keystore_wallet.go | 133 | ||||
-rw-r--r-- | accounts/keystore/watch.go | 4 | ||||
-rw-r--r-- | accounts/keystore/watch_fallback.go | 2 | ||||
-rw-r--r-- | accounts/manager.go | 194 | ||||
-rw-r--r-- | accounts/usbwallet/ledger_hub.go | 205 | ||||
-rw-r--r-- | accounts/usbwallet/ledger_wallet.go (renamed from accounts/usbwallet/ledger.go) | 547 |
13 files changed, 1339 insertions, 565 deletions
diff --git a/accounts/accounts.go b/accounts/accounts.go index 234b6e456..b367ee2f4 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -18,162 +18,128 @@ package accounts import ( - "encoding/json" - "errors" "math/big" - "reflect" - "sync" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" ) -// ErrUnknownAccount is returned for any requested operation for which no backend -// provides the specified account. -var ErrUnknownAccount = errors.New("unknown account") - -// ErrNotSupported is returned when an operation is requested from an account -// backend that it does not support. -var ErrNotSupported = errors.New("not supported") - -// Account represents a stored key. -// When used as an argument, it selects a unique key to act on. +// Account represents an Ethereum account located at a specific location defined +// by the optional URL field. type Account struct { - Address common.Address // Ethereum account address derived from the key - URL string // Optional resource locator within a backend - backend Backend // Backend where this account originates from -} - -func (acc *Account) MarshalJSON() ([]byte, error) { - return []byte(`"` + acc.Address.Hex() + `"`), nil -} - -func (acc *Account) UnmarshalJSON(raw []byte) error { - return json.Unmarshal(raw, &acc.Address) -} - -// Manager is an overarching account manager that can communicate with various -// backends for signing transactions. -type Manager struct { - backends []Backend // List of currently registered backends (ordered by registration) - index map[reflect.Type]Backend // Set of currently registered backends - lock sync.RWMutex -} - -// NewManager creates a generic account manager to sign transaction via various -// supported backends. -func NewManager(backends ...Backend) *Manager { - am := &Manager{ - backends: backends, - index: make(map[reflect.Type]Backend), - } - for _, backend := range backends { - am.index[reflect.TypeOf(backend)] = backend - } - return am -} - -// Backend retrieves the backend with the given type from the account manager. -func (am *Manager) Backend(backend reflect.Type) Backend { - return am.index[backend] + Address common.Address `json:"address"` // Ethereum account address derived from the key + URL string `json:"url"` // Optional resource locator within a backend } -// Accounts returns all signer accounts registered under this account manager. -func (am *Manager) Accounts() []Account { - am.lock.RLock() - defer am.lock.RUnlock() - - var all []Account - for _, backend := range am.backends { // TODO(karalabe): cache these after subscriptions are in - accounts := backend.Accounts() - for i := 0; i < len(accounts); i++ { - accounts[i].backend = backend - } - all = append(all, accounts...) - } - return all +// Wallet represents a software or hardware wallet that might contain one or more +// accounts (derived from the same seed). +type Wallet interface { + // Type retrieves a textual representation of the type of the wallet. + Type() string + + // URL retrieves the canonical path under which this wallet is reachable. It is + // user by upper layers to define a sorting order over all wallets from multiple + // backends. + URL() string + + // Status returns a textual status to aid the user in the current state of the + // wallet. + Status() string + + // Open initializes access to a wallet instance. It is not meant to unlock or + // decrypt account keys, rather simply to establish a connection to hardware + // wallets and/or to access derivation seeds. + // + // The passphrase parameter may or may not be used by the implementation of a + // particular wallet instance. The reason there is no passwordless open method + // is to strive towards a uniform wallet handling, oblivious to the different + // backend providers. + // + // Please note, if you open a wallet, you must close it to release any allocated + // resources (especially important when working with hardware wallets). + Open(passphrase string) error + + // Close releases any resources held by an open wallet instance. + Close() error + + // Accounts retrieves the list of signing accounts the wallet is currently aware + // of. For hierarchical deterministic wallets, the list will not be exhaustive, + // rather only contain the accounts explicitly pinned during account derivation. + Accounts() []Account + + // Contains returns whether an account is part of this particular wallet or not. + Contains(account Account) bool + + // 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) + + // SignHash requests the wallet to sign the given hash. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + // + // If the wallet requires additional authentication to sign the request (e.g. + // a password to decrypt the account, or a PIN code o verify the transaction), + // an AuthNeededError instance will be returned, containing infos for the user + // about which fields or actions are needed. The user may retry by providing + // the needed details via SignHashWithPassphrase, or by other means (e.g. unlock + // the account in a keystore). + SignHash(account Account, hash []byte) ([]byte, error) + + // SignTx requests the wallet to sign the given transaction. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + // + // If the wallet requires additional authentication to sign the request (e.g. + // a password to decrypt the account, or a PIN code o verify the transaction), + // an AuthNeededError instance will be returned, containing infos for the user + // about which fields or actions are needed. The user may retry by providing + // the needed details via SignTxWithPassphrase, or by other means (e.g. unlock + // the account in a keystore). + SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + + // SignHashWithPassphrase requests the wallet to sign the given hash with the + // given passphrase as extra authentication information. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + SignHashWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error) + + // SignTxWithPassphrase requests the wallet to sign the given transaction, with the + // given passphrase as extra authentication information. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + SignTxWithPassphrase(account Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) } -// HasAddress reports whether a key with the given address is present. -func (am *Manager) HasAddress(addr common.Address) bool { - am.lock.RLock() - defer am.lock.RUnlock() - - for _, backend := range am.backends { - if backend.HasAddress(addr) { - return true - } - } - return false +// Backend is a "wallet provider" that may contain a batch of accounts they can +// sign transactions with and upon request, do so. +type Backend interface { + // Wallets retrieves the list of wallets the backend is currently aware of. + // + // The returned wallets are not opened by default. For software HD wallets this + // means that no base seeds are decrypted, and for hardware wallets that no actual + // connection is established. + // + // The resulting wallet list will be sorted alphabetically based on its internal + // URL assigned by the backend. Since wallets (especially hardware) may come and + // go, the same wallet might appear at a different positions in the list during + // subsequent retrievals. + Wallets() []Wallet + + // Subscribe creates an async subscription to receive notifications when the + // backend detects the arrival or departure of a wallet. + Subscribe(sink chan<- WalletEvent) event.Subscription } -// SignHash requests the account manager to get the hash signed with an arbitrary -// signing backend holding the authorization for the specified account. -func (am *Manager) SignHash(acc Account, hash []byte) ([]byte, error) { - am.lock.RLock() - defer am.lock.RUnlock() - - if err := am.ensureBackend(&acc); err != nil { - return nil, err - } - return acc.backend.SignHash(acc, hash) -} - -// SignTx requests the account manager to get the transaction signed with an -// arbitrary signing backend holding the authorization for the specified account. -func (am *Manager) SignTx(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - am.lock.RLock() - defer am.lock.RUnlock() - - if err := am.ensureBackend(&acc); err != nil { - return nil, err - } - return acc.backend.SignTx(acc, tx, chainID) -} - -// SignHashWithPassphrase requests the account manager to get the hash signed with -// an arbitrary signing backend holding the authorization for the specified account. -func (am *Manager) SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) { - am.lock.RLock() - defer am.lock.RUnlock() - - if err := am.ensureBackend(&acc); err != nil { - return nil, err - } - return acc.backend.SignHashWithPassphrase(acc, passphrase, hash) -} - -// SignTxWithPassphrase requests the account manager to get the transaction signed -// with an arbitrary signing backend holding the authorization for the specified -// account. -func (am *Manager) SignTxWithPassphrase(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - am.lock.RLock() - defer am.lock.RUnlock() - - if err := am.ensureBackend(&acc); err != nil { - return nil, err - } - return acc.backend.SignTxWithPassphrase(acc, passphrase, tx, chainID) -} - -// ensureBackend ensures that the account has a correctly set backend and that -// it is still alive. -// -// Please note, this method assumes the manager lock is held! -func (am *Manager) ensureBackend(acc *Account) error { - // If we have a backend, make sure it's still live - if acc.backend != nil { - if _, exists := am.index[reflect.TypeOf(acc.backend)]; !exists { - return ErrUnknownAccount - } - return nil - } - // If we don't have a known backend, look up one that can service it - for _, backend := range am.backends { - if backend.HasAddress(acc.Address) { // TODO(karalabe): this assumes unique addresses per backend - acc.backend = backend - return nil - } - } - return ErrUnknownAccount +// WalletEvent is an event fired by an account backend when a wallet arrival or +// departure is detected. +type WalletEvent struct { + Wallet Wallet // Wallet instance arrived or departed + Arrive bool // Whether the wallet was added or removed } diff --git a/accounts/backend.go b/accounts/backend.go deleted file mode 100644 index 5f7ac0717..000000000 --- a/accounts/backend.go +++ /dev/null @@ -1,88 +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/>. - -package accounts - -import ( - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" -) - -// Backend is an "account provider" that can specify a batch of accounts it can -// sign transactions with and upon request, do so. -type Backend interface { - // Accounts retrieves the list of signing accounts the backend is currently aware of. - Accounts() []Account - - // HasAddress reports whether an account with the given address is present. - HasAddress(addr common.Address) bool - - // SignHash requests the backend to sign the given hash. - // - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - // - // If the backend requires additional authentication to sign the request (e.g. - // a password to decrypt the account, or a PIN code o verify the transaction), - // an AuthNeededError instance will be returned, containing infos for the user - // about which fields or actions are needed. The user may retry by providing - // the needed details via SignHashWithPassphrase, or by other means (e.g. unlock - // the account in a keystore). - SignHash(acc Account, hash []byte) ([]byte, error) - - // SignTx requests the backend to sign the given transaction. - // - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - // - // If the backend requires additional authentication to sign the request (e.g. - // a password to decrypt the account, or a PIN code o verify the transaction), - // an AuthNeededError instance will be returned, containing infos for the user - // about which fields or actions are needed. The user may retry by providing - // the needed details via SignTxWithPassphrase, or by other means (e.g. unlock - // the account in a keystore). - SignTx(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) - - // SignHashWithPassphrase requests the backend to sign the given transaction with - // the given passphrase as extra authentication information. - // - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) - - // SignTxWithPassphrase requests the backend to sign the given transaction, with the - // given passphrase as extra authentication information. - // - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - SignTxWithPassphrase(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) - - // TODO(karalabe,fjl): watching and caching needs the Go subscription system - // Watch requests the backend to send a notification to the specified channel whenever - // an new account appears or an existing one disappears. - //Watch(chan AccountEvent) error - - // Unwatch requests the backend stop sending notifications to the given channel. - //Unwatch(chan AccountEvent) error -} - -// TODO(karalabe,fjl): watching and caching needs the Go subscription system -// type AccountEvent struct { -// Account Account -// Added bool -// } diff --git a/accounts/errors.go b/accounts/errors.go index d6f578b52..9ecc1eafd 100644 --- a/accounts/errors.go +++ b/accounts/errors.go @@ -16,7 +16,34 @@ package accounts -import "fmt" +import ( + "errors" + "fmt" +) + +// ErrUnknownAccount is returned for any requested operation for which no backend +// provides the specified account. +var ErrUnknownAccount = errors.New("unknown account") + +// ErrUnknownWallet is returned for any requested operation for which no backend +// provides the specified wallet. +var ErrUnknownWallet = errors.New("unknown wallet") + +// ErrNotSupported is returned when an operation is requested from an account +// backend that it does not support. +var ErrNotSupported = errors.New("not supported") + +// ErrInvalidPassphrase is returned when a decryption operation receives a bad +// passphrase. +var ErrInvalidPassphrase = errors.New("invalid passphrase") + +// ErrWalletAlreadyOpen is returned if a wallet is attempted to be opened the +// secodn time. +var ErrWalletAlreadyOpen = errors.New("wallet already open") + +// ErrWalletClosed is returned if a wallet is attempted to be opened the +// secodn time. +var ErrWalletClosed = errors.New("wallet closed") // AuthNeededError is returned by backends for signing requests where the user // is required to provide further authentication before signing can succeed. diff --git a/accounts/keystore/address_cache.go b/accounts/keystore/account_cache.go index eb3e3263b..cc8626afc 100644 --- a/accounts/keystore/address_cache.go +++ b/accounts/keystore/account_cache.go @@ -39,11 +39,11 @@ import ( // exist yet, the code will attempt to create a watcher at most this often. const minReloadInterval = 2 * time.Second -type accountsByFile []accounts.Account +type accountsByURL []accounts.Account -func (s accountsByFile) Len() int { return len(s) } -func (s accountsByFile) Less(i, j int) bool { return s[i].URL < s[j].URL } -func (s accountsByFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s accountsByURL) Len() int { return len(s) } +func (s accountsByURL) Less(i, j int) bool { return s[i].URL < s[j].URL } +func (s accountsByURL) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // AmbiguousAddrError is returned when attempting to unlock // an address for which more than one file exists. @@ -63,26 +63,28 @@ func (err *AmbiguousAddrError) Error() string { return fmt.Sprintf("multiple keys match address (%s)", files) } -// addressCache is a live index of all accounts in the keystore. -type addressCache struct { +// accountCache is a live index of all accounts in the keystore. +type accountCache struct { keydir string watcher *watcher mu sync.Mutex - all accountsByFile + all accountsByURL byAddr map[common.Address][]accounts.Account throttle *time.Timer + notify chan struct{} } -func newAddrCache(keydir string) *addressCache { - ac := &addressCache{ +func newAccountCache(keydir string) (*accountCache, chan struct{}) { + ac := &accountCache{ keydir: keydir, byAddr: make(map[common.Address][]accounts.Account), + notify: make(chan struct{}, 1), } ac.watcher = newWatcher(ac) - return ac + return ac, ac.notify } -func (ac *addressCache) accounts() []accounts.Account { +func (ac *accountCache) accounts() []accounts.Account { ac.maybeReload() ac.mu.Lock() defer ac.mu.Unlock() @@ -91,14 +93,14 @@ func (ac *addressCache) accounts() []accounts.Account { return cpy } -func (ac *addressCache) hasAddress(addr common.Address) bool { +func (ac *accountCache) hasAddress(addr common.Address) bool { ac.maybeReload() ac.mu.Lock() defer ac.mu.Unlock() return len(ac.byAddr[addr]) > 0 } -func (ac *addressCache) add(newAccount accounts.Account) { +func (ac *accountCache) add(newAccount accounts.Account) { ac.mu.Lock() defer ac.mu.Unlock() @@ -111,18 +113,28 @@ func (ac *addressCache) add(newAccount accounts.Account) { copy(ac.all[i+1:], ac.all[i:]) ac.all[i] = newAccount ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount) + + select { + case ac.notify <- struct{}{}: + default: + } } // note: removed needs to be unique here (i.e. both File and Address must be set). -func (ac *addressCache) delete(removed accounts.Account) { +func (ac *accountCache) delete(removed accounts.Account) { ac.mu.Lock() defer ac.mu.Unlock() + ac.all = removeAccount(ac.all, removed) if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { delete(ac.byAddr, removed.Address) } else { ac.byAddr[removed.Address] = ba } + select { + case ac.notify <- struct{}{}: + default: + } } func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account { @@ -137,7 +149,7 @@ func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.A // find returns the cached account for address if there is a unique match. // The exact matching rules are explained by the documentation of accounts.Account. // Callers must hold ac.mu. -func (ac *addressCache) find(a accounts.Account) (accounts.Account, error) { +func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { // Limit search to address candidates if possible. matches := ac.all if (a.Address != common.Address{}) { @@ -169,9 +181,10 @@ func (ac *addressCache) find(a accounts.Account) (accounts.Account, error) { } } -func (ac *addressCache) maybeReload() { +func (ac *accountCache) maybeReload() { ac.mu.Lock() defer ac.mu.Unlock() + if ac.watcher.running { return // A watcher is running and will keep the cache up-to-date. } @@ -189,18 +202,22 @@ func (ac *addressCache) maybeReload() { ac.throttle.Reset(minReloadInterval) } -func (ac *addressCache) close() { +func (ac *accountCache) close() { ac.mu.Lock() ac.watcher.close() if ac.throttle != nil { ac.throttle.Stop() } + if ac.notify != nil { + close(ac.notify) + ac.notify = nil + } ac.mu.Unlock() } // reload caches addresses of existing accounts. // Callers must hold ac.mu. -func (ac *addressCache) reload() { +func (ac *accountCache) reload() { accounts, err := ac.scan() if err != nil && glog.V(logger.Debug) { glog.Errorf("can't load keys: %v", err) @@ -213,10 +230,14 @@ func (ac *addressCache) reload() { for _, a := range accounts { ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a) } + select { + case ac.notify <- struct{}{}: + default: + } glog.V(logger.Debug).Infof("reloaded keys, cache has %d accounts", len(ac.all)) } -func (ac *addressCache) scan() ([]accounts.Account, error) { +func (ac *accountCache) scan() ([]accounts.Account, error) { files, err := ioutil.ReadDir(ac.keydir) if err != nil { return nil, err diff --git a/accounts/keystore/address_cache_test.go b/accounts/keystore/account_cache_test.go index 68af74338..ea6f7d011 100644 --- a/accounts/keystore/address_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -53,11 +53,11 @@ var ( func TestWatchNewFile(t *testing.T) { t.Parallel() - dir, am := tmpKeyStore(t, false) + dir, ks := tmpKeyStore(t, false) defer os.RemoveAll(dir) // Ensure the watcher is started before adding any files. - am.Accounts() + ks.Accounts() time.Sleep(200 * time.Millisecond) // Move in the files. @@ -71,11 +71,17 @@ func TestWatchNewFile(t *testing.T) { } } - // am should see the accounts. + // ks should see the accounts. var list []accounts.Account for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 { - list = am.Accounts() + list = ks.Accounts() if reflect.DeepEqual(list, wantAccounts) { + // ks should have also received change notifications + select { + case <-ks.changes: + default: + t.Fatalf("wasn't notified of new accounts") + } return } time.Sleep(d) @@ -86,12 +92,12 @@ func TestWatchNewFile(t *testing.T) { func TestWatchNoDir(t *testing.T) { t.Parallel() - // Create am but not the directory that it watches. + // Create ks but not the directory that it watches. rand.Seed(time.Now().UnixNano()) dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watch-test-%d-%d", os.Getpid(), rand.Int())) - am := NewKeyStore(dir, LightScryptN, LightScryptP) + ks := NewKeyStore(dir, LightScryptN, LightScryptP) - list := am.Accounts() + list := ks.Accounts() if len(list) > 0 { t.Error("initial account list not empty:", list) } @@ -105,12 +111,18 @@ func TestWatchNoDir(t *testing.T) { t.Fatal(err) } - // am should see the account. + // ks should see the account. wantAccounts := []accounts.Account{cachetestAccounts[0]} wantAccounts[0].URL = file for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { - list = am.Accounts() + list = ks.Accounts() if reflect.DeepEqual(list, wantAccounts) { + // ks should have also received change notifications + select { + case <-ks.changes: + default: + t.Fatalf("wasn't notified of new accounts") + } return } time.Sleep(d) @@ -119,7 +131,7 @@ func TestWatchNoDir(t *testing.T) { } func TestCacheInitialReload(t *testing.T) { - cache := newAddrCache(cachetestDir) + cache, _ := newAccountCache(cachetestDir) accounts := cache.accounts() if !reflect.DeepEqual(accounts, cachetestAccounts) { t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) @@ -127,7 +139,7 @@ func TestCacheInitialReload(t *testing.T) { } func TestCacheAddDeleteOrder(t *testing.T) { - cache := newAddrCache("testdata/no-such-dir") + cache, notify := newAccountCache("testdata/no-such-dir") cache.watcher.running = true // prevent unexpected reloads accs := []accounts.Account{ @@ -163,14 +175,24 @@ func TestCacheAddDeleteOrder(t *testing.T) { for _, a := range accs { cache.add(a) } + select { + case <-notify: + default: + t.Fatalf("notifications didn't fire for adding new accounts") + } // Add some of them twice to check that they don't get reinserted. cache.add(accs[0]) cache.add(accs[2]) + select { + case <-notify: + t.Fatalf("notifications fired for adding existing accounts") + default: + } // Check that the account list is sorted by filename. wantAccounts := make([]accounts.Account, len(accs)) copy(wantAccounts, accs) - sort.Sort(accountsByFile(wantAccounts)) + sort.Sort(accountsByURL(wantAccounts)) list := cache.accounts() if !reflect.DeepEqual(list, wantAccounts) { t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts)) @@ -190,6 +212,11 @@ func TestCacheAddDeleteOrder(t *testing.T) { } cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"}) + select { + case <-notify: + default: + t.Fatalf("notifications didn't fire for deleting accounts") + } // Check content again after deletion. wantAccountsAfterDelete := []accounts.Account{ wantAccounts[1], @@ -212,7 +239,7 @@ func TestCacheAddDeleteOrder(t *testing.T) { func TestCacheFind(t *testing.T) { dir := filepath.Join("testdata", "dir") - cache := newAddrCache(dir) + cache, _ := newAccountCache(dir) cache.watcher.running = true // prevent unexpected reloads accs := []accounts.Account{ diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index d125f7d62..ce4e87ce9 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -37,23 +37,34 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/event" ) var ( - ErrNeedPasswordOrUnlock = accounts.NewAuthNeededError("password or unlock") - ErrNoMatch = errors.New("no key for given address or file") - ErrDecrypt = errors.New("could not decrypt key with given passphrase") + ErrLocked = accounts.NewAuthNeededError("password or unlock") + ErrNoMatch = errors.New("no key for given address or file") + ErrDecrypt = errors.New("could not decrypt key with given passphrase") ) -// BackendType can be used to query the account manager for encrypted keystores. -var BackendType = reflect.TypeOf(new(KeyStore)) +// KeyStoreType is the reflect type of a keystore backend. +var KeyStoreType = reflect.TypeOf(&KeyStore{}) + +// Maximum time between wallet refreshes (if filesystem notifications don't work). +const walletRefreshCycle = 3 * time.Second // KeyStore manages a key storage directory on disk. type KeyStore struct { - cache *addressCache - keyStore keyStore - mu sync.RWMutex - unlocked map[common.Address]*unlocked + storage keyStore // Storage backend, might be cleartext or encrypted + cache *accountCache // In-memory account cache over the filesystem storage + changes chan struct{} // Channel receiving change notifications from the cache + unlocked map[common.Address]*unlocked // Currently unlocked account (decrypted private keys) + + wallets []accounts.Wallet // Wallet wrappers around the individual key files + updateFeed event.Feed // Event feed to notify wallet additions/removals + updateScope event.SubscriptionScope // Subscription scope tracking current live listeners + updating bool // Whether the event notification loop is running + + mu sync.RWMutex } type unlocked struct { @@ -64,7 +75,7 @@ type unlocked struct { // NewKeyStore creates a keystore for the given directory. func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { keydir, _ = filepath.Abs(keydir) - ks := &KeyStore{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}} + ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP}} ks.init(keydir) return ks } @@ -73,20 +84,136 @@ func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { // Deprecated: Use NewKeyStore. func NewPlaintextKeyStore(keydir string) *KeyStore { keydir, _ = filepath.Abs(keydir) - ks := &KeyStore{keyStore: &keyStorePlain{keydir}} + ks := &KeyStore{storage: &keyStorePlain{keydir}} ks.init(keydir) return ks } func (ks *KeyStore) init(keydir string) { + // Lock the mutex since the account cache might call back with events + ks.mu.Lock() + defer ks.mu.Unlock() + + // Initialize the set of unlocked keys and the account cache ks.unlocked = make(map[common.Address]*unlocked) - ks.cache = newAddrCache(keydir) + ks.cache, ks.changes = newAccountCache(keydir) + // TODO: In order for this finalizer to work, there must be no references // to ks. addressCache doesn't keep a reference but unlocked keys do, // so the finalizer will not trigger until all timed unlocks have expired. runtime.SetFinalizer(ks, func(m *KeyStore) { m.cache.close() }) + // Create the initial list of wallets from the cache + accs := ks.cache.accounts() + ks.wallets = make([]accounts.Wallet, len(accs)) + for i := 0; i < len(accs); i++ { + ks.wallets[i] = &keystoreWallet{account: accs[i], keystore: ks} + } +} + +// Wallets implements accounts.Backend, returning all single-key wallets from the +// keystore directory. +func (ks *KeyStore) Wallets() []accounts.Wallet { + // Make sure the list of wallets is in sync with the account cache + ks.refreshWallets() + + ks.mu.RLock() + defer ks.mu.RUnlock() + + cpy := make([]accounts.Wallet, len(ks.wallets)) + copy(cpy, ks.wallets) + return cpy +} + +// refreshWallets retrieves the current account list and based on that does any +// necessary wallet refreshes. +func (ks *KeyStore) refreshWallets() { + // Retrieve the current list of accounts + accs := ks.cache.accounts() + + // Transform the current list of wallets into the new one + ks.mu.Lock() + + wallets := make([]accounts.Wallet, 0, len(accs)) + events := []accounts.WalletEvent{} + + for _, account := range accs { + // Drop wallets while they were in front of the next account + for len(ks.wallets) > 0 && ks.wallets[0].URL() < account.URL { + events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Arrive: false}) + ks.wallets = ks.wallets[1:] + } + // If there are no more wallets or the account is before the next, wrap new wallet + if len(ks.wallets) == 0 || ks.wallets[0].URL() > account.URL { + wallet := &keystoreWallet{account: account, keystore: ks} + + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) + wallets = append(wallets, wallet) + continue + } + // If the account is the same as the first wallet, keep it + if ks.wallets[0].Accounts()[0] == account { + wallets = append(wallets, ks.wallets[0]) + ks.wallets = ks.wallets[1:] + continue + } + } + // Drop any leftover wallets and set the new batch + for _, wallet := range ks.wallets { + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false}) + } + ks.wallets = wallets + ks.mu.Unlock() + + // Fire all wallet events and return + for _, event := range events { + ks.updateFeed.Send(event) + } +} + +// Subscribe implements accounts.Backend, creating an async subscription to +// receive notifications on the addition or removal of keystore wallets. +func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { + // We need the mutex to reliably start/stop the update loop + ks.mu.Lock() + defer ks.mu.Unlock() + + // Subscribe the caller and track the subscriber count + sub := ks.updateScope.Track(ks.updateFeed.Subscribe(sink)) + + // Subscribers require an active notification loop, start it + if !ks.updating { + ks.updating = true + go ks.updater() + } + return sub +} + +// updater is responsible for maintaining an up-to-date list of wallets stored in +// the keystore, and for firing wallet addition/removal events. It listens for +// account change events from the underlying account cache, and also periodically +// forces a manual refresh (only triggers for systems where the filesystem notifier +// is not running). +func (ks *KeyStore) updater() { + for { + // Wait for an account update or a refresh timeout + select { + case <-ks.changes: + case <-time.After(walletRefreshCycle): + } + // Run the wallet refresher + ks.refreshWallets() + + // If all our subscribers left, stop the updater + ks.mu.Lock() + if ks.updateScope.Count() == 0 { + ks.updating = false + ks.mu.Unlock() + return + } + ks.mu.Unlock() + } } // HasAddress reports whether a key with the given address is present. @@ -118,6 +245,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { err = os.Remove(a.URL) if err == nil { ks.cache.delete(a) + ks.refreshWallets() } return err } @@ -131,7 +259,7 @@ func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) { unlockedKey, found := ks.unlocked[a.Address] if !found { - return nil, ErrNeedPasswordOrUnlock + return nil, ErrLocked } // Sign the hash using plain ECDSA operations return crypto.Sign(hash, unlockedKey.PrivateKey) @@ -145,7 +273,7 @@ func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *b unlockedKey, found := ks.unlocked[a.Address] if !found { - return nil, ErrNeedPasswordOrUnlock + return nil, ErrLocked } // Depending on the presence of the chain ID, sign with EIP155 or homestead if chainID != nil { @@ -221,10 +349,9 @@ func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout t // it with a timeout would be confusing. zeroKey(key.PrivateKey) return nil - } else { - // Terminate the expire goroutine and replace it below. - close(u.abort) } + // Terminate the expire goroutine and replace it below. + close(u.abort) } if timeout > 0 { u = &unlocked{Key: key, abort: make(chan struct{})} @@ -250,7 +377,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A if err != nil { return a, nil, err } - key, err := ks.keyStore.GetKey(a.Address, a.URL, auth) + key, err := ks.storage.GetKey(a.Address, a.URL, auth) return a, key, err } @@ -277,13 +404,14 @@ func (ks *KeyStore) expire(addr common.Address, u *unlocked, timeout time.Durati // NewAccount generates a new key and stores it into the key directory, // encrypting it with the passphrase. func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { - _, account, err := storeNewKey(ks.keyStore, crand.Reader, passphrase) + _, account, err := storeNewKey(ks.storage, crand.Reader, passphrase) if err != nil { return accounts.Account{}, err } // Add the account to the cache immediately rather // than waiting for file system notifications to pick it up. ks.cache.add(account) + ks.refreshWallets() return account, nil } @@ -294,7 +422,7 @@ func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) return nil, err } var N, P int - if store, ok := ks.keyStore.(*keyStorePassphrase); ok { + if store, ok := ks.storage.(*keyStorePassphrase); ok { N, P = store.scryptN, store.scryptP } else { N, P = StandardScryptN, StandardScryptP @@ -325,11 +453,12 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco } func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { - a := accounts.Account{Address: key.Address, URL: ks.keyStore.JoinPath(keyFileName(key.Address))} - if err := ks.keyStore.StoreKey(a.URL, key, passphrase); err != nil { + a := accounts.Account{Address: key.Address, URL: ks.storage.JoinPath(keyFileName(key.Address))} + if err := ks.storage.StoreKey(a.URL, key, passphrase); err != nil { return accounts.Account{}, err } ks.cache.add(a) + ks.refreshWallets() return a, nil } @@ -339,17 +468,18 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) if err != nil { return err } - return ks.keyStore.StoreKey(a.URL, key, newPassphrase) + return ks.storage.StoreKey(a.URL, key, newPassphrase) } // ImportPreSaleKey decrypts the given Ethereum presale wallet and stores // a key file in the key directory. The key file is encrypted with the same passphrase. func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { - a, _, err := importPreSaleKey(ks.keyStore, keyJSON, passphrase) + a, _, err := importPreSaleKey(ks.storage, keyJSON, passphrase) if err != nil { return a, err } ks.cache.add(a) + ks.refreshWallets() return a, nil } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index af2140c31..6b7170a2f 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -18,14 +18,17 @@ package keystore import ( "io/ioutil" + "math/rand" "os" "runtime" + "sort" "strings" "testing" "time" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/event" ) var testSigData = make([]byte, 32) @@ -122,8 +125,8 @@ func TestTimedUnlock(t *testing.T) { // Signing without passphrase fails because account is locked _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) - if err != ErrNeedPasswordOrUnlock { - t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock before unlocking, got ", err) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err) } // Signing with passphrase works @@ -140,8 +143,8 @@ func TestTimedUnlock(t *testing.T) { // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) - if err != ErrNeedPasswordOrUnlock { - t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock timeout expired, got ", err) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) } } @@ -180,8 +183,8 @@ func TestOverrideUnlock(t *testing.T) { // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) - if err != ErrNeedPasswordOrUnlock { - t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock timeout expired, got ", err) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) } } @@ -201,7 +204,7 @@ func TestSignRace(t *testing.T) { } end := time.Now().Add(500 * time.Millisecond) for time.Now().Before(end) { - if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrNeedPasswordOrUnlock { + if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrLocked { return } else if err != nil { t.Errorf("Sign error: %v", err) @@ -212,6 +215,145 @@ func TestSignRace(t *testing.T) { t.Errorf("Account did not lock within the timeout") } +// Tests that the wallet notifier loop starts and stops correctly based on the +// addition and removal of wallet event subscriptions. +func TestWalletNotifierLifecycle(t *testing.T) { + // Create a temporary kesytore to test with + dir, ks := tmpKeyStore(t, false) + defer os.RemoveAll(dir) + + // Ensure that the notification updater is not running yet + time.Sleep(250 * time.Millisecond) + ks.mu.RLock() + updating := ks.updating + ks.mu.RUnlock() + + if updating { + t.Errorf("wallet notifier running without subscribers") + } + // Subscribe to the wallet feed and ensure the updater boots up + updates := make(chan accounts.WalletEvent) + + subs := make([]event.Subscription, 2) + for i := 0; i < len(subs); i++ { + // Create a new subscription + subs[i] = ks.Subscribe(updates) + + // Ensure the notifier comes online + time.Sleep(250 * time.Millisecond) + ks.mu.RLock() + updating = ks.updating + ks.mu.RUnlock() + + if !updating { + t.Errorf("sub %d: wallet notifier not running after subscription", i) + } + } + // Unsubscribe and ensure the updater terminates eventually + for i := 0; i < len(subs); i++ { + // Close an existing subscription + subs[i].Unsubscribe() + + // Ensure the notifier shuts down at and only at the last close + for k := 0; k < int(walletRefreshCycle/(250*time.Millisecond))+2; k++ { + ks.mu.RLock() + updating = ks.updating + ks.mu.RUnlock() + + if i < len(subs)-1 && !updating { + t.Fatalf("sub %d: event notifier stopped prematurely", i) + } + if i == len(subs)-1 && !updating { + return + } + time.Sleep(250 * time.Millisecond) + } + } + t.Errorf("wallet notifier didn't terminate after unsubscribe") +} + +// Tests that wallet notifications and correctly fired when accounts are added +// or deleted from the keystore. +func TestWalletNotifications(t *testing.T) { + // Create a temporary kesytore to test with + dir, ks := tmpKeyStore(t, false) + defer os.RemoveAll(dir) + + // Subscribe to the wallet feed + updates := make(chan accounts.WalletEvent, 1) + sub := ks.Subscribe(updates) + defer sub.Unsubscribe() + + // Randomly add and remove account and make sure events and wallets are in sync + live := make(map[common.Address]accounts.Account) + for i := 0; i < 1024; i++ { + // Execute a creation or deletion and ensure event arrival + if create := len(live) == 0 || rand.Int()%4 > 0; create { + // Add a new account and ensure wallet notifications arrives + account, err := ks.NewAccount("") + if err != nil { + t.Fatalf("failed to create test account: %v", err) + } + select { + case event := <-updates: + if !event.Arrive { + t.Errorf("departure event on account creation") + } + if event.Wallet.Accounts()[0] != account { + t.Errorf("account mismatch on created wallet: have %v, want %v", event.Wallet.Accounts()[0], account) + } + default: + t.Errorf("wallet arrival event not fired on account creation") + } + live[account.Address] = account + } else { + // Select a random account to delete (crude, but works) + var account accounts.Account + for _, a := range live { + account = a + break + } + // Remove an account and ensure wallet notifiaction arrives + if err := ks.Delete(account, ""); err != nil { + t.Fatalf("failed to delete test account: %v", err) + } + select { + case event := <-updates: + if event.Arrive { + t.Errorf("arrival event on account deletion") + } + if event.Wallet.Accounts()[0] != account { + t.Errorf("account mismatch on deleted wallet: have %v, want %v", event.Wallet.Accounts()[0], account) + } + default: + t.Errorf("wallet departure event not fired on account creation") + } + delete(live, account.Address) + } + // Retrieve the list of wallets and ensure it matches with our required live set + liveList := make([]accounts.Account, 0, len(live)) + for _, account := range live { + liveList = append(liveList, account) + } + sort.Sort(accountsByURL(liveList)) + + wallets := ks.Wallets() + if len(liveList) != len(wallets) { + t.Errorf("wallet list doesn't match required accounts: have %v, want %v", wallets, liveList) + } else { + for j, wallet := range wallets { + if accs := wallet.Accounts(); len(accs) != 1 { + t.Errorf("wallet %d: contains invalid number of accounts: have %d, want 1", j, len(accs)) + } else if accs[0] != liveList[j] { + t.Errorf("wallet %d: account mismatch: have %v, want %v", j, accs[0], liveList[j]) + } + } + } + // Sleep a bit to avoid same-timestamp keyfiles + time.Sleep(10 * time.Millisecond) + } +} + func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) { d, err := ioutil.TempDir("", "eth-keystore-test") if err != nil { diff --git a/accounts/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go new file mode 100644 index 000000000..d92926478 --- /dev/null +++ b/accounts/keystore/keystore_wallet.go @@ -0,0 +1,133 @@ +// 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 keystore + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/core/types" +) + +// keystoreWallet implements the accounts.Wallet interface for the original +// keystore. +type keystoreWallet struct { + account accounts.Account // Single account contained in this wallet + keystore *KeyStore // Keystore where the account originates from +} + +// Type implements accounts.Wallet, returning the textual type of the wallet. +func (w *keystoreWallet) Type() string { + return "secret-storage" +} + +// URL implements accounts.Wallet, returning the URL of the account within. +func (w *keystoreWallet) URL() string { + return w.account.URL +} + +// Status implements accounts.Wallet, always returning "open", since there is no +// concept of open/close for plain keystore accounts. +func (w *keystoreWallet) Status() string { + return "Open" +} + +// Open implements accounts.Wallet, but is a noop for plain wallets since there +// is no connection or decryption step necessary to access the list of accounts. +func (w *keystoreWallet) Open(passphrase string) error { return nil } + +// Close implements accounts.Wallet, but is a noop for plain wallets since is no +// meaningful open operation. +func (w *keystoreWallet) Close() error { return nil } + +// Accounts implements accounts.Wallet, returning an account list consisting of +// a single account that the plain kestore wallet contains. +func (w *keystoreWallet) Accounts() []accounts.Account { + return []accounts.Account{w.account} +} + +// Contains implements accounts.Wallet, returning whether a particular account is +// or is not wrapped by this wallet instance. +func (w *keystoreWallet) Contains(account accounts.Account) bool { + return account.Address == w.account.Address && (account.URL == "" || account.URL == w.account.URL) +} + +// 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) { + return accounts.Account{}, accounts.ErrNotSupported +} + +// 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 +// able to sign via our shared keystore backend). +func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) { + // Make sure the requested account is contained within + if account.Address != w.account.Address { + return nil, accounts.ErrUnknownAccount + } + if account.URL != "" && account.URL != w.account.URL { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignHash(account, hash) +} + +// SignTx implements accounts.Wallet, attempting to sign the given transaction +// 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 able to sign via our shared keystore backend). +func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // Make sure the requested account is contained within + if account.Address != w.account.Address { + return nil, accounts.ErrUnknownAccount + } + if account.URL != "" && account.URL != w.account.URL { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignTx(account, tx, chainID) +} + +// SignHashWithPassphrase implements accounts.Wallet, attempting to sign the +// given hash with the given account using passphrase as extra authentication. +func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { + // Make sure the requested account is contained within + if account.Address != w.account.Address { + return nil, accounts.ErrUnknownAccount + } + if account.URL != "" && account.URL != w.account.URL { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignHashWithPassphrase(account, passphrase, hash) +} + +// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given +// transaction with the given account using passphrase as extra authentication. +func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // Make sure the requested account is contained within + if account.Address != w.account.Address { + return nil, accounts.ErrUnknownAccount + } + if account.URL != "" && account.URL != w.account.URL { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignTxWithPassphrase(account, passphrase, tx, chainID) +} diff --git a/accounts/keystore/watch.go b/accounts/keystore/watch.go index 04a87b12e..0b4401255 100644 --- a/accounts/keystore/watch.go +++ b/accounts/keystore/watch.go @@ -27,14 +27,14 @@ import ( ) type watcher struct { - ac *addressCache + ac *accountCache starting bool running bool ev chan notify.EventInfo quit chan struct{} } -func newWatcher(ac *addressCache) *watcher { +func newWatcher(ac *accountCache) *watcher { return &watcher{ ac: ac, ev: make(chan notify.EventInfo, 10), diff --git a/accounts/keystore/watch_fallback.go b/accounts/keystore/watch_fallback.go index 6412f3b33..7c5e9cb2e 100644 --- a/accounts/keystore/watch_fallback.go +++ b/accounts/keystore/watch_fallback.go @@ -23,6 +23,6 @@ package keystore type watcher struct{ running bool } -func newWatcher(*addressCache) *watcher { return new(watcher) } +func newWatcher(*accountCache) *watcher { return new(watcher) } func (*watcher) start() {} func (*watcher) close() {} diff --git a/accounts/manager.go b/accounts/manager.go new file mode 100644 index 000000000..0822500eb --- /dev/null +++ b/accounts/manager.go @@ -0,0 +1,194 @@ +// 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" + "sort" + "sync" + + "github.com/ethereum/go-ethereum/event" +) + +// Manager is an overarching account manager that can communicate with various +// backends for signing transactions. +type Manager struct { + backends map[reflect.Type][]Backend // Index of backends currently registered + updaters []event.Subscription // Wallet update subscriptions for all backends + updates chan WalletEvent // Subscription sink for backend wallet changes + wallets []Wallet // Cache of all wallets from all registered backends + + feed event.Feed // Wallet feed notifying of arrivals/departures + + quit chan chan error + lock sync.RWMutex +} + +// NewManager creates a generic account manager to sign transaction via various +// supported backends. +func NewManager(backends ...Backend) *Manager { + // Subscribe to wallet notifications from all backends + updates := make(chan WalletEvent, 4*len(backends)) + + subs := make([]event.Subscription, len(backends)) + for i, backend := range backends { + subs[i] = backend.Subscribe(updates) + } + // Retrieve the initial list of wallets from the backends and sort by URL + var wallets []Wallet + for _, backend := range backends { + wallets = merge(wallets, backend.Wallets()...) + } + // Assemble the account manager and return + am := &Manager{ + backends: make(map[reflect.Type][]Backend), + updaters: subs, + updates: updates, + wallets: wallets, + quit: make(chan chan error), + } + for _, backend := range backends { + kind := reflect.TypeOf(backend) + am.backends[kind] = append(am.backends[kind], backend) + } + go am.update() + + return am +} + +// Close terminates the account manager's internal notification processes. +func (am *Manager) Close() error { + errc := make(chan error) + am.quit <- errc + return <-errc +} + +// update is the wallet event loop listening for notifications from the backends +// and updating the cache of wallets. +func (am *Manager) update() { + // Close all subscriptions when the manager terminates + defer func() { + am.lock.Lock() + for _, sub := range am.updaters { + sub.Unsubscribe() + } + am.updaters = nil + am.lock.Unlock() + }() + + // Loop until termination + for { + select { + case event := <-am.updates: + // Wallet event arrived, update local cache + am.lock.Lock() + if event.Arrive { + am.wallets = merge(am.wallets, event.Wallet) + } else { + am.wallets = drop(am.wallets, event.Wallet) + } + am.lock.Unlock() + + // Notify any listeners of the event + am.feed.Send(event) + + case errc := <-am.quit: + // Manager terminating, return + errc <- nil + return + } + } +} + +// Backends retrieves the backend(s) with the given type from the account manager. +func (am *Manager) Backends(kind reflect.Type) []Backend { + return am.backends[kind] +} + +// Wallets returns all signer accounts registered under this account manager. +func (am *Manager) Wallets() []Wallet { + am.lock.RLock() + defer am.lock.RUnlock() + + cpy := make([]Wallet, len(am.wallets)) + copy(cpy, am.wallets) + return cpy +} + +// Wallet retrieves the wallet associated with a particular URL. +func (am *Manager) Wallet(url string) (Wallet, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + for _, wallet := range am.Wallets() { + if wallet.URL() == url { + return wallet, nil + } + } + return nil, ErrUnknownWallet +} + +// Find attempts to locate the wallet corresponding to a specific account. Since +// accounts can be dynamically added to and removed from wallets, this method has +// a linear runtime in the number of wallets. +func (am *Manager) Find(account Account) (Wallet, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + for _, wallet := range am.wallets { + if wallet.Contains(account) { + return wallet, nil + } + } + return nil, ErrUnknownAccount +} + +// Subscribe creates an async subscription to receive notifications when the +// manager detects the arrival or departure of a wallet from any of its backends. +func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription { + return am.feed.Subscribe(sink) +} + +// merge is a sorted analogue of append for wallets, where the ordering of the +// origin list is preserved by inserting new wallets at the correct position. +// +// The original slice is assumed to be already sorted by URL. +func merge(slice []Wallet, wallets ...Wallet) []Wallet { + for _, wallet := range wallets { + n := sort.Search(len(slice), func(i int) bool { return slice[i].URL() >= wallet.URL() }) + if n == len(slice) { + slice = append(slice, wallet) + continue + } + slice = append(slice[:n], append([]Wallet{wallet}, slice[n:]...)...) + } + return slice +} + +// drop is the couterpart of merge, which looks up wallets from within the sorted +// cache and removes the ones specified. +func drop(slice []Wallet, wallets ...Wallet) []Wallet { + for _, wallet := range wallets { + n := sort.Search(len(slice), func(i int) bool { return slice[i].URL() >= wallet.URL() }) + if n == len(slice) { + // Wallet not found, may happen during startup + continue + } + slice = append(slice[:n], slice[n+1:]...) + } + return slice +} diff --git a/accounts/usbwallet/ledger_hub.go b/accounts/usbwallet/ledger_hub.go new file mode 100644 index 000000000..ebd6fddb4 --- /dev/null +++ b/accounts/usbwallet/ledger_hub.go @@ -0,0 +1,205 @@ +// 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/>. + +// This file contains the implementation for interacting with the Ledger hardware +// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo: +// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc + +// +build !ios + +package usbwallet + +import ( + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/event" + "github.com/karalabe/gousb/usb" +) + +// ledgerDeviceIDs are the known device IDs that Ledger wallets use. +var ledgerDeviceIDs = []deviceID{ + {Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue + {Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S +} + +// Maximum time between wallet refreshes (if USB hotplug notifications don't work). +const ledgerRefreshCycle = time.Second + +// Minimum time between wallet refreshes to avoid USB trashing. +const ledgerRefreshThrottling = 500 * time.Millisecond + +// LedgerHub is a accounts.Backend that can find and handle Ledger hardware wallets. +type LedgerHub struct { + ctx *usb.Context // Context interfacing with a libusb instance + + refreshed time.Time // Time instance when the list of wallets was last refreshed + wallets []accounts.Wallet // List of Ledger devices currently tracking + updateFeed event.Feed // Event feed to notify wallet additions/removals + updateScope event.SubscriptionScope // Subscription scope tracking current live listeners + updating bool // Whether the event notification loop is running + + quit chan chan error + lock sync.RWMutex +} + +// NewLedgerHub creates a new hardware wallet manager for Ledger devices. +func NewLedgerHub() (*LedgerHub, error) { + // Initialize the USB library to access Ledgers through + ctx, err := usb.NewContext() + if err != nil { + return nil, err + } + // Create the USB hub, start and return it + hub := &LedgerHub{ + ctx: ctx, + quit: make(chan chan error), + } + hub.refreshWallets() + + return hub, nil +} + +// Wallets implements accounts.Backend, returning all the currently tracked USB +// devices that appear to be Ledger hardware wallets. +func (hub *LedgerHub) Wallets() []accounts.Wallet { + // Make sure the list of wallets is up to date + hub.refreshWallets() + + hub.lock.RLock() + defer hub.lock.RUnlock() + + cpy := make([]accounts.Wallet, len(hub.wallets)) + copy(cpy, hub.wallets) + return cpy +} + +// refreshWallets scans the USB devices attached to the machine and updates the +// list of wallets based on the found devices. +func (hub *LedgerHub) refreshWallets() { + // Don't scan the USB like crazy it the user fetches wallets in a loop + hub.lock.RLock() + elapsed := time.Since(hub.refreshed) + hub.lock.RUnlock() + + if elapsed < ledgerRefreshThrottling { + return + } + // Retrieve the current list of Ledger devices + var devIDs []deviceID + var busIDs []uint16 + + hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { + // Gather Ledger devices, don't connect any just yet + for _, id := range ledgerDeviceIDs { + if desc.Vendor == id.Vendor && desc.Product == id.Product { + devIDs = append(devIDs, deviceID{Vendor: desc.Vendor, Product: desc.Product}) + busIDs = append(busIDs, uint16(desc.Bus)<<8+uint16(desc.Address)) + return false + } + } + // Not ledger, ignore and don't connect either + return false + }) + // Transform the current list of wallets into the new one + hub.lock.Lock() + + wallets := make([]accounts.Wallet, 0, len(devIDs)) + events := []accounts.WalletEvent{} + + for i := 0; i < len(devIDs); i++ { + devID, busID := devIDs[i], busIDs[i] + url := fmt.Sprintf("ledger://%03d:%03d", busID>>8, busID&0xff) + + // Drop wallets while they were in front of the next account + for len(hub.wallets) > 0 && hub.wallets[0].URL() < url { + events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false}) + hub.wallets = hub.wallets[1:] + } + // If there are no more wallets or the account is before the next, wrap new wallet + if len(hub.wallets) == 0 || hub.wallets[0].URL() > url { + wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: url} + + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) + wallets = append(wallets, wallet) + continue + } + // If the account is the same as the first wallet, keep it + if hub.wallets[0].URL() == url { + wallets = append(wallets, hub.wallets[0]) + hub.wallets = hub.wallets[1:] + continue + } + } + // Drop any leftover wallets and set the new batch + for _, wallet := range hub.wallets { + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false}) + } + hub.refreshed = time.Now() + hub.wallets = wallets + hub.lock.Unlock() + + // Fire all wallet events and return + for _, event := range events { + hub.updateFeed.Send(event) + } +} + +// Subscribe implements accounts.Backend, creating an async subscription to +// receive notifications on the addition or removal of Ledger wallets. +func (hub *LedgerHub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { + // We need the mutex to reliably start/stop the update loop + hub.lock.Lock() + defer hub.lock.Unlock() + + // Subscribe the caller and track the subscriber count + sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink)) + + // Subscribers require an active notification loop, start it + if !hub.updating { + hub.updating = true + go hub.updater() + } + return sub +} + +// updater is responsible for maintaining an up-to-date list of wallets stored in +// the keystore, and for firing wallet addition/removal events. It listens for +// account change events from the underlying account cache, and also periodically +// forces a manual refresh (only triggers for systems where the filesystem notifier +// is not running). +func (hub *LedgerHub) updater() { + for { + // Wait for a USB hotplug event (not supported yet) or a refresh timeout + select { + //case <-hub.changes: // reenable on hutplug implementation + case <-time.After(ledgerRefreshCycle): + } + // Run the wallet refresher + hub.refreshWallets() + + // If all our subscribers left, stop the updater + hub.lock.Lock() + if hub.updateScope.Count() == 0 { + hub.updating = false + hub.lock.Unlock() + return + } + hub.lock.Unlock() + } +} diff --git a/accounts/usbwallet/ledger.go b/accounts/usbwallet/ledger_wallet.go index 9917d0d70..35e671c46 100644 --- a/accounts/usbwallet/ledger.go +++ b/accounts/usbwallet/ledger_wallet.go @@ -43,14 +43,8 @@ import ( "github.com/karalabe/gousb/usb" ) -// ledgerDeviceIDs are the known device IDs that Ledger wallets use. -var ledgerDeviceIDs = []deviceID{ - {Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue - {Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S -} - -// ledgerDerivationPath is the key derivation parameters used by the wallet. -var ledgerDerivationPath = [4]uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} +// ledgerDerivationPath is the base derivation parameters used by the wallet. +var ledgerDerivationPath = []uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} // ledgerOpcode is an enumeration encoding the supported Ledger opcodes. type ledgerOpcode byte @@ -78,274 +72,297 @@ const ( // ledgerWallet represents a live USB Ledger hardware wallet. type ledgerWallet struct { - device *usb.Device // USB device advertising itself as a Ledger wallet - input usb.Endpoint // Input endpoint to send data to this device - output usb.Endpoint // Output endpoint to receive data from this device - - address common.Address // Current address of the wallet (may be zero if Ethereum app offline) - url string // Textual URL uniquely identifying this wallet - version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline) -} + context *usb.Context // USB context to interface libusb through + hardwareID deviceID // USB identifiers to identify this device type + locationID uint16 // USB bus and address to identify this device instance + url string // Textual URL uniquely identifying this wallet -// LedgerHub is a USB hardware wallet interface that can find and handle Ledger -// wallets. -type LedgerHub struct { - ctx *usb.Context // Context interfacing with a libusb instance + device *usb.Device // USB device advertising itself as a Ledger wallet + input usb.Endpoint // Input endpoint to send data to this device + output usb.Endpoint // Output endpoint to receive data from this device + failure error // Any failure that would make the device unusable - wallets map[uint16]*ledgerWallet // Apparent Ledger wallets (some may be inactive) - accounts []accounts.Account // List of active Ledger accounts - index map[common.Address]uint16 // Set of addresses with active wallets + 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 quit chan chan error lock sync.RWMutex } -// NewLedgerHub creates a new hardware wallet manager for Ledger devices. -func NewLedgerHub() (*LedgerHub, error) { - // Initialize the USB library to access Ledgers through - ctx, err := usb.NewContext() - if err != nil { - return nil, err - } - // Create the USB hub, start and return it - hub := &LedgerHub{ - ctx: ctx, - wallets: make(map[uint16]*ledgerWallet), - index: make(map[common.Address]uint16), - quit: make(chan chan error), - } - go hub.watch() - return hub, nil +// Type implements accounts.Wallet, returning the textual type of the wallet. +func (w *ledgerWallet) Type() string { + return "ledger" } -// Accounts retrieves the live of accounts currently known by the Ledger hub. -func (hub *LedgerHub) Accounts() []accounts.Account { - hub.lock.RLock() - defer hub.lock.RUnlock() - - cpy := make([]accounts.Account, len(hub.accounts)) - copy(cpy, hub.accounts) - return cpy +// URL implements accounts.Wallet, returning the URL of the Ledger device. +func (w *ledgerWallet) URL() string { + return w.url } -// HasAddress reports whether an account with the given address is present. -func (hub *LedgerHub) HasAddress(addr common.Address) bool { - hub.lock.RLock() - defer hub.lock.RUnlock() +// Status implements accounts.Wallet, always whether the Ledger is opened, closed +// or whether the Ethereum app was not started on it. +func (w *ledgerWallet) Status() string { + w.lock.RLock() + defer w.lock.RUnlock() - _, known := hub.index[addr] - return known + if w.failure != nil { + return fmt.Sprintf("Failed: %v", w.failure) + } + if w.device == nil { + return "Closed" + } + if w.version == [3]byte{0, 0, 0} { + return "Ethereum app not started" + } + return fmt.Sprintf("Ethereum app v%d.%d.%d", w.version[0], w.version[1], w.version[2]) } -// SignHash is not supported for Ledger wallets, so this method will always -// return an error. -func (hub *LedgerHub) SignHash(acc accounts.Account, hash []byte) ([]byte, error) { - return nil, accounts.ErrNotSupported -} +// 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. +func (w *ledgerWallet) Open(passphrase string) error { + w.lock.Lock() + defer w.lock.Unlock() -// SignTx sends the transaction over to the Ledger wallet to request a confirmation -// from the user. It returns either the signed transaction or a failure if the user -// denied the transaction. -// -// Note, if the version of the Ethereum application running on the Ledger wallet is -// too old to sign EIP-155 transactions, but such is requested nonetheless, an error -// will be returned opposed to silently signing in Homestead mode. -func (hub *LedgerHub) SignTx(acc accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - hub.lock.RLock() - defer hub.lock.RUnlock() - - // If the account contains the device URL, flatten it to make sure - var id uint16 - if acc.URL != "" { - if parts := strings.Split(acc.URL, "."); len(parts) == 2 { - bus, busErr := strconv.Atoi(parts[0]) - addr, addrErr := strconv.Atoi(parts[1]) - - if busErr == nil && addrErr == nil { - id = uint16(bus)<<8 + uint16(addr) - } - } + // If the wallet was already opened, don't try to open again + if w.device != nil { + return accounts.ErrWalletAlreadyOpen } - // If the id is still zero, URL is either missing or bad, resolve - if id == 0 { - var ok bool - if id, ok = hub.index[acc.Address]; !ok { - return nil, accounts.ErrUnknownAccount - } + // Otherwise iterate over all USB devices and find this again (no way to directly do this) + // Iterate over all attached devices and fetch those seemingly Ledger + devices, err := w.context.ListDevices(func(desc *usb.Descriptor) bool { + // Only open this single specific device + return desc.Vendor == w.hardwareID.Vendor && desc.Product == w.hardwareID.Product && + uint16(desc.Bus)<<8+uint16(desc.Address) == w.locationID + }) + if err != nil { + return err } - // Retrieve the wallet associated with the URL - wallet, ok := hub.wallets[id] - if !ok { - return nil, accounts.ErrUnknownAccount + // Device opened, attach to the input and output endpoints + device := devices[0] + + var invalid string + switch { + case len(device.Descriptor.Configs) == 0: + invalid = "no endpoint config available" + case len(device.Descriptor.Configs[0].Interfaces) == 0: + invalid = "no endpoint interface available" + case len(device.Descriptor.Configs[0].Interfaces[0].Setups) == 0: + invalid = "no endpoint setup available" + case len(device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints) < 2: + invalid = "not enough IO endpoints available" } - // Ensure the wallet is capable of signing the given transaction - if chainID != nil && wallet.version[0] <= 1 && wallet.version[1] <= 0 && wallet.version[2] <= 2 { - return nil, fmt.Errorf("Ledger v%d.%d.%d doesn't support signing this transaction, please update to v1.0.3 at least", - wallet.version[0], wallet.version[1], wallet.version[2]) + if invalid != "" { + device.Close() + return fmt.Errorf("ledger wallet [%s] invalid: %s", w.url, invalid) } - return wallet.sign(tx, chainID) + // Open the input and output endpoints to the device + input, err := device.OpenEndpoint( + device.Descriptor.Configs[0].Config, + device.Descriptor.Configs[0].Interfaces[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[1].Address, + ) + if err != nil { + device.Close() + return fmt.Errorf("ledger wallet [%s] input open failed: %v", w.url, err) + } + output, err := device.OpenEndpoint( + device.Descriptor.Configs[0].Config, + device.Descriptor.Configs[0].Interfaces[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[0].Address, + ) + if err != nil { + device.Close() + return fmt.Errorf("ledger wallet [%s] output open failed: %v", w.url, err) + } + // 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.quit = make(chan chan error) + defer func() { + go w.heartbeat() + }() + + if _, err := w.deriveAddress(ledgerDerivationPath); err != nil { + // Ethereum app is not running, nothing more to do, return + return nil + } + // Try to resolve the Ethereum app's version, will fail prior to v1.0.2 + if w.resolveVersion() != nil { + w.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1 + } + return nil } -// SignHashWithPassphrase is not supported for Ledger wallets, so this method -// will always return an error. -func (hub *LedgerHub) SignHashWithPassphrase(acc accounts.Account, passphrase string, hash []byte) ([]byte, error) { - return nil, accounts.ErrNotSupported +// heartbeat is a health check loop for the Ledger wallets to periodically verify +// whether they are still present or if they malfunctioned. It is needed because: +// - libusb on Windows doesn't support hotplug, so we can't detect USB unplugs +// - communication timeout on the Ledger requires a device power cycle to fix +func (w *ledgerWallet) heartbeat() { + // Execute heartbeat checks until termination or error + var ( + errc chan error + fail error + ) + for errc == nil && fail == nil { + // Wait until termination is requested or the heartbeat cycle arrives + select { + case errc = <-w.quit: + // Termination requested + continue + case <-time.After(time.Second): + // Heartbeat time + } + // Execute a tiny data exchange to see responsiveness + w.lock.Lock() + if err := w.resolveVersion(); err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { + w.failure = err + fail = err + } + w.lock.Unlock() + } + // In case of error, wait for termination + if fail != nil { + errc = <-w.quit + } + errc <- fail } -// SignTxWithPassphrase requests the backend to sign the given transaction, with the -// given passphrase as extra authentication information. Since the Ledger does not -// support this feature, it will just silently ignore the passphrase. -func (hub *LedgerHub) SignTxWithPassphrase(acc accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - return hub.SignTx(acc, tx, chainID) +// Close implements accounts.Wallet, closing the USB connection to the Ledger. +func (w *ledgerWallet) Close() error { + // Terminate the health checks + errc := make(chan error) + w.quit <- errc + herr := <-errc // Save for later, we *must* close the USB + + // Terminate the device connection + w.lock.Lock() + defer w.lock.Unlock() + + if err := w.device.Close(); err != nil { + return err + } + w.device, w.input, w.output, w.paths, w.quit = nil, nil, nil, nil, nil + + return herr // If all went well, return any health-check errors } -// Close terminates the usb watching for Ledger wallets and returns when it -// successfully terminated. -func (hub *LedgerHub) Close() error { - // Terminate the USB scanner - errc := make(chan error) - hub.quit <- errc - err := <-errc +// Accounts implements accounts.Wallet, returning the list of accounts pinned to +// the Ledger hardware wallet. +func (w *ledgerWallet) Accounts() []accounts.Account { + w.lock.RLock() + defer w.lock.RUnlock() - // Release the USB interface and return - hub.ctx.Close() - return err + cpy := make([]accounts.Account, len(w.accounts)) + copy(cpy, w.accounts) + return cpy } -// watch starts watching the local machine's USB ports for the connection or -// disconnection of Ledger devices. -func (hub *LedgerHub) watch() { - for { - // Rescan the USB ports for devices newly added or removed - hub.rescan() +// Contains implements accounts.Wallet, returning whether a particular account is +// or is not pinned into this Ledger instance. Although we could attempt to resolve +// unpinned accounts, that would be an non-negligible hardware operation. +func (w *ledgerWallet) Contains(account accounts.Account) bool { + w.lock.RLock() + defer w.lock.RUnlock() - // Sleep for a certain amount of time or until terminated - select { - case errc := <-hub.quit: - errc <- nil - return - case <-time.After(time.Second): - } - } + _, exists := w.paths[account.Address] + return exists } -// rescan searches the USB ports for attached Ledger hardware wallets. -func (hub *LedgerHub) rescan() { - hub.lock.Lock() - defer hub.lock.Unlock() - - // Iterate over all connected Ledger devices and do a heartbeat test - for id, wallet := range hub.wallets { - // If the device doesn't respond (io error on Windows, no device on Linux), drop - if err := wallet.resolveVersion(); err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { - // Wallet disconnected or at least in a useless state - if wallet.address == (common.Address{}) { - glog.V(logger.Info).Infof("ledger wallet [%03d.%03d] disconnected", wallet.device.Bus, wallet.device.Address) - } else { - // A live account disconnected, remove it from the tracked accounts - for i, account := range hub.accounts { - if account.Address == wallet.address && account.URL == wallet.url { - hub.accounts = append(hub.accounts[:i], hub.accounts[i+1:]...) - break - } - } - delete(hub.index, wallet.address) - - glog.V(logger.Info).Infof("ledger wallet [%03d.%03d] v%d.%d.%d disconnected: %s", wallet.device.Bus, wallet.device.Address, - wallet.version[0], wallet.version[1], wallet.version[2], wallet.address.Hex()) - } - delete(hub.wallets, id) - wallet.device.Close() - } +// 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) { + 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} { + return accounts.Account{}, accounts.ErrWalletClosed } - // Iterate over all attached devices and fetch those seemingly Ledger - devices, _ := hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { - // Discard all devices not advertizing as Ledger - ledger := false - for _, id := range ledgerDeviceIDs { - if desc.Vendor == id.Vendor && desc.Product == id.Product { - ledger = true - } + // 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, "'") } - if !ledger { - return false - } - // If we have an already known Ledger, skip opening it - id := uint16(desc.Bus)<<8 + uint16(desc.Address) - if _, known := hub.wallets[id]; known { - return false - } - // New Ledger device, open it for communication - return true - }) - // Start tracking all wallets which newly appeared - var err error - for _, device := range devices { - // Make sure the alleged device has the correct IO endpoints - wallet := &ledgerWallet{ - device: device, - url: fmt.Sprintf("%03d.%03d", device.Bus, device.Address), - } - var invalid string - switch { - case len(device.Descriptor.Configs) == 0: - invalid = "no endpoint config available" - case len(device.Descriptor.Configs[0].Interfaces) == 0: - invalid = "no endpoint interface available" - case len(device.Descriptor.Configs[0].Interfaces[0].Setups) == 0: - invalid = "no endpoint setup available" - case len(device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints) < 2: - invalid = "not enough IO endpoints available" - } - if invalid != "" { - glog.V(logger.Debug).Infof("ledger wallet [%s] deemed invalid: %s", wallet.url, invalid) - device.Close() - continue - } - // Open the input and output endpoints to the device - wallet.input, err = device.OpenEndpoint( - device.Descriptor.Configs[0].Config, - device.Descriptor.Configs[0].Interfaces[0].Number, - device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, - device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[1].Address, - ) + // Handle the non hardened component + val, err := strconv.Atoi(part) if err != nil { - glog.V(logger.Debug).Infof("ledger wallet [%s] input open failed: %v", wallet.url, err) - device.Close() - continue + return accounts.Account{}, fmt.Errorf("path element %d: %v", i, err) } - wallet.output, err = device.OpenEndpoint( - device.Descriptor.Configs[0].Config, - device.Descriptor.Configs[0].Interfaces[0].Number, - device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, - device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[0].Address, - ) - if err != nil { - glog.V(logger.Debug).Infof("ledger wallet [%s] output open failed: %v", wallet.url, err) - device.Close() - continue + lpath[i] += uint32(val) + } + // Try to derive the actual account and update it's URL if succeeful + address, err := w.deriveAddress(lpath) + if err != nil { + return accounts.Account{}, err + } + account := accounts.Account{ + Address: address, + URL: fmt.Sprintf("%s/%s", w.url, path), + } + // If pinning was requested, track the account + if pin { + if _, ok := w.paths[address]; !ok { + w.accounts = append(w.accounts, account) + w.paths[address] = lpath } - // Start tracking the device as a probably Ledger wallet - id := uint16(device.Bus)<<8 + uint16(device.Address) - hub.wallets[id] = wallet + } + return account, nil +} - if wallet.resolveAddress() != nil { - glog.V(logger.Info).Infof("ledger wallet [%s] connected, Ethereum app not started", wallet.url) - } else { - // Try to resolve the Ethereum app's version, will fail prior to v1.0.2 - if wallet.resolveVersion() != nil { - wallet.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1 - } - hub.accounts = append(hub.accounts, accounts.Account{ - Address: wallet.address, - URL: wallet.url, - }) - hub.index[wallet.address] = id - - glog.V(logger.Info).Infof("ledger wallet [%s] v%d.%d.%d connected: %s", wallet.url, - wallet.version[0], wallet.version[1], wallet.version[2], wallet.address.Hex()) - } +// 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) { + return nil, accounts.ErrNotSupported +} + +// SignTx implements accounts.Wallet. It sends the transaction over to the Ledger +// wallet to request a confirmation from the user. It returns either the signed +// transaction or a failure if the user denied the transaction. +// +// Note, if the version of the Ethereum application running on the Ledger wallet is +// too old to sign EIP-155 transactions, but such is requested nonetheless, an error +// will be returned opposed to silently signing in Homestead mode. +func (w *ledgerWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + w.lock.Lock() + defer w.lock.Unlock() + + // Make sure the requested account is contained within + path, ok := w.paths[account.Address] + if !ok { + return nil, accounts.ErrUnknownAccount + } + // Ensure the wallet is capable of signing the given transaction + if chainID != nil && w.version[0] <= 1 && w.version[1] <= 0 && w.version[2] <= 2 { + return nil, fmt.Errorf("Ledger v%d.%d.%d doesn't support signing this transaction, please update to v1.0.3 at least", + w.version[0], w.version[1], w.version[2]) } + return w.sign(path, account.Address, tx, chainID) +} + +// SignHashWithPassphrase implements accounts.Wallet, however signing arbitrary +// data is not supported for Ledger wallets, so this method will always return +// an error. +func (w *ledgerWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { + return nil, accounts.ErrNotSupported +} + +// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given +// transaction with the given account using passphrase as extra authentication. +// Since the Ledger does not support extra passphrases, it is silently ignored. +func (w *ledgerWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + return w.SignTx(account, tx, chainID) } // resolveVersion retrieves the current version of the Ethereum wallet app running @@ -379,8 +396,8 @@ func (wallet *ledgerWallet) resolveVersion() error { return nil } -// resolveAddress retrieves the currently active Ethereum address from a Ledger -// wallet and caches it for future reference. +// deriveAddress retrieves the currently active Ethereum address from a Ledger +// wallet at the specified derivation path. // // The address derivation protocol is defined as follows: // @@ -410,33 +427,34 @@ func (wallet *ledgerWallet) resolveVersion() error { // Ethereum address length | 1 byte // Ethereum address | 40 bytes hex ascii // Chain code if requested | 32 bytes -func (wallet *ledgerWallet) resolveAddress() error { +func (w *ledgerWallet) deriveAddress(derivationPath []uint32) (common.Address, error) { // Flatten the derivation path into the Ledger request - path := make([]byte, 1+4*len(ledgerDerivationPath)) - path[0] = byte(len(ledgerDerivationPath)) - for i, component := range ledgerDerivationPath { + path := make([]byte, 1+4*len(derivationPath)) + path[0] = byte(len(derivationPath)) + for i, component := range derivationPath { binary.BigEndian.PutUint32(path[1+4*i:], component) } // Send the request and wait for the response - reply, err := wallet.exchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) + reply, err := w.exchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) if err != nil { - return err + return common.Address{}, err } // Discard the public key, we don't need that for now if len(reply) < 1 || len(reply) < 1+int(reply[0]) { - return errors.New("reply lacks public key entry") + return common.Address{}, errors.New("reply lacks public key entry") } reply = reply[1+int(reply[0]):] // Extract the Ethereum hex address string if len(reply) < 1 || len(reply) < 1+int(reply[0]) { - return errors.New("reply lacks address entry") + return common.Address{}, errors.New("reply lacks address entry") } hexstr := reply[1 : 1+int(reply[0])] // Decode the hex sting into an Ethereum address and return - hex.Decode(wallet.address[:], hexstr) - return nil + var address common.Address + hex.Decode(address[:], hexstr) + return address, nil } // sign sends the transaction to the Ledger wallet, and waits for the user to @@ -473,15 +491,15 @@ func (wallet *ledgerWallet) resolveAddress() error { // signature V | 1 byte // signature R | 32 bytes // signature S | 32 bytes -func (wallet *ledgerWallet) sign(tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { +func (w *ledgerWallet) sign(derivationPath []uint32, address common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { // We need to modify the timeouts to account for user feedback - defer func(old time.Duration) { wallet.device.ReadTimeout = old }(wallet.device.ReadTimeout) - wallet.device.ReadTimeout = time.Minute + defer func(old time.Duration) { w.device.ReadTimeout = old }(w.device.ReadTimeout) + w.device.ReadTimeout = time.Minute // Flatten the derivation path into the Ledger request - path := make([]byte, 1+4*len(ledgerDerivationPath)) - path[0] = byte(len(ledgerDerivationPath)) - for i, component := range ledgerDerivationPath { + path := make([]byte, 1+4*len(derivationPath)) + path[0] = byte(len(derivationPath)) + for i, component := range derivationPath { binary.BigEndian.PutUint32(path[1+4*i:], component) } // Create the transaction RLP based on whether legacy or EIP155 signing was requeste @@ -512,7 +530,7 @@ func (wallet *ledgerWallet) sign(tx *types.Transaction, chainID *big.Int) (*type chunk = len(payload) } // Send the chunk over, ensuring it's processed correctly - reply, err = wallet.exchange(ledgerOpSignTransaction, op, 0, payload[:chunk]) + reply, err = w.exchange(ledgerOpSignTransaction, op, 0, payload[:chunk]) if err != nil { return nil, err } @@ -543,9 +561,8 @@ func (wallet *ledgerWallet) sign(tx *types.Transaction, chainID *big.Int) (*type if err != nil { return nil, err } - if sender != wallet.address { - glog.V(logger.Error).Infof("Ledger signer mismatch: expected %s, got %s", wallet.address.Hex(), sender.Hex()) - return nil, fmt.Errorf("signer mismatch: expected %s, got %s", wallet.address.Hex(), sender.Hex()) + if sender != address { + return nil, fmt.Errorf("signer mismatch: expected %s, got %s", address.Hex(), sender.Hex()) } return signed, nil } @@ -583,7 +600,7 @@ func (wallet *ledgerWallet) sign(tx *types.Transaction, chainID *big.Int) (*type // APDU P2 | 1 byte // APDU length | 1 byte // Optional APDU data | arbitrary -func (wallet *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { +func (w *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { // Construct the message payload, possibly split into multiple chunks var chunks [][]byte for left := data; len(left) > 0 || len(chunks) == 0; { @@ -613,9 +630,9 @@ func (wallet *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 le // Send over to the device if glog.V(logger.Core) { - glog.Infof("-> %03d.%03d: %x", wallet.device.Bus, wallet.device.Address, msg) + glog.Infof("-> %03d.%03d: %x", w.device.Bus, w.device.Address, msg) } - if _, err := wallet.input.Write(msg); err != nil { + if _, err := w.input.Write(msg); err != nil { return nil, err } } @@ -624,11 +641,11 @@ func (wallet *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 le for { // Read the next chunk from the Ledger wallet chunk := make([]byte, 64) - if _, err := io.ReadFull(wallet.output, chunk); err != nil { + if _, err := io.ReadFull(w.output, chunk); err != nil { return nil, err } if glog.V(logger.Core) { - glog.Infof("<- %03d.%03d: %x", wallet.device.Bus, wallet.device.Address, chunk) + glog.Infof("<- %03d.%03d: %x", w.device.Bus, w.device.Address, chunk) } // Make sure the transport header matches if chunk[0] != 0x01 || chunk[1] != 0x01 || chunk[2] != 0x05 { |