aboutsummaryrefslogtreecommitdiffstats
path: root/accounts
diff options
context:
space:
mode:
authorPéter Szilágyi <peterke@gmail.com>2017-02-13 21:03:16 +0800
committerGitHub <noreply@github.com>2017-02-13 21:03:16 +0800
commitf8f428cc18c5f70814d7b3937128781bac14bffd (patch)
treed93d285d2ec22bd8ed646695c3db116c69fa3329 /accounts
parente23e86921b55cb1ee2fca6b6fb9ed91f5532f9fd (diff)
parente99c788155ddd754c73d2c81b6051dcbd42e6575 (diff)
downloadgo-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar
go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.gz
go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.bz2
go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.lz
go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.xz
go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.zst
go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.zip
Merge pull request #3592 from karalabe/hw-wallets
accounts: initial support for Ledger hardware wallets
Diffstat (limited to 'accounts')
-rw-r--r--accounts/abi/bind/auth.go4
-rw-r--r--accounts/account_manager.go350
-rw-r--r--accounts/accounts.go155
-rw-r--r--accounts/accounts_test.go224
-rw-r--r--accounts/errors.go68
-rw-r--r--accounts/hd.go130
-rw-r--r--accounts/hd_test.go79
-rw-r--r--accounts/keystore/account_cache.go (renamed from accounts/addrcache.go)91
-rw-r--r--accounts/keystore/account_cache_test.go (renamed from accounts/addrcache_test.go)146
-rw-r--r--accounts/keystore/key.go (renamed from accounts/key.go)11
-rw-r--r--accounts/keystore/keystore.go494
-rw-r--r--accounts/keystore/keystore_passphrase.go (renamed from accounts/key_store_passphrase.go)2
-rw-r--r--accounts/keystore/keystore_passphrase_test.go (renamed from accounts/key_store_passphrase_test.go)2
-rw-r--r--accounts/keystore/keystore_plain.go (renamed from accounts/key_store_plain.go)2
-rw-r--r--accounts/keystore/keystore_plain_test.go (renamed from accounts/key_store_test.go)30
-rw-r--r--accounts/keystore/keystore_test.go365
-rw-r--r--accounts/keystore/keystore_wallet.go139
-rw-r--r--accounts/keystore/presale.go (renamed from accounts/presale.go)11
-rw-r--r--accounts/keystore/testdata/dupes/1 (renamed from accounts/testdata/dupes/1)0
-rw-r--r--accounts/keystore/testdata/dupes/2 (renamed from accounts/testdata/dupes/2)0
-rw-r--r--accounts/keystore/testdata/dupes/foo (renamed from accounts/testdata/dupes/foo)0
-rw-r--r--accounts/keystore/testdata/keystore/.hiddenfile (renamed from accounts/testdata/keystore/.hiddenfile)0
-rw-r--r--accounts/keystore/testdata/keystore/README (renamed from accounts/testdata/keystore/README)0
-rw-r--r--accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 (renamed from accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8)0
-rw-r--r--accounts/keystore/testdata/keystore/aaa (renamed from accounts/testdata/keystore/aaa)0
-rw-r--r--accounts/keystore/testdata/keystore/empty (renamed from accounts/testdata/keystore/empty)0
-rw-r--r--accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e (renamed from accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e)0
-rw-r--r--accounts/keystore/testdata/keystore/garbage (renamed from accounts/testdata/keystore/garbage)bin300 -> 300 bytes
-rw-r--r--accounts/keystore/testdata/keystore/no-address (renamed from accounts/testdata/keystore/no-address)0
-rw-r--r--accounts/keystore/testdata/keystore/zero (renamed from accounts/testdata/keystore/zero)0
-rw-r--r--accounts/keystore/testdata/keystore/zzz (renamed from accounts/testdata/keystore/zzz)0
-rw-r--r--accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e (renamed from accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e)0
-rw-r--r--accounts/keystore/testdata/v1_test_vector.json (renamed from accounts/testdata/v1_test_vector.json)0
-rw-r--r--accounts/keystore/testdata/v3_test_vector.json (renamed from accounts/testdata/v3_test_vector.json)0
-rw-r--r--accounts/keystore/testdata/very-light-scrypt.json (renamed from accounts/testdata/very-light-scrypt.json)0
-rw-r--r--accounts/keystore/watch.go (renamed from accounts/watch.go)6
-rw-r--r--accounts/keystore/watch_fallback.go (renamed from accounts/watch_fallback.go)8
-rw-r--r--accounts/manager.go198
-rw-r--r--accounts/url.go79
-rw-r--r--accounts/usbwallet/ledger_hub.go209
-rw-r--r--accounts/usbwallet/ledger_wallet.go945
-rw-r--r--accounts/usbwallet/usbwallet.go29
-rw-r--r--accounts/usbwallet/usbwallet_ios.go38
43 files changed, 3099 insertions, 716 deletions
diff --git a/accounts/abi/bind/auth.go b/accounts/abi/bind/auth.go
index dbb235c14..e6bb0c3b5 100644
--- a/accounts/abi/bind/auth.go
+++ b/accounts/abi/bind/auth.go
@@ -22,7 +22,7 @@ import (
"io"
"io/ioutil"
- "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
@@ -35,7 +35,7 @@ func NewTransactor(keyin io.Reader, passphrase string) (*TransactOpts, error) {
if err != nil {
return nil, err
}
- key, err := accounts.DecryptKey(json, passphrase)
+ key, err := keystore.DecryptKey(json, passphrase)
if err != nil {
return nil, err
}
diff --git a/accounts/account_manager.go b/accounts/account_manager.go
deleted file mode 100644
index 01dd62e25..000000000
--- a/accounts/account_manager.go
+++ /dev/null
@@ -1,350 +0,0 @@
-// Copyright 2015 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 implements encrypted storage of secp256k1 private keys.
-//
-// Keys are stored as encrypted JSON files according to the Web3 Secret Storage specification.
-// See https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition for more information.
-package accounts
-
-import (
- "crypto/ecdsa"
- crand "crypto/rand"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "runtime"
- "sync"
- "time"
-
- "github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/crypto"
-)
-
-var (
- ErrLocked = errors.New("account is locked")
- ErrNoMatch = errors.New("no key for given address or file")
- ErrDecrypt = errors.New("could not decrypt key with given passphrase")
-)
-
-// Account represents a stored key.
-// When used as an argument, it selects a unique key file to act on.
-type Account struct {
- Address common.Address // Ethereum account address derived from the key
-
- // File contains the key file name.
- // When Acccount is used as an argument to select a key, File can be left blank to
- // select just by address or set to the basename or absolute path of a file in the key
- // directory. Accounts returned by Manager will always contain an absolute path.
- File string
-}
-
-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 manages a key storage directory on disk.
-type Manager struct {
- cache *addrCache
- keyStore keyStore
- mu sync.RWMutex
- unlocked map[common.Address]*unlocked
-}
-
-type unlocked struct {
- *Key
- abort chan struct{}
-}
-
-// NewManager creates a manager for the given directory.
-func NewManager(keydir string, scryptN, scryptP int) *Manager {
- keydir, _ = filepath.Abs(keydir)
- am := &Manager{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}}
- am.init(keydir)
- return am
-}
-
-// NewPlaintextManager creates a manager for the given directory.
-// Deprecated: Use NewManager.
-func NewPlaintextManager(keydir string) *Manager {
- keydir, _ = filepath.Abs(keydir)
- am := &Manager{keyStore: &keyStorePlain{keydir}}
- am.init(keydir)
- return am
-}
-
-func (am *Manager) init(keydir string) {
- am.unlocked = make(map[common.Address]*unlocked)
- am.cache = newAddrCache(keydir)
- // TODO: In order for this finalizer to work, there must be no references
- // to am. addrCache doesn't keep a reference but unlocked keys do,
- // so the finalizer will not trigger until all timed unlocks have expired.
- runtime.SetFinalizer(am, func(m *Manager) {
- m.cache.close()
- })
-}
-
-// HasAddress reports whether a key with the given address is present.
-func (am *Manager) HasAddress(addr common.Address) bool {
- return am.cache.hasAddress(addr)
-}
-
-// Accounts returns all key files present in the directory.
-func (am *Manager) Accounts() []Account {
- return am.cache.accounts()
-}
-
-// Delete deletes the key matched by account if the passphrase is correct.
-// If the account contains no filename, the address must match a unique key.
-func (am *Manager) Delete(a Account, passphrase string) error {
- // Decrypting the key isn't really necessary, but we do
- // it anyway to check the password and zero out the key
- // immediately afterwards.
- a, key, err := am.getDecryptedKey(a, passphrase)
- if key != nil {
- zeroKey(key.PrivateKey)
- }
- if err != nil {
- return err
- }
- // The order is crucial here. The key is dropped from the
- // cache after the file is gone so that a reload happening in
- // between won't insert it into the cache again.
- err = os.Remove(a.File)
- if err == nil {
- am.cache.delete(a)
- }
- return err
-}
-
-// Sign calculates a ECDSA signature for the given hash. The produced signature
-// is in the [R || S || V] format where V is 0 or 1.
-func (am *Manager) Sign(addr common.Address, hash []byte) ([]byte, error) {
- am.mu.RLock()
- defer am.mu.RUnlock()
-
- unlockedKey, found := am.unlocked[addr]
- if !found {
- return nil, ErrLocked
- }
- return crypto.Sign(hash, unlockedKey.PrivateKey)
-}
-
-// SignWithPassphrase signs hash if the private key matching the given address
-// can be decrypted with the given passphrase. The produced signature is in the
-// [R || S || V] format where V is 0 or 1.
-func (am *Manager) SignWithPassphrase(a Account, passphrase string, hash []byte) (signature []byte, err error) {
- _, key, err := am.getDecryptedKey(a, passphrase)
- if err != nil {
- return nil, err
- }
- defer zeroKey(key.PrivateKey)
- return crypto.Sign(hash, key.PrivateKey)
-}
-
-// Unlock unlocks the given account indefinitely.
-func (am *Manager) Unlock(a Account, passphrase string) error {
- return am.TimedUnlock(a, passphrase, 0)
-}
-
-// Lock removes the private key with the given address from memory.
-func (am *Manager) Lock(addr common.Address) error {
- am.mu.Lock()
- if unl, found := am.unlocked[addr]; found {
- am.mu.Unlock()
- am.expire(addr, unl, time.Duration(0)*time.Nanosecond)
- } else {
- am.mu.Unlock()
- }
- return nil
-}
-
-// TimedUnlock unlocks the given account with the passphrase. The account
-// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account
-// until the program exits. The account must match a unique key file.
-//
-// If the account address is already unlocked for a duration, TimedUnlock extends or
-// shortens the active unlock timeout. If the address was previously unlocked
-// indefinitely the timeout is not altered.
-func (am *Manager) TimedUnlock(a Account, passphrase string, timeout time.Duration) error {
- a, key, err := am.getDecryptedKey(a, passphrase)
- if err != nil {
- return err
- }
-
- am.mu.Lock()
- defer am.mu.Unlock()
- u, found := am.unlocked[a.Address]
- if found {
- if u.abort == nil {
- // The address was unlocked indefinitely, so unlocking
- // it with a timeout would be confusing.
- zeroKey(key.PrivateKey)
- return nil
- } else {
- // Terminate the expire goroutine and replace it below.
- close(u.abort)
- }
- }
- if timeout > 0 {
- u = &unlocked{Key: key, abort: make(chan struct{})}
- go am.expire(a.Address, u, timeout)
- } else {
- u = &unlocked{Key: key}
- }
- am.unlocked[a.Address] = u
- return nil
-}
-
-// Find resolves the given account into a unique entry in the keystore.
-func (am *Manager) Find(a Account) (Account, error) {
- am.cache.maybeReload()
- am.cache.mu.Lock()
- a, err := am.cache.find(a)
- am.cache.mu.Unlock()
- return a, err
-}
-
-func (am *Manager) getDecryptedKey(a Account, auth string) (Account, *Key, error) {
- a, err := am.Find(a)
- if err != nil {
- return a, nil, err
- }
- key, err := am.keyStore.GetKey(a.Address, a.File, auth)
- return a, key, err
-}
-
-func (am *Manager) expire(addr common.Address, u *unlocked, timeout time.Duration) {
- t := time.NewTimer(timeout)
- defer t.Stop()
- select {
- case <-u.abort:
- // just quit
- case <-t.C:
- am.mu.Lock()
- // only drop if it's still the same key instance that dropLater
- // was launched with. we can check that using pointer equality
- // because the map stores a new pointer every time the key is
- // unlocked.
- if am.unlocked[addr] == u {
- zeroKey(u.PrivateKey)
- delete(am.unlocked, addr)
- }
- am.mu.Unlock()
- }
-}
-
-// NewAccount generates a new key and stores it into the key directory,
-// encrypting it with the passphrase.
-func (am *Manager) NewAccount(passphrase string) (Account, error) {
- _, account, err := storeNewKey(am.keyStore, crand.Reader, passphrase)
- if err != nil {
- return Account{}, err
- }
- // Add the account to the cache immediately rather
- // than waiting for file system notifications to pick it up.
- am.cache.add(account)
- return account, nil
-}
-
-// AccountByIndex returns the ith account.
-func (am *Manager) AccountByIndex(i int) (Account, error) {
- accounts := am.Accounts()
- if i < 0 || i >= len(accounts) {
- return Account{}, fmt.Errorf("account index %d out of range [0, %d]", i, len(accounts)-1)
- }
- return accounts[i], nil
-}
-
-// Export exports as a JSON key, encrypted with newPassphrase.
-func (am *Manager) Export(a Account, passphrase, newPassphrase string) (keyJSON []byte, err error) {
- _, key, err := am.getDecryptedKey(a, passphrase)
- if err != nil {
- return nil, err
- }
- var N, P int
- if store, ok := am.keyStore.(*keyStorePassphrase); ok {
- N, P = store.scryptN, store.scryptP
- } else {
- N, P = StandardScryptN, StandardScryptP
- }
- return EncryptKey(key, newPassphrase, N, P)
-}
-
-// Import stores the given encrypted JSON key into the key directory.
-func (am *Manager) Import(keyJSON []byte, passphrase, newPassphrase string) (Account, error) {
- key, err := DecryptKey(keyJSON, passphrase)
- if key != nil && key.PrivateKey != nil {
- defer zeroKey(key.PrivateKey)
- }
- if err != nil {
- return Account{}, err
- }
- return am.importKey(key, newPassphrase)
-}
-
-// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase.
-func (am *Manager) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (Account, error) {
- key := newKeyFromECDSA(priv)
- if am.cache.hasAddress(key.Address) {
- return Account{}, fmt.Errorf("account already exists")
- }
-
- return am.importKey(key, passphrase)
-}
-
-func (am *Manager) importKey(key *Key, passphrase string) (Account, error) {
- a := Account{Address: key.Address, File: am.keyStore.JoinPath(keyFileName(key.Address))}
- if err := am.keyStore.StoreKey(a.File, key, passphrase); err != nil {
- return Account{}, err
- }
- am.cache.add(a)
- return a, nil
-}
-
-// Update changes the passphrase of an existing account.
-func (am *Manager) Update(a Account, passphrase, newPassphrase string) error {
- a, key, err := am.getDecryptedKey(a, passphrase)
- if err != nil {
- return err
- }
- return am.keyStore.StoreKey(a.File, 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 (am *Manager) ImportPreSaleKey(keyJSON []byte, passphrase string) (Account, error) {
- a, _, err := importPreSaleKey(am.keyStore, keyJSON, passphrase)
- if err != nil {
- return a, err
- }
- am.cache.add(a)
- return a, nil
-}
-
-// zeroKey zeroes a private key in memory.
-func zeroKey(k *ecdsa.PrivateKey) {
- b := k.D.Bits()
- for i := range b {
- b[i] = 0
- }
-}
diff --git a/accounts/accounts.go b/accounts/accounts.go
new file mode 100644
index 000000000..640de5220
--- /dev/null
+++ b/accounts/accounts.go
@@ -0,0 +1,155 @@
+// 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 implements high level Ethereum account management.
+package accounts
+
+import (
+ "math/big"
+
+ ethereum "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/event"
+)
+
+// Account represents an Ethereum account located at a specific location defined
+// by the optional URL field.
+type Account struct {
+ Address common.Address `json:"address"` // Ethereum account address derived from the key
+ URL URL `json:"url"` // Optional resource locator within a backend
+}
+
+// Wallet represents a software or hardware wallet that might contain one or more
+// accounts (derived from the same seed).
+type Wallet interface {
+ // 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() URL
+
+ // 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 DerivationPath, pin bool) (Account, error)
+
+ // SelfDerive sets a base account derivation path from which the wallet attempts
+ // to discover non zero accounts and automatically add them to list of tracked
+ // accounts.
+ //
+ // Note, self derivaton will increment the last component of the specified path
+ // opposed to decending into a child path to allow discovering accounts starting
+ // from non zero components.
+ //
+ // You can disable automatic account discovery by calling SelfDerive with a nil
+ // chain state reader.
+ SelfDerive(base DerivationPath, chain ethereum.ChainStateReader)
+
+ // SignHash requests the wallet to sign the given hash.
+ //
+ // 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)
+}
+
+// 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
+}
+
+// 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/accounts_test.go b/accounts/accounts_test.go
deleted file mode 100644
index b3ab87d50..000000000
--- a/accounts/accounts_test.go
+++ /dev/null
@@ -1,224 +0,0 @@
-// Copyright 2015 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 (
- "io/ioutil"
- "os"
- "runtime"
- "strings"
- "testing"
- "time"
-
- "github.com/ethereum/go-ethereum/common"
-)
-
-var testSigData = make([]byte, 32)
-
-func TestManager(t *testing.T) {
- dir, am := tmpManager(t, true)
- defer os.RemoveAll(dir)
-
- a, err := am.NewAccount("foo")
- if err != nil {
- t.Fatal(err)
- }
- if !strings.HasPrefix(a.File, dir) {
- t.Errorf("account file %s doesn't have dir prefix", a.File)
- }
- stat, err := os.Stat(a.File)
- if err != nil {
- t.Fatalf("account file %s doesn't exist (%v)", a.File, err)
- }
- if runtime.GOOS != "windows" && stat.Mode() != 0600 {
- t.Fatalf("account file has wrong mode: got %o, want %o", stat.Mode(), 0600)
- }
- if !am.HasAddress(a.Address) {
- t.Errorf("HasAccount(%x) should've returned true", a.Address)
- }
- if err := am.Update(a, "foo", "bar"); err != nil {
- t.Errorf("Update error: %v", err)
- }
- if err := am.Delete(a, "bar"); err != nil {
- t.Errorf("Delete error: %v", err)
- }
- if common.FileExist(a.File) {
- t.Errorf("account file %s should be gone after Delete", a.File)
- }
- if am.HasAddress(a.Address) {
- t.Errorf("HasAccount(%x) should've returned true after Delete", a.Address)
- }
-}
-
-func TestSign(t *testing.T) {
- dir, am := tmpManager(t, true)
- defer os.RemoveAll(dir)
-
- pass := "" // not used but required by API
- a1, err := am.NewAccount(pass)
- if err != nil {
- t.Fatal(err)
- }
- if err := am.Unlock(a1, ""); err != nil {
- t.Fatal(err)
- }
- if _, err := am.Sign(a1.Address, testSigData); err != nil {
- t.Fatal(err)
- }
-}
-
-func TestSignWithPassphrase(t *testing.T) {
- dir, am := tmpManager(t, true)
- defer os.RemoveAll(dir)
-
- pass := "passwd"
- acc, err := am.NewAccount(pass)
- if err != nil {
- t.Fatal(err)
- }
-
- if _, unlocked := am.unlocked[acc.Address]; unlocked {
- t.Fatal("expected account to be locked")
- }
-
- _, err = am.SignWithPassphrase(acc, pass, testSigData)
- if err != nil {
- t.Fatal(err)
- }
-
- if _, unlocked := am.unlocked[acc.Address]; unlocked {
- t.Fatal("expected account to be locked")
- }
-
- if _, err = am.SignWithPassphrase(acc, "invalid passwd", testSigData); err == nil {
- t.Fatal("expected SignHash to fail with invalid password")
- }
-}
-
-func TestTimedUnlock(t *testing.T) {
- dir, am := tmpManager(t, true)
- defer os.RemoveAll(dir)
-
- pass := "foo"
- a1, err := am.NewAccount(pass)
- if err != nil {
- t.Fatal(err)
- }
-
- // Signing without passphrase fails because account is locked
- _, err = am.Sign(a1.Address, testSigData)
- if err != ErrLocked {
- t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err)
- }
-
- // Signing with passphrase works
- if err = am.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
- t.Fatal(err)
- }
-
- // Signing without passphrase works because account is temp unlocked
- _, err = am.Sign(a1.Address, testSigData)
- if err != nil {
- t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
- }
-
- // Signing fails again after automatic locking
- time.Sleep(250 * time.Millisecond)
- _, err = am.Sign(a1.Address, testSigData)
- if err != ErrLocked {
- t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
- }
-}
-
-func TestOverrideUnlock(t *testing.T) {
- dir, am := tmpManager(t, false)
- defer os.RemoveAll(dir)
-
- pass := "foo"
- a1, err := am.NewAccount(pass)
- if err != nil {
- t.Fatal(err)
- }
-
- // Unlock indefinitely.
- if err = am.TimedUnlock(a1, pass, 5*time.Minute); err != nil {
- t.Fatal(err)
- }
-
- // Signing without passphrase works because account is temp unlocked
- _, err = am.Sign(a1.Address, testSigData)
- if err != nil {
- t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
- }
-
- // reset unlock to a shorter period, invalidates the previous unlock
- if err = am.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
- t.Fatal(err)
- }
-
- // Signing without passphrase still works because account is temp unlocked
- _, err = am.Sign(a1.Address, testSigData)
- if err != nil {
- t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
- }
-
- // Signing fails again after automatic locking
- time.Sleep(250 * time.Millisecond)
- _, err = am.Sign(a1.Address, testSigData)
- if err != ErrLocked {
- t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
- }
-}
-
-// This test should fail under -race if signing races the expiration goroutine.
-func TestSignRace(t *testing.T) {
- dir, am := tmpManager(t, false)
- defer os.RemoveAll(dir)
-
- // Create a test account.
- a1, err := am.NewAccount("")
- if err != nil {
- t.Fatal("could not create the test account", err)
- }
-
- if err := am.TimedUnlock(a1, "", 15*time.Millisecond); err != nil {
- t.Fatal("could not unlock the test account", err)
- }
- end := time.Now().Add(500 * time.Millisecond)
- for time.Now().Before(end) {
- if _, err := am.Sign(a1.Address, testSigData); err == ErrLocked {
- return
- } else if err != nil {
- t.Errorf("Sign error: %v", err)
- return
- }
- time.Sleep(1 * time.Millisecond)
- }
- t.Errorf("Account did not lock within the timeout")
-}
-
-func tmpManager(t *testing.T, encrypted bool) (string, *Manager) {
- d, err := ioutil.TempDir("", "eth-keystore-test")
- if err != nil {
- t.Fatal(err)
- }
- new := NewPlaintextManager
- if encrypted {
- new = func(kd string) *Manager { return NewManager(kd, veryLightScryptN, veryLightScryptP) }
- }
- return d, new(d)
-}
diff --git a/accounts/errors.go b/accounts/errors.go
new file mode 100644
index 000000000..9ecc1eafd
--- /dev/null
+++ b/accounts/errors.go
@@ -0,0 +1,68 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+ "errors"
+ "fmt"
+)
+
+// 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.
+//
+// This usually means either that a password needs to be supplied, or perhaps a
+// one time PIN code displayed by some hardware device.
+type AuthNeededError struct {
+ Needed string // Extra authentication the user needs to provide
+}
+
+// NewAuthNeededError creates a new authentication error with the extra details
+// about the needed fields set.
+func NewAuthNeededError(needed string) error {
+ return &AuthNeededError{
+ Needed: needed,
+ }
+}
+
+// Error implements the standard error interfacel.
+func (err *AuthNeededError) Error() string {
+ return fmt.Sprintf("authentication needed: %s", err.Needed)
+}
diff --git a/accounts/hd.go b/accounts/hd.go
new file mode 100644
index 000000000..e8bc191af
--- /dev/null
+++ b/accounts/hd.go
@@ -0,0 +1,130 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+ "errors"
+ "fmt"
+ "math"
+ "math/big"
+ "strings"
+)
+
+// DefaultRootDerivationPath is the root path to which custom derivation endpoints
+// are appended. As such, the first account will be at m/44'/60'/0'/0, the second
+// at m/44'/60'/0'/1, etc.
+var DefaultRootDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0}
+
+// DefaultBaseDerivationPath is the base path from which custom derivation endpoints
+// are incremented. As such, the first account will be at m/44'/60'/0'/0, the second
+// at m/44'/60'/0'/1, etc.
+var DefaultBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}
+
+// DerivationPath represents the computer friendly version of a hierarchical
+// deterministic wallet account derivaion path.
+//
+// The BIP-32 spec https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
+// defines derivation paths to be of the form:
+//
+// m / purpose' / coin_type' / account' / change / address_index
+//
+// The BIP-44 spec https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
+// defines that the `purpose` be 44' (or 0x8000002C) for crypto currencies, and
+// SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns
+// the `coin_type` 60' (or 0x8000003C) to Ethereum.
+//
+// The root path for Ethereum is m/44'/60'/0'/0 according to the specification
+// from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone
+// yet whether accounts should increment the last component or the children of
+// that. We will go with the simpler approach of incrementing the last component.
+type DerivationPath []uint32
+
+// ParseDerivationPath converts a user specified derivation path string to the
+// internal binary representation.
+//
+// Full derivation paths need to start with the `m/` prefix, relative derivation
+// paths (which will get appended to the default root path) must not have prefixes
+// in front of the first element. Whitespace is ignored.
+func ParseDerivationPath(path string) (DerivationPath, error) {
+ var result DerivationPath
+
+ // Handle absolute or relative paths
+ components := strings.Split(path, "/")
+ switch {
+ case len(components) == 0:
+ return nil, errors.New("empty derivation path")
+
+ case strings.TrimSpace(components[0]) == "":
+ return nil, errors.New("ambiguous path: use 'm/' prefix for absolute paths, or no leading '/' for relative ones")
+
+ case strings.TrimSpace(components[0]) == "m":
+ components = components[1:]
+
+ default:
+ result = append(result, DefaultRootDerivationPath...)
+ }
+ // All remaining components are relative, append one by one
+ if len(components) == 0 {
+ return nil, errors.New("empty derivation path") // Empty relative paths
+ }
+ for _, component := range components {
+ // Ignore any user added whitespace
+ component = strings.TrimSpace(component)
+ var value uint32
+
+ // Handle hardened paths
+ if strings.HasSuffix(component, "'") {
+ value = 0x80000000
+ component = strings.TrimSpace(strings.TrimSuffix(component, "'"))
+ }
+ // Handle the non hardened component
+ bigval, ok := new(big.Int).SetString(component, 0)
+ if !ok {
+ return nil, fmt.Errorf("invalid component: %s", component)
+ }
+ max := math.MaxUint32 - value
+ if bigval.Sign() < 0 || bigval.Cmp(big.NewInt(int64(max))) > 0 {
+ if value == 0 {
+ return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigval, max)
+ }
+ return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigval, max)
+ }
+ value += uint32(bigval.Uint64())
+
+ // Append and repeat
+ result = append(result, value)
+ }
+ return result, nil
+}
+
+// String implements the stringer interface, converting a binary derivation path
+// to its canonical representation.
+func (path DerivationPath) String() string {
+ result := "m"
+ for _, component := range path {
+ var hardened bool
+ if component >= 0x80000000 {
+ component -= 0x80000000
+ hardened = true
+ }
+ result = fmt.Sprintf("%s/%d", result, component)
+ if hardened {
+ result += "'"
+ }
+ }
+ return result
+}
diff --git a/accounts/hd_test.go b/accounts/hd_test.go
new file mode 100644
index 000000000..83ec34adb
--- /dev/null
+++ b/accounts/hd_test.go
@@ -0,0 +1,79 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+ "reflect"
+ "testing"
+)
+
+// Tests that HD derivation paths can be correctly parsed into our internal binary
+// representation.
+func TestHDPathParsing(t *testing.T) {
+ tests := []struct {
+ input string
+ output DerivationPath
+ }{
+ // Plain absolute derivation paths
+ {"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+ {"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
+ {"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+ {"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
+ {"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+ {"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+
+ // Plain relative derivation paths
+ {"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+ {"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
+ {"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+ {"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
+ {"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+
+ // Hexadecimal absolute derivation paths
+ {"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+ {"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
+ {"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+ {"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
+ {"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+ {"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+
+ // Hexadecimal relative derivation paths
+ {"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+ {"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}},
+ {"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+ {"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}},
+ {"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}},
+
+ // Weird inputs just to ensure they work
+ {" m / 44 '\n/\n 60 \n\n\t' /\n0 ' /\t\t 0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}},
+
+ // Invaid derivation paths
+ {"", nil}, // Empty relative derivation path
+ {"m", nil}, // Empty absolute derivation path
+ {"m/", nil}, // Missing last derivation component
+ {"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error
+ {"m/2147483648'", nil}, // Overflows 32 bit integer
+ {"m/-1'", nil}, // Cannot contain negative number
+ }
+ for i, tt := range tests {
+ if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) {
+ t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output)
+ } else if path == nil && err == nil {
+ t.Errorf("test %d: nil path and error: %v", i, err)
+ }
+ }
+}
diff --git a/accounts/addrcache.go b/accounts/keystore/account_cache.go
index a99f23606..3fae3ef5b 100644
--- a/accounts/addrcache.go
+++ b/accounts/keystore/account_cache.go
@@ -14,7 +14,7 @@
// 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
+package keystore
import (
"bufio"
@@ -28,6 +28,7 @@ import (
"sync"
"time"
+ "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
@@ -38,23 +39,23 @@ import (
// exist yet, the code will attempt to create a watcher at most this often.
const minReloadInterval = 2 * time.Second
-type accountsByFile []Account
+type accountsByURL []accounts.Account
-func (s accountsByFile) Len() int { return len(s) }
-func (s accountsByFile) Less(i, j int) bool { return s[i].File < s[j].File }
-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.Cmp(s[j].URL) < 0 }
+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.
type AmbiguousAddrError struct {
Addr common.Address
- Matches []Account
+ Matches []accounts.Account
}
func (err *AmbiguousAddrError) Error() string {
files := ""
for i, a := range err.Matches {
- files += a.File
+ files += a.URL.Path
if i < len(err.Matches)-1 {
files += ", "
}
@@ -62,60 +63,63 @@ func (err *AmbiguousAddrError) Error() string {
return fmt.Sprintf("multiple keys match address (%s)", files)
}
-// addrCache is a live index of all accounts in the keystore.
-type addrCache struct {
+// accountCache is a live index of all accounts in the keystore.
+type accountCache struct {
keydir string
watcher *watcher
mu sync.Mutex
- all accountsByFile
- byAddr map[common.Address][]Account
+ all accountsByURL
+ byAddr map[common.Address][]accounts.Account
throttle *time.Timer
+ notify chan struct{}
}
-func newAddrCache(keydir string) *addrCache {
- ac := &addrCache{
+func newAccountCache(keydir string) (*accountCache, chan struct{}) {
+ ac := &accountCache{
keydir: keydir,
- byAddr: make(map[common.Address][]Account),
+ byAddr: make(map[common.Address][]accounts.Account),
+ notify: make(chan struct{}, 1),
}
ac.watcher = newWatcher(ac)
- return ac
+ return ac, ac.notify
}
-func (ac *addrCache) accounts() []Account {
+func (ac *accountCache) accounts() []accounts.Account {
ac.maybeReload()
ac.mu.Lock()
defer ac.mu.Unlock()
- cpy := make([]Account, len(ac.all))
+ cpy := make([]accounts.Account, len(ac.all))
copy(cpy, ac.all)
return cpy
}
-func (ac *addrCache) 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 *addrCache) add(newAccount Account) {
+func (ac *accountCache) add(newAccount accounts.Account) {
ac.mu.Lock()
defer ac.mu.Unlock()
- i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].File >= newAccount.File })
+ i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Cmp(newAccount.URL) >= 0 })
if i < len(ac.all) && ac.all[i] == newAccount {
return
}
// newAccount is not in the cache.
- ac.all = append(ac.all, Account{})
+ ac.all = append(ac.all, accounts.Account{})
copy(ac.all[i+1:], ac.all[i:])
ac.all[i] = newAccount
ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount)
}
// note: removed needs to be unique here (i.e. both File and Address must be set).
-func (ac *addrCache) delete(removed 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)
@@ -124,7 +128,7 @@ func (ac *addrCache) delete(removed Account) {
}
}
-func removeAccount(slice []Account, elem Account) []Account {
+func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account {
for i := range slice {
if slice[i] == elem {
return append(slice[:i], slice[i+1:]...)
@@ -134,43 +138,44 @@ func removeAccount(slice []Account, elem Account) []Account {
}
// find returns the cached account for address if there is a unique match.
-// The exact matching rules are explained by the documentation of Account.
+// The exact matching rules are explained by the documentation of accounts.Account.
// Callers must hold ac.mu.
-func (ac *addrCache) find(a Account) (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{}) {
matches = ac.byAddr[a.Address]
}
- if a.File != "" {
+ if a.URL.Path != "" {
// If only the basename is specified, complete the path.
- if !strings.ContainsRune(a.File, filepath.Separator) {
- a.File = filepath.Join(ac.keydir, a.File)
+ if !strings.ContainsRune(a.URL.Path, filepath.Separator) {
+ a.URL.Path = filepath.Join(ac.keydir, a.URL.Path)
}
for i := range matches {
- if matches[i].File == a.File {
+ if matches[i].URL == a.URL {
return matches[i], nil
}
}
if (a.Address == common.Address{}) {
- return Account{}, ErrNoMatch
+ return accounts.Account{}, ErrNoMatch
}
}
switch len(matches) {
case 1:
return matches[0], nil
case 0:
- return Account{}, ErrNoMatch
+ return accounts.Account{}, ErrNoMatch
default:
- err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]Account, len(matches))}
+ err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))}
copy(err.Matches, matches)
- return Account{}, err
+ return accounts.Account{}, err
}
}
-func (ac *addrCache) 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.
}
@@ -188,18 +193,22 @@ func (ac *addrCache) maybeReload() {
ac.throttle.Reset(minReloadInterval)
}
-func (ac *addrCache) 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 *addrCache) reload() {
+func (ac *accountCache) reload() {
accounts, err := ac.scan()
if err != nil && glog.V(logger.Debug) {
glog.Errorf("can't load keys: %v", err)
@@ -212,10 +221,14 @@ func (ac *addrCache) 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 *addrCache) scan() ([]Account, error) {
+func (ac *accountCache) scan() ([]accounts.Account, error) {
files, err := ioutil.ReadDir(ac.keydir)
if err != nil {
return nil, err
@@ -223,7 +236,7 @@ func (ac *addrCache) scan() ([]Account, error) {
var (
buf = new(bufio.Reader)
- addrs []Account
+ addrs []accounts.Account
keyJSON struct {
Address string `json:"address"`
}
@@ -250,7 +263,7 @@ func (ac *addrCache) scan() ([]Account, error) {
case (addr == common.Address{}):
glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path)
default:
- addrs = append(addrs, Account{Address: addr, File: path})
+ addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}})
}
fd.Close()
}
diff --git a/accounts/addrcache_test.go b/accounts/keystore/account_cache_test.go
index e5f08cffc..554196321 100644
--- a/accounts/addrcache_test.go
+++ b/accounts/keystore/account_cache_test.go
@@ -14,7 +14,7 @@
// 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
+package keystore
import (
"fmt"
@@ -28,23 +28,24 @@ import (
"github.com/cespare/cp"
"github.com/davecgh/go-spew/spew"
+ "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
)
var (
cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore"))
- cachetestAccounts = []Account{
+ cachetestAccounts = []accounts.Account{
{
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
- File: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")},
},
{
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
- File: filepath.Join(cachetestDir, "aaa"),
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")},
},
{
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
- File: filepath.Join(cachetestDir, "zzz"),
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")},
},
}
)
@@ -52,29 +53,36 @@ var (
func TestWatchNewFile(t *testing.T) {
t.Parallel()
- dir, am := tmpManager(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.
- wantAccounts := make([]Account, len(cachetestAccounts))
+ wantAccounts := make([]accounts.Account, len(cachetestAccounts))
for i := range cachetestAccounts {
- a := cachetestAccounts[i]
- a.File = filepath.Join(dir, filepath.Base(a.File))
- wantAccounts[i] = a
- if err := cp.CopyFile(a.File, cachetestAccounts[i].File); err != nil {
+ wantAccounts[i] = accounts.Account{
+ Address: cachetestAccounts[i].Address,
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))},
+ }
+ if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil {
t.Fatal(err)
}
}
- // am should see the accounts.
- var list []Account
+ // 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)
@@ -85,12 +93,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 := NewManager(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)
}
@@ -100,16 +108,22 @@ func TestWatchNoDir(t *testing.T) {
os.MkdirAll(dir, 0700)
defer os.RemoveAll(dir)
file := filepath.Join(dir, "aaa")
- if err := cp.CopyFile(file, cachetestAccounts[0].File); err != nil {
+ if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil {
t.Fatal(err)
}
- // am should see the account.
- wantAccounts := []Account{cachetestAccounts[0]}
- wantAccounts[0].File = file
+ // ks should see the account.
+ wantAccounts := []accounts.Account{cachetestAccounts[0]}
+ wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: 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)
@@ -118,7 +132,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))
@@ -126,55 +140,55 @@ func TestCacheInitialReload(t *testing.T) {
}
func TestCacheAddDeleteOrder(t *testing.T) {
- cache := newAddrCache("testdata/no-such-dir")
+ cache, _ := newAccountCache("testdata/no-such-dir")
cache.watcher.running = true // prevent unexpected reloads
- accounts := []Account{
+ accs := []accounts.Account{
{
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
- File: "-309830980",
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"},
},
{
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
- File: "ggg",
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"},
},
{
Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"),
- File: "zzzzzz-the-very-last-one.keyXXX",
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"},
},
{
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
- File: "SOMETHING.key",
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"},
},
{
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
- File: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8",
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"},
},
{
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
- File: "aaa",
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"},
},
{
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
- File: "zzz",
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"},
},
}
- for _, a := range accounts {
+ for _, a := range accs {
cache.add(a)
}
// Add some of them twice to check that they don't get reinserted.
- cache.add(accounts[0])
- cache.add(accounts[2])
+ cache.add(accs[0])
+ cache.add(accs[2])
// Check that the account list is sorted by filename.
- wantAccounts := make([]Account, len(accounts))
- copy(wantAccounts, accounts)
- sort.Sort(accountsByFile(wantAccounts))
+ wantAccounts := make([]accounts.Account, len(accs))
+ copy(wantAccounts, accs)
+ sort.Sort(accountsByURL(wantAccounts))
list := cache.accounts()
if !reflect.DeepEqual(list, wantAccounts) {
- t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accounts), spew.Sdump(wantAccounts))
+ t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts))
}
- for _, a := range accounts {
+ for _, a := range accs {
if !cache.hasAddress(a.Address) {
t.Errorf("expected hasAccount(%x) to return true", a.Address)
}
@@ -184,13 +198,13 @@ func TestCacheAddDeleteOrder(t *testing.T) {
}
// Delete a few keys from the cache.
- for i := 0; i < len(accounts); i += 2 {
+ for i := 0; i < len(accs); i += 2 {
cache.delete(wantAccounts[i])
}
- cache.delete(Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), File: "something"})
+ cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}})
// Check content again after deletion.
- wantAccountsAfterDelete := []Account{
+ wantAccountsAfterDelete := []accounts.Account{
wantAccounts[1],
wantAccounts[3],
wantAccounts[5],
@@ -211,63 +225,63 @@ 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
- accounts := []Account{
+ accs := []accounts.Account{
{
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
- File: filepath.Join(dir, "a.key"),
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")},
},
{
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
- File: filepath.Join(dir, "b.key"),
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")},
},
{
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
- File: filepath.Join(dir, "c.key"),
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")},
},
{
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
- File: filepath.Join(dir, "c2.key"),
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")},
},
}
- for _, a := range accounts {
+ for _, a := range accs {
cache.add(a)
}
- nomatchAccount := Account{
+ nomatchAccount := accounts.Account{
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
- File: filepath.Join(dir, "something"),
+ URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")},
}
tests := []struct {
- Query Account
- WantResult Account
+ Query accounts.Account
+ WantResult accounts.Account
WantError error
}{
// by address
- {Query: Account{Address: accounts[0].Address}, WantResult: accounts[0]},
+ {Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]},
// by file
- {Query: Account{File: accounts[0].File}, WantResult: accounts[0]},
+ {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]},
// by basename
- {Query: Account{File: filepath.Base(accounts[0].File)}, WantResult: accounts[0]},
+ {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]},
// by file and address
- {Query: accounts[0], WantResult: accounts[0]},
+ {Query: accs[0], WantResult: accs[0]},
// ambiguous address, tie resolved by file
- {Query: accounts[2], WantResult: accounts[2]},
+ {Query: accs[2], WantResult: accs[2]},
// ambiguous address error
{
- Query: Account{Address: accounts[2].Address},
+ Query: accounts.Account{Address: accs[2].Address},
WantError: &AmbiguousAddrError{
- Addr: accounts[2].Address,
- Matches: []Account{accounts[2], accounts[3]},
+ Addr: accs[2].Address,
+ Matches: []accounts.Account{accs[2], accs[3]},
},
},
// no match error
{Query: nomatchAccount, WantError: ErrNoMatch},
- {Query: Account{File: nomatchAccount.File}, WantError: ErrNoMatch},
- {Query: Account{File: filepath.Base(nomatchAccount.File)}, WantError: ErrNoMatch},
- {Query: Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch},
+ {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch},
+ {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: ErrNoMatch},
+ {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch},
}
for i, test := range tests {
a, err := cache.find(test.Query)
diff --git a/accounts/key.go b/accounts/keystore/key.go
index dbcb49dcf..e2bdf09fe 100644
--- a/accounts/key.go
+++ b/accounts/keystore/key.go
@@ -14,7 +14,7 @@
// 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
+package keystore
import (
"bytes"
@@ -29,6 +29,7 @@ import (
"strings"
"time"
+ "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1"
@@ -175,13 +176,13 @@ func newKey(rand io.Reader) (*Key, error) {
return newKeyFromECDSA(privateKeyECDSA), nil
}
-func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, Account, error) {
+func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) {
key, err := newKey(rand)
if err != nil {
- return nil, Account{}, err
+ return nil, accounts.Account{}, err
}
- a := Account{Address: key.Address, File: ks.JoinPath(keyFileName(key.Address))}
- if err := ks.StoreKey(a.File, key, auth); err != nil {
+ a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}}
+ if err := ks.StoreKey(a.URL.Path, key, auth); err != nil {
zeroKey(key.PrivateKey)
return nil, a, err
}
diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go
new file mode 100644
index 000000000..a01ff17e3
--- /dev/null
+++ b/accounts/keystore/keystore.go
@@ -0,0 +1,494 @@
+// Copyright 2015 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 implements encrypted storage of secp256k1 private keys.
+//
+// Keys are stored as encrypted JSON files according to the Web3 Secret Storage specification.
+// See https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition for more information.
+package keystore
+
+import (
+ "crypto/ecdsa"
+ crand "crypto/rand"
+ "errors"
+ "fmt"
+ "math/big"
+ "os"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "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 (
+ 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")
+)
+
+// KeyStoreType is the reflect type of a keystore backend.
+var KeyStoreType = reflect.TypeOf(&KeyStore{})
+
+// KeyStoreScheme is the protocol scheme prefixing account and wallet URLs.
+var KeyStoreScheme = "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 {
+ 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 {
+ *Key
+ abort chan struct{}
+}
+
+// NewKeyStore creates a keystore for the given directory.
+func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
+ keydir, _ = filepath.Abs(keydir)
+ ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP}}
+ ks.init(keydir)
+ return ks
+}
+
+// NewPlaintextKeyStore creates a keystore for the given directory.
+// Deprecated: Use NewKeyStore.
+func NewPlaintextKeyStore(keydir string) *KeyStore {
+ keydir, _ = filepath.Abs(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, 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
+ ks.mu.Lock()
+ accs := ks.cache.accounts()
+
+ // Transform the current list of wallets into the new one
+ 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().Cmp(account.URL) < 0 {
+ 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().Cmp(account.URL) > 0 {
+ 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.
+func (ks *KeyStore) HasAddress(addr common.Address) bool {
+ return ks.cache.hasAddress(addr)
+}
+
+// Accounts returns all key files present in the directory.
+func (ks *KeyStore) Accounts() []accounts.Account {
+ return ks.cache.accounts()
+}
+
+// Delete deletes the key matched by account if the passphrase is correct.
+// If the account contains no filename, the address must match a unique key.
+func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error {
+ // Decrypting the key isn't really necessary, but we do
+ // it anyway to check the password and zero out the key
+ // immediately afterwards.
+ a, key, err := ks.getDecryptedKey(a, passphrase)
+ if key != nil {
+ zeroKey(key.PrivateKey)
+ }
+ if err != nil {
+ return err
+ }
+ // The order is crucial here. The key is dropped from the
+ // cache after the file is gone so that a reload happening in
+ // between won't insert it into the cache again.
+ err = os.Remove(a.URL.Path)
+ if err == nil {
+ ks.cache.delete(a)
+ ks.refreshWallets()
+ }
+ return err
+}
+
+// SignHash calculates a ECDSA signature for the given hash. The produced
+// signature is in the [R || S || V] format where V is 0 or 1.
+func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) {
+ // Look up the key to sign with and abort if it cannot be found
+ ks.mu.RLock()
+ defer ks.mu.RUnlock()
+
+ unlockedKey, found := ks.unlocked[a.Address]
+ if !found {
+ return nil, ErrLocked
+ }
+ // Sign the hash using plain ECDSA operations
+ return crypto.Sign(hash, unlockedKey.PrivateKey)
+}
+
+// SignTx signs the given transaction with the requested account.
+func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
+ // Look up the key to sign with and abort if it cannot be found
+ ks.mu.RLock()
+ defer ks.mu.RUnlock()
+
+ unlockedKey, found := ks.unlocked[a.Address]
+ if !found {
+ return nil, ErrLocked
+ }
+ // Depending on the presence of the chain ID, sign with EIP155 or homestead
+ if chainID != nil {
+ return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey)
+ }
+ return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey)
+}
+
+// SignHashWithPassphrase signs hash if the private key matching the given address
+// can be decrypted with the given passphrase. The produced signature is in the
+// [R || S || V] format where V is 0 or 1.
+func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, passphrase string, hash []byte) (signature []byte, err error) {
+ _, key, err := ks.getDecryptedKey(a, passphrase)
+ if err != nil {
+ return nil, err
+ }
+ defer zeroKey(key.PrivateKey)
+ return crypto.Sign(hash, key.PrivateKey)
+}
+
+// SignTxWithPassphrase signs the transaction if the private key matching the
+// given address can be decrypted with the given passphrase.
+func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
+ _, key, err := ks.getDecryptedKey(a, passphrase)
+ if err != nil {
+ return nil, err
+ }
+ defer zeroKey(key.PrivateKey)
+
+ // Depending on the presence of the chain ID, sign with EIP155 or homestead
+ if chainID != nil {
+ return types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey)
+ }
+ return types.SignTx(tx, types.HomesteadSigner{}, key.PrivateKey)
+}
+
+// Unlock unlocks the given account indefinitely.
+func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error {
+ return ks.TimedUnlock(a, passphrase, 0)
+}
+
+// Lock removes the private key with the given address from memory.
+func (ks *KeyStore) Lock(addr common.Address) error {
+ ks.mu.Lock()
+ if unl, found := ks.unlocked[addr]; found {
+ ks.mu.Unlock()
+ ks.expire(addr, unl, time.Duration(0)*time.Nanosecond)
+ } else {
+ ks.mu.Unlock()
+ }
+ return nil
+}
+
+// TimedUnlock unlocks the given account with the passphrase. The account
+// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account
+// until the program exits. The account must match a unique key file.
+//
+// If the account address is already unlocked for a duration, TimedUnlock extends or
+// shortens the active unlock timeout. If the address was previously unlocked
+// indefinitely the timeout is not altered.
+func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error {
+ a, key, err := ks.getDecryptedKey(a, passphrase)
+ if err != nil {
+ return err
+ }
+
+ ks.mu.Lock()
+ defer ks.mu.Unlock()
+ u, found := ks.unlocked[a.Address]
+ if found {
+ if u.abort == nil {
+ // The address was unlocked indefinitely, so unlocking
+ // it with a timeout would be confusing.
+ zeroKey(key.PrivateKey)
+ return nil
+ }
+ // Terminate the expire goroutine and replace it below.
+ close(u.abort)
+ }
+ if timeout > 0 {
+ u = &unlocked{Key: key, abort: make(chan struct{})}
+ go ks.expire(a.Address, u, timeout)
+ } else {
+ u = &unlocked{Key: key}
+ }
+ ks.unlocked[a.Address] = u
+ return nil
+}
+
+// Find resolves the given account into a unique entry in the keystore.
+func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) {
+ ks.cache.maybeReload()
+ ks.cache.mu.Lock()
+ a, err := ks.cache.find(a)
+ ks.cache.mu.Unlock()
+ return a, err
+}
+
+func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) {
+ a, err := ks.Find(a)
+ if err != nil {
+ return a, nil, err
+ }
+ key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth)
+ return a, key, err
+}
+
+func (ks *KeyStore) expire(addr common.Address, u *unlocked, timeout time.Duration) {
+ t := time.NewTimer(timeout)
+ defer t.Stop()
+ select {
+ case <-u.abort:
+ // just quit
+ case <-t.C:
+ ks.mu.Lock()
+ // only drop if it's still the same key instance that dropLater
+ // was launched with. we can check that using pointer equality
+ // because the map stores a new pointer every time the key is
+ // unlocked.
+ if ks.unlocked[addr] == u {
+ zeroKey(u.PrivateKey)
+ delete(ks.unlocked, addr)
+ }
+ ks.mu.Unlock()
+ }
+}
+
+// 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.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
+}
+
+// Export exports as a JSON key, encrypted with newPassphrase.
+func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) (keyJSON []byte, err error) {
+ _, key, err := ks.getDecryptedKey(a, passphrase)
+ if err != nil {
+ return nil, err
+ }
+ var N, P int
+ if store, ok := ks.storage.(*keyStorePassphrase); ok {
+ N, P = store.scryptN, store.scryptP
+ } else {
+ N, P = StandardScryptN, StandardScryptP
+ }
+ return EncryptKey(key, newPassphrase, N, P)
+}
+
+// Import stores the given encrypted JSON key into the key directory.
+func (ks *KeyStore) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) {
+ key, err := DecryptKey(keyJSON, passphrase)
+ if key != nil && key.PrivateKey != nil {
+ defer zeroKey(key.PrivateKey)
+ }
+ if err != nil {
+ return accounts.Account{}, err
+ }
+ return ks.importKey(key, newPassphrase)
+}
+
+// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase.
+func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) {
+ key := newKeyFromECDSA(priv)
+ if ks.cache.hasAddress(key.Address) {
+ return accounts.Account{}, fmt.Errorf("account already exists")
+ }
+
+ return ks.importKey(key, passphrase)
+}
+
+func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
+ a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}}
+ if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil {
+ return accounts.Account{}, err
+ }
+ ks.cache.add(a)
+ ks.refreshWallets()
+ return a, nil
+}
+
+// Update changes the passphrase of an existing account.
+func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error {
+ a, key, err := ks.getDecryptedKey(a, passphrase)
+ if err != nil {
+ return err
+ }
+ return ks.storage.StoreKey(a.URL.Path, 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.storage, keyJSON, passphrase)
+ if err != nil {
+ return a, err
+ }
+ ks.cache.add(a)
+ ks.refreshWallets()
+ return a, nil
+}
+
+// zeroKey zeroes a private key in memory.
+func zeroKey(k *ecdsa.PrivateKey) {
+ b := k.D.Bits()
+ for i := range b {
+ b[i] = 0
+ }
+}
diff --git a/accounts/key_store_passphrase.go b/accounts/keystore/keystore_passphrase.go
index 4a777956d..8ef510fcf 100644
--- a/accounts/key_store_passphrase.go
+++ b/accounts/keystore/keystore_passphrase.go
@@ -23,7 +23,7 @@ The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-St
*/
-package accounts
+package keystore
import (
"bytes"
diff --git a/accounts/key_store_passphrase_test.go b/accounts/keystore/keystore_passphrase_test.go
index 217393fa5..086addbc1 100644
--- a/accounts/key_store_passphrase_test.go
+++ b/accounts/keystore/keystore_passphrase_test.go
@@ -14,7 +14,7 @@
// 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
+package keystore
import (
"io/ioutil"
diff --git a/accounts/key_store_plain.go b/accounts/keystore/keystore_plain.go
index 2cbaa94df..b490ca72b 100644
--- a/accounts/key_store_plain.go
+++ b/accounts/keystore/keystore_plain.go
@@ -14,7 +14,7 @@
// 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
+package keystore
import (
"encoding/json"
diff --git a/accounts/key_store_test.go b/accounts/keystore/keystore_plain_test.go
index d0713caa0..8c0eb52ea 100644
--- a/accounts/key_store_test.go
+++ b/accounts/keystore/keystore_plain_test.go
@@ -14,7 +14,7 @@
// 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
+package keystore
import (
"crypto/rand"
@@ -30,7 +30,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
)
-func tmpKeyStore(t *testing.T, encrypted bool) (dir string, ks keyStore) {
+func tmpKeyStoreIface(t *testing.T, encrypted bool) (dir string, ks keyStore) {
d, err := ioutil.TempDir("", "geth-keystore-test")
if err != nil {
t.Fatal(err)
@@ -44,7 +44,7 @@ func tmpKeyStore(t *testing.T, encrypted bool) (dir string, ks keyStore) {
}
func TestKeyStorePlain(t *testing.T) {
- dir, ks := tmpKeyStore(t, false)
+ dir, ks := tmpKeyStoreIface(t, false)
defer os.RemoveAll(dir)
pass := "" // not used but required by API
@@ -52,7 +52,7 @@ func TestKeyStorePlain(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- k2, err := ks.GetKey(k1.Address, account.File, pass)
+ k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
if err != nil {
t.Fatal(err)
}
@@ -65,7 +65,7 @@ func TestKeyStorePlain(t *testing.T) {
}
func TestKeyStorePassphrase(t *testing.T) {
- dir, ks := tmpKeyStore(t, true)
+ dir, ks := tmpKeyStoreIface(t, true)
defer os.RemoveAll(dir)
pass := "foo"
@@ -73,7 +73,7 @@ func TestKeyStorePassphrase(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- k2, err := ks.GetKey(k1.Address, account.File, pass)
+ k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
if err != nil {
t.Fatal(err)
}
@@ -86,7 +86,7 @@ func TestKeyStorePassphrase(t *testing.T) {
}
func TestKeyStorePassphraseDecryptionFail(t *testing.T) {
- dir, ks := tmpKeyStore(t, true)
+ dir, ks := tmpKeyStoreIface(t, true)
defer os.RemoveAll(dir)
pass := "foo"
@@ -94,13 +94,13 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if _, err = ks.GetKey(k1.Address, account.File, "bar"); err != ErrDecrypt {
+ if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err != ErrDecrypt {
t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt)
}
}
func TestImportPreSaleKey(t *testing.T) {
- dir, ks := tmpKeyStore(t, true)
+ dir, ks := tmpKeyStoreIface(t, true)
defer os.RemoveAll(dir)
// file content of a presale key file generated with:
@@ -115,8 +115,8 @@ func TestImportPreSaleKey(t *testing.T) {
if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") {
t.Errorf("imported account has wrong address %x", account.Address)
}
- if !strings.HasPrefix(account.File, dir) {
- t.Errorf("imported account file not in keystore directory: %q", account.File)
+ if !strings.HasPrefix(account.URL.Path, dir) {
+ t.Errorf("imported account file not in keystore directory: %q", account.URL)
}
}
@@ -142,19 +142,19 @@ func TestV3_PBKDF2_1(t *testing.T) {
func TestV3_PBKDF2_2(t *testing.T) {
t.Parallel()
- tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t)
+ tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
testDecryptV3(tests["test1"], t)
}
func TestV3_PBKDF2_3(t *testing.T) {
t.Parallel()
- tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t)
+ tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
testDecryptV3(tests["python_generated_test_with_odd_iv"], t)
}
func TestV3_PBKDF2_4(t *testing.T) {
t.Parallel()
- tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t)
+ tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
testDecryptV3(tests["evilnonce"], t)
}
@@ -166,7 +166,7 @@ func TestV3_Scrypt_1(t *testing.T) {
func TestV3_Scrypt_2(t *testing.T) {
t.Parallel()
- tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t)
+ tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
testDecryptV3(tests["test2"], t)
}
diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go
new file mode 100644
index 000000000..60f2606ee
--- /dev/null
+++ b/accounts/keystore/keystore_test.go
@@ -0,0 +1,365 @@
+// Copyright 2015 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 (
+ "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)
+
+func TestKeyStore(t *testing.T) {
+ dir, ks := tmpKeyStore(t, true)
+ defer os.RemoveAll(dir)
+
+ a, err := ks.NewAccount("foo")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.HasPrefix(a.URL.Path, dir) {
+ t.Errorf("account file %s doesn't have dir prefix", a.URL)
+ }
+ stat, err := os.Stat(a.URL.Path)
+ if err != nil {
+ t.Fatalf("account file %s doesn't exist (%v)", a.URL, err)
+ }
+ if runtime.GOOS != "windows" && stat.Mode() != 0600 {
+ t.Fatalf("account file has wrong mode: got %o, want %o", stat.Mode(), 0600)
+ }
+ if !ks.HasAddress(a.Address) {
+ t.Errorf("HasAccount(%x) should've returned true", a.Address)
+ }
+ if err := ks.Update(a, "foo", "bar"); err != nil {
+ t.Errorf("Update error: %v", err)
+ }
+ if err := ks.Delete(a, "bar"); err != nil {
+ t.Errorf("Delete error: %v", err)
+ }
+ if common.FileExist(a.URL.Path) {
+ t.Errorf("account file %s should be gone after Delete", a.URL)
+ }
+ if ks.HasAddress(a.Address) {
+ t.Errorf("HasAccount(%x) should've returned true after Delete", a.Address)
+ }
+}
+
+func TestSign(t *testing.T) {
+ dir, ks := tmpKeyStore(t, true)
+ defer os.RemoveAll(dir)
+
+ pass := "" // not used but required by API
+ a1, err := ks.NewAccount(pass)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := ks.Unlock(a1, ""); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSignWithPassphrase(t *testing.T) {
+ dir, ks := tmpKeyStore(t, true)
+ defer os.RemoveAll(dir)
+
+ pass := "passwd"
+ acc, err := ks.NewAccount(pass)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, unlocked := ks.unlocked[acc.Address]; unlocked {
+ t.Fatal("expected account to be locked")
+ }
+
+ _, err = ks.SignHashWithPassphrase(acc, pass, testSigData)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if _, unlocked := ks.unlocked[acc.Address]; unlocked {
+ t.Fatal("expected account to be locked")
+ }
+
+ if _, err = ks.SignHashWithPassphrase(acc, "invalid passwd", testSigData); err == nil {
+ t.Fatal("expected SignHashWithPassphrase to fail with invalid password")
+ }
+}
+
+func TestTimedUnlock(t *testing.T) {
+ dir, ks := tmpKeyStore(t, true)
+ defer os.RemoveAll(dir)
+
+ pass := "foo"
+ a1, err := ks.NewAccount(pass)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Signing without passphrase fails because account is locked
+ _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
+ if err != ErrLocked {
+ t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err)
+ }
+
+ // Signing with passphrase works
+ if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
+ t.Fatal(err)
+ }
+
+ // Signing without passphrase works because account is temp unlocked
+ _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
+ if err != nil {
+ t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
+ }
+
+ // Signing fails again after automatic locking
+ time.Sleep(250 * time.Millisecond)
+ _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
+ if err != ErrLocked {
+ t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
+ }
+}
+
+func TestOverrideUnlock(t *testing.T) {
+ dir, ks := tmpKeyStore(t, false)
+ defer os.RemoveAll(dir)
+
+ pass := "foo"
+ a1, err := ks.NewAccount(pass)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Unlock indefinitely.
+ if err = ks.TimedUnlock(a1, pass, 5*time.Minute); err != nil {
+ t.Fatal(err)
+ }
+
+ // Signing without passphrase works because account is temp unlocked
+ _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
+ if err != nil {
+ t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
+ }
+
+ // reset unlock to a shorter period, invalidates the previous unlock
+ if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil {
+ t.Fatal(err)
+ }
+
+ // Signing without passphrase still works because account is temp unlocked
+ _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
+ if err != nil {
+ t.Fatal("Signing shouldn't return an error after unlocking, got ", err)
+ }
+
+ // Signing fails again after automatic locking
+ time.Sleep(250 * time.Millisecond)
+ _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData)
+ if err != ErrLocked {
+ t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err)
+ }
+}
+
+// This test should fail under -race if signing races the expiration goroutine.
+func TestSignRace(t *testing.T) {
+ dir, ks := tmpKeyStore(t, false)
+ defer os.RemoveAll(dir)
+
+ // Create a test account.
+ a1, err := ks.NewAccount("")
+ if err != nil {
+ t.Fatal("could not create the test account", err)
+ }
+
+ if err := ks.TimedUnlock(a1, "", 15*time.Millisecond); err != nil {
+ t.Fatal("could not unlock the test account", err)
+ }
+ end := time.Now().Add(500 * time.Millisecond)
+ for time.Now().Before(end) {
+ if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrLocked {
+ return
+ } else if err != nil {
+ t.Errorf("Sign error: %v", err)
+ return
+ }
+ time.Sleep(1 * time.Millisecond)
+ }
+ 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])
+ }
+ }
+ }
+ }
+}
+
+func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) {
+ d, err := ioutil.TempDir("", "eth-keystore-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ new := NewPlaintextKeyStore
+ if encrypted {
+ new = func(kd string) *KeyStore { return NewKeyStore(kd, veryLightScryptN, veryLightScryptP) }
+ }
+ return d, new(d)
+}
diff --git a/accounts/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go
new file mode 100644
index 000000000..7165d2821
--- /dev/null
+++ b/accounts/keystore/keystore_wallet.go
@@ -0,0 +1,139 @@
+// 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"
+
+ ethereum "github.com/ethereum/go-ethereum"
+ "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
+}
+
+// URL implements accounts.Wallet, returning the URL of the account within.
+func (w *keystoreWallet) URL() accounts.URL {
+ 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 {
+ w.keystore.mu.RLock()
+ defer w.keystore.mu.RUnlock()
+
+ if _, ok := w.keystore.unlocked[w.account.Address]; ok {
+ return "Unlocked"
+ }
+ return "Locked"
+}
+
+// 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 == (accounts.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 accounts.DerivationPath, pin bool) (accounts.Account, error) {
+ return accounts.Account{}, accounts.ErrNotSupported
+}
+
+// SelfDerive implements accounts.Wallet, but is a noop for plain wallets since
+// there is no notion of hierarchical account derivation for plain keystore accounts.
+func (w *keystoreWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {}
+
+// SignHash implements accounts.Wallet, attempting to sign the given hash with
+// the given account. If the wallet does not wrap this particular account, an
+// error is returned to avoid account leakage (even though in theory we may be
+// 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 != (accounts.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 != (accounts.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 != (accounts.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 != (accounts.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/presale.go b/accounts/keystore/presale.go
index f00b4f502..5b883c45f 100644
--- a/accounts/presale.go
+++ b/accounts/keystore/presale.go
@@ -14,7 +14,7 @@
// 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
+package keystore
import (
"crypto/aes"
@@ -25,20 +25,21 @@ import (
"errors"
"fmt"
+ "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/crypto"
"github.com/pborman/uuid"
"golang.org/x/crypto/pbkdf2"
)
// creates a Key and stores that in the given KeyStore by decrypting a presale key JSON
-func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (Account, *Key, error) {
+func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accounts.Account, *Key, error) {
key, err := decryptPreSaleKey(keyJSON, password)
if err != nil {
- return Account{}, nil, err
+ return accounts.Account{}, nil, err
}
key.Id = uuid.NewRandom()
- a := Account{Address: key.Address, File: keyStore.JoinPath(keyFileName(key.Address))}
- err = keyStore.StoreKey(a.File, key, password)
+ a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: keyStore.JoinPath(keyFileName(key.Address))}}
+ err = keyStore.StoreKey(a.URL.Path, key, password)
return a, key, err
}
diff --git a/accounts/testdata/dupes/1 b/accounts/keystore/testdata/dupes/1
index a3868ec6d..a3868ec6d 100644
--- a/accounts/testdata/dupes/1
+++ b/accounts/keystore/testdata/dupes/1
diff --git a/accounts/testdata/dupes/2 b/accounts/keystore/testdata/dupes/2
index a3868ec6d..a3868ec6d 100644
--- a/accounts/testdata/dupes/2
+++ b/accounts/keystore/testdata/dupes/2
diff --git a/accounts/testdata/dupes/foo b/accounts/keystore/testdata/dupes/foo
index c57060aea..c57060aea 100644
--- a/accounts/testdata/dupes/foo
+++ b/accounts/keystore/testdata/dupes/foo
diff --git a/accounts/testdata/keystore/.hiddenfile b/accounts/keystore/testdata/keystore/.hiddenfile
index d91faccde..d91faccde 100644
--- a/accounts/testdata/keystore/.hiddenfile
+++ b/accounts/keystore/testdata/keystore/.hiddenfile
diff --git a/accounts/testdata/keystore/README b/accounts/keystore/testdata/keystore/README
index a5a86f964..a5a86f964 100644
--- a/accounts/testdata/keystore/README
+++ b/accounts/keystore/testdata/keystore/README
diff --git a/accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 b/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
index c57060aea..c57060aea 100644
--- a/accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
+++ b/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
diff --git a/accounts/testdata/keystore/aaa b/accounts/keystore/testdata/keystore/aaa
index a3868ec6d..a3868ec6d 100644
--- a/accounts/testdata/keystore/aaa
+++ b/accounts/keystore/testdata/keystore/aaa
diff --git a/accounts/testdata/keystore/empty b/accounts/keystore/testdata/keystore/empty
index e69de29bb..e69de29bb 100644
--- a/accounts/testdata/keystore/empty
+++ b/accounts/keystore/testdata/keystore/empty
diff --git a/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e
index 309841e52..309841e52 100644
--- a/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e
+++ b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e
diff --git a/accounts/testdata/keystore/garbage b/accounts/keystore/testdata/keystore/garbage
index ff45091e7..ff45091e7 100644
--- a/accounts/testdata/keystore/garbage
+++ b/accounts/keystore/testdata/keystore/garbage
Binary files differ
diff --git a/accounts/testdata/keystore/no-address b/accounts/keystore/testdata/keystore/no-address
index ad51269ea..ad51269ea 100644
--- a/accounts/testdata/keystore/no-address
+++ b/accounts/keystore/testdata/keystore/no-address
diff --git a/accounts/testdata/keystore/zero b/accounts/keystore/testdata/keystore/zero
index b52617f8a..b52617f8a 100644
--- a/accounts/testdata/keystore/zero
+++ b/accounts/keystore/testdata/keystore/zero
diff --git a/accounts/testdata/keystore/zzz b/accounts/keystore/testdata/keystore/zzz
index cfd8a4701..cfd8a4701 100644
--- a/accounts/testdata/keystore/zzz
+++ b/accounts/keystore/testdata/keystore/zzz
diff --git a/accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e b/accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e
index 498d8131e..498d8131e 100644
--- a/accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e
+++ b/accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e
diff --git a/accounts/testdata/v1_test_vector.json b/accounts/keystore/testdata/v1_test_vector.json
index 3d09b55b5..3d09b55b5 100644
--- a/accounts/testdata/v1_test_vector.json
+++ b/accounts/keystore/testdata/v1_test_vector.json
diff --git a/accounts/testdata/v3_test_vector.json b/accounts/keystore/testdata/v3_test_vector.json
index 1e7f790c0..1e7f790c0 100644
--- a/accounts/testdata/v3_test_vector.json
+++ b/accounts/keystore/testdata/v3_test_vector.json
diff --git a/accounts/testdata/very-light-scrypt.json b/accounts/keystore/testdata/very-light-scrypt.json
index d23b9b2b9..d23b9b2b9 100644
--- a/accounts/testdata/very-light-scrypt.json
+++ b/accounts/keystore/testdata/very-light-scrypt.json
diff --git a/accounts/watch.go b/accounts/keystore/watch.go
index 472be2df7..0b4401255 100644
--- a/accounts/watch.go
+++ b/accounts/keystore/watch.go
@@ -16,7 +16,7 @@
// +build darwin,!ios freebsd linux,!arm64 netbsd solaris
-package accounts
+package keystore
import (
"time"
@@ -27,14 +27,14 @@ import (
)
type watcher struct {
- ac *addrCache
+ ac *accountCache
starting bool
running bool
ev chan notify.EventInfo
quit chan struct{}
}
-func newWatcher(ac *addrCache) *watcher {
+func newWatcher(ac *accountCache) *watcher {
return &watcher{
ac: ac,
ev: make(chan notify.EventInfo, 10),
diff --git a/accounts/watch_fallback.go b/accounts/keystore/watch_fallback.go
index bf971cb1b..7c5e9cb2e 100644
--- a/accounts/watch_fallback.go
+++ b/accounts/keystore/watch_fallback.go
@@ -19,10 +19,10 @@
// This is the fallback implementation of directory watching.
// It is used on unsupported platforms.
-package accounts
+package keystore
type watcher struct{ running bool }
-func newWatcher(*addrCache) *watcher { return new(watcher) }
-func (*watcher) start() {}
-func (*watcher) close() {}
+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..12a5bfcd9
--- /dev/null
+++ b/accounts/manager.go
@@ -0,0 +1,198 @@
+// 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()
+
+ parsed, err := parseURL(url)
+ if err != nil {
+ return nil, err
+ }
+ for _, wallet := range am.Wallets() {
+ if wallet.URL() == parsed {
+ 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().Cmp(wallet.URL()) >= 0 })
+ 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().Cmp(wallet.URL()) >= 0 })
+ 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/url.go b/accounts/url.go
new file mode 100644
index 000000000..a2d00c1c6
--- /dev/null
+++ b/accounts/url.go
@@ -0,0 +1,79 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+)
+
+// URL represents the canonical identification URL of a wallet or account.
+//
+// It is a simplified version of url.URL, with the important limitations (which
+// are considered features here) that it contains value-copyable components only,
+// as well as that it doesn't do any URL encoding/decoding of special characters.
+//
+// The former is important to allow an account to be copied without leaving live
+// references to the original version, whereas the latter is important to ensure
+// one single canonical form opposed to many allowed ones by the RFC 3986 spec.
+//
+// As such, these URLs should not be used outside of the scope of an Ethereum
+// wallet or account.
+type URL struct {
+ Scheme string // Protocol scheme to identify a capable account backend
+ Path string // Path for the backend to identify a unique entity
+}
+
+// parseURL converts a user supplied URL into the accounts specific structure.
+func parseURL(url string) (URL, error) {
+ parts := strings.Split(url, "://")
+ if len(parts) != 2 || parts[0] == "" {
+ return URL{}, errors.New("protocol scheme missing")
+ }
+ return URL{
+ Scheme: parts[0],
+ Path: parts[1],
+ }, nil
+}
+
+// String implements the stringer interface.
+func (u URL) String() string {
+ if u.Scheme != "" {
+ return fmt.Sprintf("%s://%s", u.Scheme, u.Path)
+ }
+ return u.Path
+}
+
+// MarshalJSON implements the json.Marshaller interface.
+func (u URL) MarshalJSON() ([]byte, error) {
+ return json.Marshal(u.String())
+}
+
+// Cmp compares x and y and returns:
+//
+// -1 if x < y
+// 0 if x == y
+// +1 if x > y
+//
+func (u URL) Cmp(url URL) int {
+ if u.Scheme == url.Scheme {
+ return strings.Compare(u.Path, url.Path)
+ }
+ return strings.Compare(u.Scheme, url.Scheme)
+}
diff --git a/accounts/usbwallet/ledger_hub.go b/accounts/usbwallet/ledger_hub.go
new file mode 100644
index 000000000..ad5940cd4
--- /dev/null
+++ b/accounts/usbwallet/ledger_hub.go
@@ -0,0 +1,209 @@
+// 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"
+)
+
+// LedgerScheme is the protocol scheme prefixing account and wallet URLs.
+var LedgerScheme = "ledger"
+
+// 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 := accounts.URL{Scheme: LedgerScheme, Path: fmt.Sprintf("%03d:%03d", busID>>8, busID&0xff)}
+
+ // Drop wallets in front of the next device or those that failed for some reason
+ for len(hub.wallets) > 0 && (hub.wallets[0].URL().Cmp(url) < 0 || hub.wallets[0].(*ledgerWallet).failed()) {
+ events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false})
+ hub.wallets = hub.wallets[1:]
+ }
+ // If there are no more wallets or the device is before the next, wrap new wallet
+ if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 {
+ 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 device is the same as the first wallet, keep it
+ if hub.wallets[0].URL().Cmp(url) == 0 {
+ 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_wallet.go b/accounts/usbwallet/ledger_wallet.go
new file mode 100644
index 000000000..a667f580a
--- /dev/null
+++ b/accounts/usbwallet/ledger_wallet.go
@@ -0,0 +1,945 @@
+// 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 (
+ "encoding/binary"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "math/big"
+ "sync"
+ "time"
+
+ ethereum "github.com/ethereum/go-ethereum"
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/logger"
+ "github.com/ethereum/go-ethereum/logger/glog"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/karalabe/gousb/usb"
+ "golang.org/x/net/context"
+)
+
+// Maximum time between wallet health checks to detect USB unplugs.
+const ledgerHeartbeatCycle = time.Second
+
+// Minimum time to wait between self derivation attempts, even it the user is
+// requesting accounts like crazy.
+const ledgerSelfDeriveThrottling = time.Second
+
+// ledgerOpcode is an enumeration encoding the supported Ledger opcodes.
+type ledgerOpcode byte
+
+// ledgerParam1 is an enumeration encoding the supported Ledger parameters for
+// specific opcodes. The same parameter values may be reused between opcodes.
+type ledgerParam1 byte
+
+// ledgerParam2 is an enumeration encoding the supported Ledger parameters for
+// specific opcodes. The same parameter values may be reused between opcodes.
+type ledgerParam2 byte
+
+const (
+ ledgerOpRetrieveAddress ledgerOpcode = 0x02 // Returns the public key and Ethereum address for a given BIP 32 path
+ ledgerOpSignTransaction ledgerOpcode = 0x04 // Signs an Ethereum transaction after having the user validate the parameters
+ ledgerOpGetConfiguration ledgerOpcode = 0x06 // Returns specific wallet application configuration
+
+ ledgerP1DirectlyFetchAddress ledgerParam1 = 0x00 // Return address directly from the wallet
+ ledgerP1ConfirmFetchAddress ledgerParam1 = 0x01 // Require a user confirmation before returning the address
+ ledgerP1InitTransactionData ledgerParam1 = 0x00 // First transaction data block for signing
+ ledgerP1ContTransactionData ledgerParam1 = 0x80 // Subsequent transaction data block for signing
+ ledgerP2DiscardAddressChainCode ledgerParam2 = 0x00 // Do not return the chain code along with the address
+ ledgerP2ReturnAddressChainCode ledgerParam2 = 0x01 // Require a user confirmation before returning the address
+)
+
+// errReplyInvalidHeader is the error message returned by a Ledfer data exchange
+// if the device replies with a mismatching header. This usually means the device
+// is in browser mode.
+var errReplyInvalidHeader = errors.New("invalid reply header")
+
+// ledgerWallet represents a live USB Ledger hardware wallet.
+type ledgerWallet struct {
+ 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 *accounts.URL // Textual URL uniquely identifying this wallet
+
+ 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
+
+ version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline)
+ browser bool // Flag whether the Ledger is in browser mode (reply channel mismatch)
+ accounts []accounts.Account // List of derive accounts pinned on the Ledger
+ paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations
+
+ deriveNextPath accounts.DerivationPath // Next derivation path for account auto-discovery
+ deriveNextAddr common.Address // Next derived account address for auto-discovery
+ deriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with
+ deriveReq chan chan struct{} // Channel to request a self-derivation on
+ deriveQuit chan chan error // Channel to terminate the self-deriver with
+
+ healthQuit chan chan error
+
+ // Locking a hardware wallet is a bit special. Since hardware devices are lower
+ // performing, any communication with them might take a non negligible amount of
+ // time. Worse still, waiting for user confirmation can take arbitrarily long,
+ // but exclusive communication must be upheld during. Locking the entire wallet
+ // in the mean time however would stall any parts of the system that don't want
+ // to communicate, just read some state (e.g. list the accounts).
+ //
+ // As such, a hardware wallet needs two locks to function correctly. A state
+ // lock can be used to protect the wallet's software-side internal state, which
+ // must not be held exlusively during hardware communication. A communication
+ // lock can be used to achieve exclusive access to the device itself, this one
+ // however should allow "skipping" waiting for operations that might want to
+ // use the device, but can live without too (e.g. account self-derivation).
+ //
+ // Since we have two locks, it's important to know how to properly use them:
+ // - Communication requires the `device` to not change, so obtaining the
+ // commsLock should be done after having a stateLock.
+ // - Communication must not disable read access to the wallet state, so it
+ // must only ever hold a *read* lock to stateLock.
+ commsLock chan struct{} // Mutex (buf=1) for the USB comms without keeping the state locked
+ stateLock sync.RWMutex // Protects read and write access to the wallet struct fields
+}
+
+// URL implements accounts.Wallet, returning the URL of the Ledger device.
+func (w *ledgerWallet) URL() accounts.URL {
+ return *w.url // Immutable, no need for a lock
+}
+
+// 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.stateLock.RLock() // No device communication, state lock is enough
+ defer w.stateLock.RUnlock()
+
+ if w.failure != nil {
+ return fmt.Sprintf("Failed: %v", w.failure)
+ }
+ if w.device == nil {
+ return "Closed"
+ }
+ if w.browser {
+ return "Ethereum app in browser mode"
+ }
+ if w.offline() {
+ return "Ethereum app offline"
+ }
+ return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2])
+}
+
+// offline returns whether the wallet and the Ethereum app is offline or not.
+//
+// The method assumes that the state lock is held!
+func (w *ledgerWallet) offline() bool {
+ return w.version == [3]byte{0, 0, 0}
+}
+
+// failed returns if the USB device wrapped by the wallet failed for some reason.
+// This is used by the device scanner to report failed wallets as departed.
+//
+// The method assumes that the state lock is *not* held!
+func (w *ledgerWallet) failed() bool {
+ w.stateLock.RLock() // No device communication, state lock is enough
+ defer w.stateLock.RUnlock()
+
+ return w.failure != nil
+}
+
+// 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
+// parameter is silently discarded.
+func (w *ledgerWallet) Open(passphrase string) error {
+ w.stateLock.Lock() // State lock is enough since there's no connection yet at this point
+ defer w.stateLock.Unlock()
+
+ // If the wallet was already opened, don't try to open again
+ if w.device != nil {
+ return accounts.ErrWalletAlreadyOpen
+ }
+ // 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
+ }
+ if len(devices) == 0 {
+ return accounts.ErrUnknownWallet
+ }
+ // 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"
+ }
+ if invalid != "" {
+ device.Close()
+ return fmt.Errorf("ledger wallet [%s] invalid: %s", w.url, invalid)
+ }
+ // 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.commsLock = make(chan struct{}, 1)
+ w.commsLock <- struct{}{} // Enable lock
+
+ w.paths = make(map[common.Address]accounts.DerivationPath)
+
+ w.deriveReq = make(chan chan struct{})
+ w.deriveQuit = make(chan chan error)
+ w.healthQuit = make(chan chan error)
+
+ defer func() {
+ go w.heartbeat()
+ go w.selfDerive()
+ }()
+
+ if _, err = w.ledgerDerive(accounts.DefaultBaseDerivationPath); err != nil {
+ // Ethereum app is not running or in browser mode, nothing more to do, return
+ if err == errReplyInvalidHeader {
+ w.browser = true
+ }
+ return nil
+ }
+ // Try to resolve the Ethereum app's version, will fail prior to v1.0.2
+ if w.version, err = w.ledgerVersion(); err != nil {
+ w.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1
+ }
+ return nil
+}
+
+// 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() {
+ glog.V(logger.Debug).Infof("%s health-check started", w.url.String())
+ defer glog.V(logger.Debug).Infof("%s health-check stopped", w.url.String())
+
+ // Execute heartbeat checks until termination or error
+ var (
+ errc chan error
+ err error
+ )
+ for errc == nil && err == nil {
+ // Wait until termination is requested or the heartbeat cycle arrives
+ select {
+ case errc = <-w.healthQuit:
+ // Termination requested
+ continue
+ case <-time.After(ledgerHeartbeatCycle):
+ // Heartbeat time
+ }
+ // Execute a tiny data exchange to see responsiveness
+ w.stateLock.RLock()
+ if w.device == nil {
+ // Terminated while waiting for the lock
+ w.stateLock.RUnlock()
+ continue
+ }
+ <-w.commsLock // Don't lock state while resolving version
+ _, err = w.ledgerVersion()
+ w.commsLock <- struct{}{}
+ w.stateLock.RUnlock()
+
+ if err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE {
+ w.stateLock.Lock() // Lock state to tear the wallet down
+ w.failure = err
+ w.close()
+ w.stateLock.Unlock()
+ }
+ // Ignore uninteresting errors
+ err = nil
+ }
+ // In case of error, wait for termination
+ if err != nil {
+ glog.V(logger.Debug).Infof("%s health-check failed: %v", w.url.String(), err)
+ errc = <-w.healthQuit
+ }
+ errc <- err
+}
+
+// Close implements accounts.Wallet, closing the USB connection to the Ledger.
+func (w *ledgerWallet) Close() error {
+ // Ensure the wallet was opened
+ w.stateLock.RLock()
+ hQuit, dQuit := w.healthQuit, w.deriveQuit
+ w.stateLock.RUnlock()
+
+ // Terminate the health checks
+ var herr error
+ if hQuit != nil {
+ errc := make(chan error)
+ hQuit <- errc
+ herr = <-errc // Save for later, we *must* close the USB
+ }
+ // Terminate the self-derivations
+ var derr error
+ if dQuit != nil {
+ errc := make(chan error)
+ dQuit <- errc
+ derr = <-errc // Save for later, we *must* close the USB
+ }
+ // Terminate the device connection
+ w.stateLock.Lock()
+ defer w.stateLock.Unlock()
+
+ w.healthQuit = nil
+ w.deriveQuit = nil
+ w.deriveReq = nil
+
+ if err := w.close(); err != nil {
+ return err
+ }
+ if herr != nil {
+ return herr
+ }
+ return derr
+}
+
+// close is the internal wallet closer that terminates the USB connection and
+// resets all the fields to their defaults.
+//
+// Note, close assumes the state lock is held!
+func (w *ledgerWallet) close() error {
+ // Allow duplicate closes, especially for health-check failures
+ if w.device == nil {
+ return nil
+ }
+ // Close the device, clear everything, then return
+ err := w.device.Close()
+
+ w.device, w.input, w.output = nil, nil, nil
+ w.browser, w.version = false, [3]byte{}
+ w.accounts, w.paths = nil, nil
+
+ return err
+}
+
+// Accounts implements accounts.Wallet, returning the list of accounts pinned to
+// the Ledger hardware wallet. If self-derivation was enabled, the account list
+// is periodically expanded based on current chain state.
+func (w *ledgerWallet) Accounts() []accounts.Account {
+ // Attempt self-derivation if it's running
+ reqc := make(chan struct{}, 1)
+ select {
+ case w.deriveReq <- reqc:
+ // Self-derivation request accepted, wait for it
+ <-reqc
+ default:
+ // Self-derivation offline, throttled or busy, skip
+ }
+ // Return whatever account list we ended up with
+ w.stateLock.RLock()
+ defer w.stateLock.RUnlock()
+
+ cpy := make([]accounts.Account, len(w.accounts))
+ copy(cpy, w.accounts)
+ return cpy
+}
+
+// selfDerive is an account derivation loop that upon request attempts to find
+// new non-zero accounts.
+func (w *ledgerWallet) selfDerive() {
+ glog.V(logger.Debug).Infof("%s self-derivation started", w.url.String())
+ defer glog.V(logger.Debug).Infof("%s self-derivation stopped", w.url.String())
+
+ // Execute self-derivations until termination or error
+ var (
+ reqc chan struct{}
+ errc chan error
+ err error
+ )
+ for errc == nil && err == nil {
+ // Wait until either derivation or termination is requested
+ select {
+ case errc = <-w.deriveQuit:
+ // Termination requested
+ continue
+ case reqc = <-w.deriveReq:
+ // Account discovery requested
+ }
+ // Derivation needs a chain and device access, skip if either unavailable
+ w.stateLock.RLock()
+ if w.device == nil || w.deriveChain == nil || w.offline() {
+ w.stateLock.RUnlock()
+ reqc <- struct{}{}
+ continue
+ }
+ select {
+ case <-w.commsLock:
+ default:
+ w.stateLock.RUnlock()
+ reqc <- struct{}{}
+ continue
+ }
+ // Device lock obtained, derive the next batch of accounts
+ var (
+ accs []accounts.Account
+ paths []accounts.DerivationPath
+
+ nextAddr = w.deriveNextAddr
+ nextPath = w.deriveNextPath
+
+ context = context.Background()
+ )
+ for empty := false; !empty; {
+ // Retrieve the next derived Ethereum account
+ if nextAddr == (common.Address{}) {
+ if nextAddr, err = w.ledgerDerive(nextPath); err != nil {
+ glog.V(logger.Warn).Infof("%s self-derivation failed: %v", w.url.String(), err)
+ break
+ }
+ }
+ // Check the account's status against the current chain state
+ var (
+ balance *big.Int
+ nonce uint64
+ )
+ balance, err = w.deriveChain.BalanceAt(context, nextAddr, nil)
+ if err != nil {
+ glog.V(logger.Warn).Infof("%s self-derivation balance retrieval failed: %v", w.url.String(), err)
+ break
+ }
+ nonce, err = w.deriveChain.NonceAt(context, nextAddr, nil)
+ if err != nil {
+ glog.V(logger.Warn).Infof("%s self-derivation nonce retrieval failed: %v", w.url.String(), err)
+ break
+ }
+ // If the next account is empty, stop self-derivation, but add it nonetheless
+ if balance.BitLen() == 0 && nonce == 0 {
+ empty = true
+ }
+ // We've just self-derived a new account, start tracking it locally
+ path := make(accounts.DerivationPath, len(nextPath))
+ copy(path[:], nextPath[:])
+ paths = append(paths, path)
+
+ account := accounts.Account{
+ Address: nextAddr,
+ URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)},
+ }
+ accs = append(accs, account)
+
+ // Display a log message to the user for new (or previously empty accounts)
+ if _, known := w.paths[nextAddr]; !known || (!empty && nextAddr == w.deriveNextAddr) {
+ glog.V(logger.Info).Infof("%s discovered %s (balance %22v, nonce %4d) at %s", w.url.String(), nextAddr.Hex(), balance, nonce, path)
+ }
+ // Fetch the next potential account
+ if !empty {
+ nextAddr = common.Address{}
+ nextPath[len(nextPath)-1]++
+ }
+ }
+ // Self derivation complete, release device lock
+ w.commsLock <- struct{}{}
+ w.stateLock.RUnlock()
+
+ // Insert any accounts successfully derived
+ w.stateLock.Lock()
+ for i := 0; i < len(accs); i++ {
+ if _, ok := w.paths[accs[i].Address]; !ok {
+ w.accounts = append(w.accounts, accs[i])
+ w.paths[accs[i].Address] = paths[i]
+ }
+ }
+ // Shift the self-derivation forward
+ // TODO(karalabe): don't overwrite changes from wallet.SelfDerive
+ w.deriveNextAddr = nextAddr
+ w.deriveNextPath = nextPath
+ w.stateLock.Unlock()
+
+ // Notify the user of termination and loop after a bit of time (to avoid trashing)
+ reqc <- struct{}{}
+ if err == nil {
+ select {
+ case errc = <-w.deriveQuit:
+ // Termination requested, abort
+ case <-time.After(ledgerSelfDeriveThrottling):
+ // Waited enough, willing to self-derive again
+ }
+ }
+ }
+ // In case of error, wait for termination
+ if err != nil {
+ glog.V(logger.Debug).Infof("%s self-derivation failed: %s", w.url.String(), err)
+ errc = <-w.deriveQuit
+ }
+ errc <- err
+}
+
+// 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.stateLock.RLock()
+ defer w.stateLock.RUnlock()
+
+ _, exists := w.paths[account.Address]
+ return exists
+}
+
+// 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 accounts.DerivationPath, pin bool) (accounts.Account, error) {
+ // Try to derive the actual account and update its URL if successful
+ w.stateLock.RLock() // Avoid device disappearing during derivation
+
+ if w.device == nil || w.offline() {
+ w.stateLock.RUnlock()
+ return accounts.Account{}, accounts.ErrWalletClosed
+ }
+ <-w.commsLock // Avoid concurrent hardware access
+ address, err := w.ledgerDerive(path)
+ w.commsLock <- struct{}{}
+
+ w.stateLock.RUnlock()
+
+ // If an error occurred or no pinning was requested, return
+ if err != nil {
+ return accounts.Account{}, err
+ }
+ account := accounts.Account{
+ Address: address,
+ URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)},
+ }
+ if !pin {
+ return account, nil
+ }
+ // Pinning needs to modify the state
+ w.stateLock.Lock()
+ defer w.stateLock.Unlock()
+
+ if _, ok := w.paths[address]; !ok {
+ w.accounts = append(w.accounts, account)
+ w.paths[address] = path
+ }
+ return account, nil
+}
+
+// SelfDerive implements accounts.Wallet, trying to discover accounts that the
+// user used previously (based on the chain state), but ones that he/she did not
+// explicitly pin to the wallet manually. To avoid chain head monitoring, self
+// derivation only runs during account listing (and even then throttled).
+func (w *ledgerWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {
+ w.stateLock.Lock()
+ defer w.stateLock.Unlock()
+
+ w.deriveNextPath = make(accounts.DerivationPath, len(base))
+ copy(w.deriveNextPath[:], base[:])
+
+ w.deriveNextAddr = common.Address{}
+ w.deriveChain = chain
+}
+
+// SignHash implements accounts.Wallet, however signing arbitrary data is not
+// supported for Ledger wallets, so this method will always return an error.
+func (w *ledgerWallet) SignHash(acc accounts.Account, hash []byte) ([]byte, error) {
+ 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.stateLock.RLock() // Comms have own mutex, this is for the state fields
+ defer w.stateLock.RUnlock()
+
+ // If the wallet is closed, or the Ethereum app doesn't run, abort
+ if w.device == nil || w.offline() {
+ return nil, accounts.ErrWalletClosed
+ }
+ // 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])
+ }
+ // All infos gathered and metadata checks out, request signing
+ <-w.commsLock
+ defer func() { w.commsLock <- struct{}{} }()
+
+ return w.ledgerSign(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)
+}
+
+// ledgerVersion retrieves the current version of the Ethereum wallet app running
+// on the Ledger wallet.
+//
+// The version retrieval protocol is defined as follows:
+//
+// CLA | INS | P1 | P2 | Lc | Le
+// ----+-----+----+----+----+---
+// E0 | 06 | 00 | 00 | 00 | 04
+//
+// With no input data, and the output data being:
+//
+// Description | Length
+// ---------------------------------------------------+--------
+// Flags 01: arbitrary data signature enabled by user | 1 byte
+// Application major version | 1 byte
+// Application minor version | 1 byte
+// Application patch version | 1 byte
+func (w *ledgerWallet) ledgerVersion() ([3]byte, error) {
+ // Send the request and wait for the response
+ reply, err := w.ledgerExchange(ledgerOpGetConfiguration, 0, 0, nil)
+ if err != nil {
+ return [3]byte{}, err
+ }
+ if len(reply) != 4 {
+ return [3]byte{}, errors.New("reply not of correct size")
+ }
+ // Cache the version for future reference
+ var version [3]byte
+ copy(version[:], reply[1:])
+ return version, nil
+}
+
+// ledgerDerive retrieves the currently active Ethereum address from a Ledger
+// wallet at the specified derivation path.
+//
+// The address derivation protocol is defined as follows:
+//
+// CLA | INS | P1 | P2 | Lc | Le
+// ----+-----+----+----+-----+---
+// E0 | 02 | 00 return address
+// 01 display address and confirm before returning
+// | 00: do not return the chain code
+// | 01: return the chain code
+// | var | 00
+//
+// Where the input data is:
+//
+// Description | Length
+// -------------------------------------------------+--------
+// Number of BIP 32 derivations to perform (max 10) | 1 byte
+// First derivation index (big endian) | 4 bytes
+// ... | 4 bytes
+// Last derivation index (big endian) | 4 bytes
+//
+// And the output data is:
+//
+// Description | Length
+// ------------------------+-------------------
+// Public Key length | 1 byte
+// Uncompressed Public Key | arbitrary
+// Ethereum address length | 1 byte
+// Ethereum address | 40 bytes hex ascii
+// Chain code if requested | 32 bytes
+func (w *ledgerWallet) ledgerDerive(derivationPath []uint32) (common.Address, error) {
+ // Flatten the derivation path into the Ledger request
+ 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 := w.ledgerExchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path)
+ if err != nil {
+ 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 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 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
+ var address common.Address
+ hex.Decode(address[:], hexstr)
+ return address, nil
+}
+
+// ledgerSign sends the transaction to the Ledger wallet, and waits for the user
+// to confirm or deny the transaction.
+//
+// The transaction signing protocol is defined as follows:
+//
+// CLA | INS | P1 | P2 | Lc | Le
+// ----+-----+----+----+-----+---
+// E0 | 04 | 00: first transaction data block
+// 80: subsequent transaction data block
+// | 00 | variable | variable
+//
+// Where the input for the first transaction block (first 255 bytes) is:
+//
+// Description | Length
+// -------------------------------------------------+----------
+// Number of BIP 32 derivations to perform (max 10) | 1 byte
+// First derivation index (big endian) | 4 bytes
+// ... | 4 bytes
+// Last derivation index (big endian) | 4 bytes
+// RLP transaction chunk | arbitrary
+//
+// And the input for subsequent transaction blocks (first 255 bytes) are:
+//
+// Description | Length
+// ----------------------+----------
+// RLP transaction chunk | arbitrary
+//
+// And the output data is:
+//
+// Description | Length
+// ------------+---------
+// signature V | 1 byte
+// signature R | 32 bytes
+// signature S | 32 bytes
+func (w *ledgerWallet) ledgerSign(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) { w.device.ReadTimeout = old }(w.device.ReadTimeout)
+ w.device.ReadTimeout = time.Hour * 24 * 30 // Timeout requires a Ledger power cycle, only if you must
+
+ // Flatten the derivation path into the Ledger request
+ 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
+ var (
+ txrlp []byte
+ err error
+ )
+ if chainID == nil {
+ if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data()}); err != nil {
+ return nil, err
+ }
+ } else {
+ if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), chainID, big.NewInt(0), big.NewInt(0)}); err != nil {
+ return nil, err
+ }
+ }
+ payload := append(path, txrlp...)
+
+ // Send the request and wait for the response
+ var (
+ op = ledgerP1InitTransactionData
+ reply []byte
+ )
+ for len(payload) > 0 {
+ // Calculate the size of the next data chunk
+ chunk := 255
+ if chunk > len(payload) {
+ chunk = len(payload)
+ }
+ // Send the chunk over, ensuring it's processed correctly
+ reply, err = w.ledgerExchange(ledgerOpSignTransaction, op, 0, payload[:chunk])
+ if err != nil {
+ return nil, err
+ }
+ // Shift the payload and ensure subsequent chunks are marked as such
+ payload = payload[chunk:]
+ op = ledgerP1ContTransactionData
+ }
+ // Extract the Ethereum signature and do a sanity validation
+ if len(reply) != 65 {
+ return nil, errors.New("reply lacks signature")
+ }
+ signature := append(reply[1:], reply[0])
+
+ // Create the correct signer and signature transform based on the chain ID
+ var signer types.Signer
+ if chainID == nil {
+ signer = new(types.HomesteadSigner)
+ } else {
+ signer = types.NewEIP155Signer(chainID)
+ signature[64] = signature[64] - byte(chainID.Uint64()*2+35)
+ }
+ // Inject the final signature into the transaction and sanity check the sender
+ signed, err := tx.WithSignature(signer, signature)
+ if err != nil {
+ return nil, err
+ }
+ sender, err := types.Sender(signer, signed)
+ if err != nil {
+ return nil, err
+ }
+ if sender != address {
+ return nil, fmt.Errorf("signer mismatch: expected %s, got %s", address.Hex(), sender.Hex())
+ }
+ return signed, nil
+}
+
+// ledgerExchange performs a data exchange with the Ledger wallet, sending it a
+// message and retrieving the response.
+//
+// The common transport header is defined as follows:
+//
+// Description | Length
+// --------------------------------------+----------
+// Communication channel ID (big endian) | 2 bytes
+// Command tag | 1 byte
+// Packet sequence index (big endian) | 2 bytes
+// Payload | arbitrary
+//
+// The Communication channel ID allows commands multiplexing over the same
+// physical link. It is not used for the time being, and should be set to 0101
+// to avoid compatibility issues with implementations ignoring a leading 00 byte.
+//
+// The Command tag describes the message content. Use TAG_APDU (0x05) for standard
+// APDU payloads, or TAG_PING (0x02) for a simple link test.
+//
+// The Packet sequence index describes the current sequence for fragmented payloads.
+// The first fragment index is 0x00.
+//
+// APDU Command payloads are encoded as follows:
+//
+// Description | Length
+// -----------------------------------
+// APDU length (big endian) | 2 bytes
+// APDU CLA | 1 byte
+// APDU INS | 1 byte
+// APDU P1 | 1 byte
+// APDU P2 | 1 byte
+// APDU length | 1 byte
+// Optional APDU data | arbitrary
+func (w *ledgerWallet) ledgerExchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) {
+ // Construct the message payload, possibly split into multiple chunks
+ apdu := make([]byte, 2, 7+len(data))
+
+ binary.BigEndian.PutUint16(apdu, uint16(5+len(data)))
+ apdu = append(apdu, []byte{0xe0, byte(opcode), byte(p1), byte(p2), byte(len(data))}...)
+ apdu = append(apdu, data...)
+
+ // Stream all the chunks to the device
+ header := []byte{0x01, 0x01, 0x05, 0x00, 0x00} // Channel ID and command tag appended
+ chunk := make([]byte, 64)
+ space := len(chunk) - len(header)
+
+ for i := 0; len(apdu) > 0; i++ {
+ // Construct the new message to stream
+ chunk = append(chunk[:0], header...)
+ binary.BigEndian.PutUint16(chunk[3:], uint16(i))
+
+ if len(apdu) > space {
+ chunk = append(chunk, apdu[:space]...)
+ apdu = apdu[space:]
+ } else {
+ chunk = append(chunk, apdu...)
+ apdu = nil
+ }
+ // Send over to the device
+ if glog.V(logger.Detail) {
+ glog.Infof("-> %03d.%03d: %x", w.device.Bus, w.device.Address, chunk)
+ }
+ if _, err := w.input.Write(chunk); err != nil {
+ return nil, err
+ }
+ }
+ // Stream the reply back from the wallet in 64 byte chunks
+ var reply []byte
+ chunk = chunk[:64] // Yeah, we surely have enough space
+ for {
+ // Read the next chunk from the Ledger wallet
+ if _, err := io.ReadFull(w.output, chunk); err != nil {
+ return nil, err
+ }
+ if glog.V(logger.Detail) {
+ 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 {
+ return nil, errReplyInvalidHeader
+ }
+ // If it's the first chunk, retrieve the total message length
+ var payload []byte
+
+ if chunk[3] == 0x00 && chunk[4] == 0x00 {
+ reply = make([]byte, 0, int(binary.BigEndian.Uint16(chunk[5:7])))
+ payload = chunk[7:]
+ } else {
+ payload = chunk[5:]
+ }
+ // Append to the reply and stop when filled up
+ if left := cap(reply) - len(reply); left > len(payload) {
+ reply = append(reply, payload...)
+ } else {
+ reply = append(reply, payload[:left]...)
+ break
+ }
+ }
+ return reply[:len(reply)-2], nil
+}
diff --git a/accounts/usbwallet/usbwallet.go b/accounts/usbwallet/usbwallet.go
new file mode 100644
index 000000000..3989f3d02
--- /dev/null
+++ b/accounts/usbwallet/usbwallet.go
@@ -0,0 +1,29 @@
+// Copyright 2017 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+// +build !ios
+
+// Package usbwallet implements support for USB hardware wallets.
+package usbwallet
+
+import "github.com/karalabe/gousb/usb"
+
+// deviceID is a combined vendor/product identifier to uniquely identify a USB
+// hardware device.
+type deviceID struct {
+ Vendor usb.ID // The Vendor identifer
+ Product usb.ID // The Product identifier
+}
diff --git a/accounts/usbwallet/usbwallet_ios.go b/accounts/usbwallet/usbwallet_ios.go
new file mode 100644
index 000000000..17d342903
--- /dev/null
+++ b/accounts/usbwallet/usbwallet_ios.go
@@ -0,0 +1,38 @@
+// 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 (
+ "errors"
+
+ "github.com/ethereum/go-ethereum/accounts"
+)
+
+// Here be dragons! There is no USB support on iOS.
+
+// ErrIOSNotSupported is returned for all USB hardware backends on iOS.
+var ErrIOSNotSupported = errors.New("no USB support on iOS")
+
+func NewLedgerHub() (accounts.Backend, error) {
+ return nil, ErrIOSNotSupported
+}