aboutsummaryrefslogtreecommitdiffstats
path: root/accounts/keystore/keystore.go
diff options
context:
space:
mode:
Diffstat (limited to 'accounts/keystore/keystore.go')
-rw-r--r--accounts/keystore/keystore.go178
1 files changed, 154 insertions, 24 deletions
diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go
index d125f7d62..ce4e87ce9 100644
--- a/accounts/keystore/keystore.go
+++ b/accounts/keystore/keystore.go
@@ -37,23 +37,34 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/event"
)
var (
- ErrNeedPasswordOrUnlock = accounts.NewAuthNeededError("password or unlock")
- ErrNoMatch = errors.New("no key for given address or file")
- ErrDecrypt = errors.New("could not decrypt key with given passphrase")
+ ErrLocked = accounts.NewAuthNeededError("password or unlock")
+ ErrNoMatch = errors.New("no key for given address or file")
+ ErrDecrypt = errors.New("could not decrypt key with given passphrase")
)
-// BackendType can be used to query the account manager for encrypted keystores.
-var BackendType = reflect.TypeOf(new(KeyStore))
+// KeyStoreType is the reflect type of a keystore backend.
+var KeyStoreType = reflect.TypeOf(&KeyStore{})
+
+// Maximum time between wallet refreshes (if filesystem notifications don't work).
+const walletRefreshCycle = 3 * time.Second
// KeyStore manages a key storage directory on disk.
type KeyStore struct {
- cache *addressCache
- keyStore keyStore
- mu sync.RWMutex
- unlocked map[common.Address]*unlocked
+ storage keyStore // Storage backend, might be cleartext or encrypted
+ cache *accountCache // In-memory account cache over the filesystem storage
+ changes chan struct{} // Channel receiving change notifications from the cache
+ unlocked map[common.Address]*unlocked // Currently unlocked account (decrypted private keys)
+
+ wallets []accounts.Wallet // Wallet wrappers around the individual key files
+ updateFeed event.Feed // Event feed to notify wallet additions/removals
+ updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
+ updating bool // Whether the event notification loop is running
+
+ mu sync.RWMutex
}
type unlocked struct {
@@ -64,7 +75,7 @@ type unlocked struct {
// NewKeyStore creates a keystore for the given directory.
func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
keydir, _ = filepath.Abs(keydir)
- ks := &KeyStore{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}}
+ ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP}}
ks.init(keydir)
return ks
}
@@ -73,20 +84,136 @@ func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
// Deprecated: Use NewKeyStore.
func NewPlaintextKeyStore(keydir string) *KeyStore {
keydir, _ = filepath.Abs(keydir)
- ks := &KeyStore{keyStore: &keyStorePlain{keydir}}
+ ks := &KeyStore{storage: &keyStorePlain{keydir}}
ks.init(keydir)
return ks
}
func (ks *KeyStore) init(keydir string) {
+ // Lock the mutex since the account cache might call back with events
+ ks.mu.Lock()
+ defer ks.mu.Unlock()
+
+ // Initialize the set of unlocked keys and the account cache
ks.unlocked = make(map[common.Address]*unlocked)
- ks.cache = newAddrCache(keydir)
+ ks.cache, ks.changes = newAccountCache(keydir)
+
// TODO: In order for this finalizer to work, there must be no references
// to ks. addressCache doesn't keep a reference but unlocked keys do,
// so the finalizer will not trigger until all timed unlocks have expired.
runtime.SetFinalizer(ks, func(m *KeyStore) {
m.cache.close()
})
+ // Create the initial list of wallets from the cache
+ accs := ks.cache.accounts()
+ ks.wallets = make([]accounts.Wallet, len(accs))
+ for i := 0; i < len(accs); i++ {
+ ks.wallets[i] = &keystoreWallet{account: accs[i], keystore: ks}
+ }
+}
+
+// Wallets implements accounts.Backend, returning all single-key wallets from the
+// keystore directory.
+func (ks *KeyStore) Wallets() []accounts.Wallet {
+ // Make sure the list of wallets is in sync with the account cache
+ ks.refreshWallets()
+
+ ks.mu.RLock()
+ defer ks.mu.RUnlock()
+
+ cpy := make([]accounts.Wallet, len(ks.wallets))
+ copy(cpy, ks.wallets)
+ return cpy
+}
+
+// refreshWallets retrieves the current account list and based on that does any
+// necessary wallet refreshes.
+func (ks *KeyStore) refreshWallets() {
+ // Retrieve the current list of accounts
+ accs := ks.cache.accounts()
+
+ // Transform the current list of wallets into the new one
+ ks.mu.Lock()
+
+ wallets := make([]accounts.Wallet, 0, len(accs))
+ events := []accounts.WalletEvent{}
+
+ for _, account := range accs {
+ // Drop wallets while they were in front of the next account
+ for len(ks.wallets) > 0 && ks.wallets[0].URL() < account.URL {
+ events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Arrive: false})
+ ks.wallets = ks.wallets[1:]
+ }
+ // If there are no more wallets or the account is before the next, wrap new wallet
+ if len(ks.wallets) == 0 || ks.wallets[0].URL() > account.URL {
+ wallet := &keystoreWallet{account: account, keystore: ks}
+
+ events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true})
+ wallets = append(wallets, wallet)
+ continue
+ }
+ // If the account is the same as the first wallet, keep it
+ if ks.wallets[0].Accounts()[0] == account {
+ wallets = append(wallets, ks.wallets[0])
+ ks.wallets = ks.wallets[1:]
+ continue
+ }
+ }
+ // Drop any leftover wallets and set the new batch
+ for _, wallet := range ks.wallets {
+ events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false})
+ }
+ ks.wallets = wallets
+ ks.mu.Unlock()
+
+ // Fire all wallet events and return
+ for _, event := range events {
+ ks.updateFeed.Send(event)
+ }
+}
+
+// Subscribe implements accounts.Backend, creating an async subscription to
+// receive notifications on the addition or removal of keystore wallets.
+func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
+ // We need the mutex to reliably start/stop the update loop
+ ks.mu.Lock()
+ defer ks.mu.Unlock()
+
+ // Subscribe the caller and track the subscriber count
+ sub := ks.updateScope.Track(ks.updateFeed.Subscribe(sink))
+
+ // Subscribers require an active notification loop, start it
+ if !ks.updating {
+ ks.updating = true
+ go ks.updater()
+ }
+ return sub
+}
+
+// updater is responsible for maintaining an up-to-date list of wallets stored in
+// the keystore, and for firing wallet addition/removal events. It listens for
+// account change events from the underlying account cache, and also periodically
+// forces a manual refresh (only triggers for systems where the filesystem notifier
+// is not running).
+func (ks *KeyStore) updater() {
+ for {
+ // Wait for an account update or a refresh timeout
+ select {
+ case <-ks.changes:
+ case <-time.After(walletRefreshCycle):
+ }
+ // Run the wallet refresher
+ ks.refreshWallets()
+
+ // If all our subscribers left, stop the updater
+ ks.mu.Lock()
+ if ks.updateScope.Count() == 0 {
+ ks.updating = false
+ ks.mu.Unlock()
+ return
+ }
+ ks.mu.Unlock()
+ }
}
// HasAddress reports whether a key with the given address is present.
@@ -118,6 +245,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error {
err = os.Remove(a.URL)
if err == nil {
ks.cache.delete(a)
+ ks.refreshWallets()
}
return err
}
@@ -131,7 +259,7 @@ func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) {
unlockedKey, found := ks.unlocked[a.Address]
if !found {
- return nil, ErrNeedPasswordOrUnlock
+ return nil, ErrLocked
}
// Sign the hash using plain ECDSA operations
return crypto.Sign(hash, unlockedKey.PrivateKey)
@@ -145,7 +273,7 @@ func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *b
unlockedKey, found := ks.unlocked[a.Address]
if !found {
- return nil, ErrNeedPasswordOrUnlock
+ return nil, ErrLocked
}
// Depending on the presence of the chain ID, sign with EIP155 or homestead
if chainID != nil {
@@ -221,10 +349,9 @@ func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout t
// it with a timeout would be confusing.
zeroKey(key.PrivateKey)
return nil
- } else {
- // Terminate the expire goroutine and replace it below.
- close(u.abort)
}
+ // Terminate the expire goroutine and replace it below.
+ close(u.abort)
}
if timeout > 0 {
u = &unlocked{Key: key, abort: make(chan struct{})}
@@ -250,7 +377,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A
if err != nil {
return a, nil, err
}
- key, err := ks.keyStore.GetKey(a.Address, a.URL, auth)
+ key, err := ks.storage.GetKey(a.Address, a.URL, auth)
return a, key, err
}
@@ -277,13 +404,14 @@ func (ks *KeyStore) expire(addr common.Address, u *unlocked, timeout time.Durati
// NewAccount generates a new key and stores it into the key directory,
// encrypting it with the passphrase.
func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) {
- _, account, err := storeNewKey(ks.keyStore, crand.Reader, passphrase)
+ _, account, err := storeNewKey(ks.storage, crand.Reader, passphrase)
if err != nil {
return accounts.Account{}, err
}
// Add the account to the cache immediately rather
// than waiting for file system notifications to pick it up.
ks.cache.add(account)
+ ks.refreshWallets()
return account, nil
}
@@ -294,7 +422,7 @@ func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string)
return nil, err
}
var N, P int
- if store, ok := ks.keyStore.(*keyStorePassphrase); ok {
+ if store, ok := ks.storage.(*keyStorePassphrase); ok {
N, P = store.scryptN, store.scryptP
} else {
N, P = StandardScryptN, StandardScryptP
@@ -325,11 +453,12 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco
}
func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
- a := accounts.Account{Address: key.Address, URL: ks.keyStore.JoinPath(keyFileName(key.Address))}
- if err := ks.keyStore.StoreKey(a.URL, key, passphrase); err != nil {
+ a := accounts.Account{Address: key.Address, URL: ks.storage.JoinPath(keyFileName(key.Address))}
+ if err := ks.storage.StoreKey(a.URL, key, passphrase); err != nil {
return accounts.Account{}, err
}
ks.cache.add(a)
+ ks.refreshWallets()
return a, nil
}
@@ -339,17 +468,18 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string)
if err != nil {
return err
}
- return ks.keyStore.StoreKey(a.URL, key, newPassphrase)
+ return ks.storage.StoreKey(a.URL, key, newPassphrase)
}
// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores
// a key file in the key directory. The key file is encrypted with the same passphrase.
func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) {
- a, _, err := importPreSaleKey(ks.keyStore, keyJSON, passphrase)
+ a, _, err := importPreSaleKey(ks.storage, keyJSON, passphrase)
if err != nil {
return a, err
}
ks.cache.add(a)
+ ks.refreshWallets()
return a, nil
}