accounts, cmd, internal, mobile, node: canonical account URLs

This commit is contained in:
Péter Szilágyi 2017-02-08 15:53:02 +02:00
parent fad5eb0a87
commit c5215fdd48
No known key found for this signature in database
GPG Key ID: E9AE538CEDF8293D
19 changed files with 195 additions and 116 deletions

View File

@ -29,19 +29,16 @@ import (
// by the optional URL field. // by the optional URL field.
type Account struct { type Account struct {
Address common.Address `json:"address"` // Ethereum account address derived from the key Address common.Address `json:"address"` // Ethereum account address derived from the key
URL string `json:"url"` // Optional resource locator within a backend URL URL `json:"url"` // Optional resource locator within a backend
} }
// Wallet represents a software or hardware wallet that might contain one or more // Wallet represents a software or hardware wallet that might contain one or more
// accounts (derived from the same seed). // accounts (derived from the same seed).
type Wallet interface { type Wallet interface {
// Type retrieves a textual representation of the type of the wallet.
Type() string
// URL retrieves the canonical path under which this wallet is reachable. It is // 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 // user by upper layers to define a sorting order over all wallets from multiple
// backends. // backends.
URL() string URL() URL
// Status returns a textual status to aid the user in the current state of the // Status returns a textual status to aid the user in the current state of the
// wallet. // wallet.

View File

@ -42,7 +42,7 @@ const minReloadInterval = 2 * time.Second
type accountsByURL []accounts.Account type accountsByURL []accounts.Account
func (s accountsByURL) Len() int { return len(s) } func (s accountsByURL) Len() int { return len(s) }
func (s accountsByURL) Less(i, j int) bool { return s[i].URL < s[j].URL } func (s accountsByURL) 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] } func (s accountsByURL) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// AmbiguousAddrError is returned when attempting to unlock // AmbiguousAddrError is returned when attempting to unlock
@ -55,7 +55,7 @@ type AmbiguousAddrError struct {
func (err *AmbiguousAddrError) Error() string { func (err *AmbiguousAddrError) Error() string {
files := "" files := ""
for i, a := range err.Matches { for i, a := range err.Matches {
files += a.URL files += a.URL.Path
if i < len(err.Matches)-1 { if i < len(err.Matches)-1 {
files += ", " files += ", "
} }
@ -104,7 +104,7 @@ func (ac *accountCache) add(newAccount accounts.Account) {
ac.mu.Lock() ac.mu.Lock()
defer ac.mu.Unlock() defer ac.mu.Unlock()
i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL >= newAccount.URL }) 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 { if i < len(ac.all) && ac.all[i] == newAccount {
return return
} }
@ -155,10 +155,10 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) {
if (a.Address != common.Address{}) { if (a.Address != common.Address{}) {
matches = ac.byAddr[a.Address] matches = ac.byAddr[a.Address]
} }
if a.URL != "" { if a.URL.Path != "" {
// If only the basename is specified, complete the path. // If only the basename is specified, complete the path.
if !strings.ContainsRune(a.URL, filepath.Separator) { if !strings.ContainsRune(a.URL.Path, filepath.Separator) {
a.URL = filepath.Join(ac.keydir, a.URL) a.URL.Path = filepath.Join(ac.keydir, a.URL.Path)
} }
for i := range matches { for i := range matches {
if matches[i].URL == a.URL { if matches[i].URL == a.URL {
@ -272,7 +272,7 @@ func (ac *accountCache) scan() ([]accounts.Account, error) {
case (addr == common.Address{}): case (addr == common.Address{}):
glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path) glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path)
default: default:
addrs = append(addrs, accounts.Account{Address: addr, URL: path}) addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}})
} }
fd.Close() fd.Close()
} }

View File

@ -37,15 +37,15 @@ var (
cachetestAccounts = []accounts.Account{ cachetestAccounts = []accounts.Account{
{ {
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
URL: 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"), Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
URL: filepath.Join(cachetestDir, "aaa"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")},
}, },
{ {
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
URL: filepath.Join(cachetestDir, "zzz"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")},
}, },
} }
) )
@ -63,10 +63,11 @@ func TestWatchNewFile(t *testing.T) {
// Move in the files. // Move in the files.
wantAccounts := make([]accounts.Account, len(cachetestAccounts)) wantAccounts := make([]accounts.Account, len(cachetestAccounts))
for i := range cachetestAccounts { for i := range cachetestAccounts {
a := cachetestAccounts[i] wantAccounts[i] = accounts.Account{
a.URL = filepath.Join(dir, filepath.Base(a.URL)) Address: cachetestAccounts[i].Address,
wantAccounts[i] = a URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))},
if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); err != nil { }
if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
@ -107,13 +108,13 @@ func TestWatchNoDir(t *testing.T) {
os.MkdirAll(dir, 0700) os.MkdirAll(dir, 0700)
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
file := filepath.Join(dir, "aaa") file := filepath.Join(dir, "aaa")
if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil { if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// ks should see the account. // ks should see the account.
wantAccounts := []accounts.Account{cachetestAccounts[0]} wantAccounts := []accounts.Account{cachetestAccounts[0]}
wantAccounts[0].URL = file wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file}
for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 {
list = ks.Accounts() list = ks.Accounts()
if reflect.DeepEqual(list, wantAccounts) { if reflect.DeepEqual(list, wantAccounts) {
@ -145,31 +146,31 @@ func TestCacheAddDeleteOrder(t *testing.T) {
accs := []accounts.Account{ accs := []accounts.Account{
{ {
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
URL: "-309830980", URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"},
}, },
{ {
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
URL: "ggg", URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"},
}, },
{ {
Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"),
URL: "zzzzzz-the-very-last-one.keyXXX", URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"},
}, },
{ {
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
URL: "SOMETHING.key", URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"},
}, },
{ {
Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"),
URL: "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"), Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
URL: "aaa", URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"},
}, },
{ {
Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"),
URL: "zzz", URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"},
}, },
} }
for _, a := range accs { for _, a := range accs {
@ -210,7 +211,7 @@ func TestCacheAddDeleteOrder(t *testing.T) {
for i := 0; i < len(accs); i += 2 { for i := 0; i < len(accs); i += 2 {
cache.delete(wantAccounts[i]) cache.delete(wantAccounts[i])
} }
cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"}) cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}})
select { select {
case <-notify: case <-notify:
@ -245,19 +246,19 @@ func TestCacheFind(t *testing.T) {
accs := []accounts.Account{ accs := []accounts.Account{
{ {
Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"),
URL: filepath.Join(dir, "a.key"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")},
}, },
{ {
Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"),
URL: filepath.Join(dir, "b.key"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")},
}, },
{ {
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
URL: filepath.Join(dir, "c.key"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")},
}, },
{ {
Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"),
URL: filepath.Join(dir, "c2.key"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")},
}, },
} }
for _, a := range accs { for _, a := range accs {
@ -266,7 +267,7 @@ func TestCacheFind(t *testing.T) {
nomatchAccount := accounts.Account{ nomatchAccount := accounts.Account{
Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"),
URL: filepath.Join(dir, "something"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")},
} }
tests := []struct { tests := []struct {
Query accounts.Account Query accounts.Account
@ -278,7 +279,7 @@ func TestCacheFind(t *testing.T) {
// by file // by file
{Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]},
// by basename // by basename
{Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]}, {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]},
// by file and address // by file and address
{Query: accs[0], WantResult: accs[0]}, {Query: accs[0], WantResult: accs[0]},
// ambiguous address, tie resolved by file // ambiguous address, tie resolved by file
@ -294,7 +295,7 @@ func TestCacheFind(t *testing.T) {
// no match error // no match error
{Query: nomatchAccount, WantError: ErrNoMatch}, {Query: nomatchAccount, WantError: ErrNoMatch},
{Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch},
{Query: accounts.Account{URL: filepath.Base(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}, {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch},
} }
for i, test := range tests { for i, test := range tests {

View File

@ -181,8 +181,8 @@ func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Accou
if err != nil { if err != nil {
return nil, accounts.Account{}, err return nil, accounts.Account{}, err
} }
a := accounts.Account{Address: key.Address, URL: ks.JoinPath(keyFileName(key.Address))} a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}}
if err := ks.StoreKey(a.URL, key, auth); err != nil { if err := ks.StoreKey(a.URL.Path, key, auth); err != nil {
zeroKey(key.PrivateKey) zeroKey(key.PrivateKey)
return nil, a, err return nil, a, err
} }

View File

@ -49,6 +49,9 @@ var (
// KeyStoreType is the reflect type of a keystore backend. // KeyStoreType is the reflect type of a keystore backend.
var KeyStoreType = reflect.TypeOf(&KeyStore{}) 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). // Maximum time between wallet refreshes (if filesystem notifications don't work).
const walletRefreshCycle = 3 * time.Second const walletRefreshCycle = 3 * time.Second
@ -130,22 +133,21 @@ func (ks *KeyStore) Wallets() []accounts.Wallet {
// necessary wallet refreshes. // necessary wallet refreshes.
func (ks *KeyStore) refreshWallets() { func (ks *KeyStore) refreshWallets() {
// Retrieve the current list of accounts // Retrieve the current list of accounts
ks.mu.Lock()
accs := ks.cache.accounts() accs := ks.cache.accounts()
// Transform the current list of wallets into the new one // Transform the current list of wallets into the new one
ks.mu.Lock()
wallets := make([]accounts.Wallet, 0, len(accs)) wallets := make([]accounts.Wallet, 0, len(accs))
events := []accounts.WalletEvent{} events := []accounts.WalletEvent{}
for _, account := range accs { for _, account := range accs {
// Drop wallets while they were in front of the next account // Drop wallets while they were in front of the next account
for len(ks.wallets) > 0 && ks.wallets[0].URL() < account.URL { for len(ks.wallets) > 0 && ks.wallets[0].URL().Cmp(account.URL) < 0 {
events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Arrive: false}) events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Arrive: false})
ks.wallets = ks.wallets[1:] ks.wallets = ks.wallets[1:]
} }
// If there are no more wallets or the account is before the next, wrap new wallet // 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 { if len(ks.wallets) == 0 || ks.wallets[0].URL().Cmp(account.URL) > 0 {
wallet := &keystoreWallet{account: account, keystore: ks} wallet := &keystoreWallet{account: account, keystore: ks}
events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true})
@ -242,7 +244,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error {
// The order is crucial here. The key is dropped from the // The order is crucial here. The key is dropped from the
// cache after the file is gone so that a reload happening in // cache after the file is gone so that a reload happening in
// between won't insert it into the cache again. // between won't insert it into the cache again.
err = os.Remove(a.URL) err = os.Remove(a.URL.Path)
if err == nil { if err == nil {
ks.cache.delete(a) ks.cache.delete(a)
ks.refreshWallets() ks.refreshWallets()
@ -377,7 +379,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A
if err != nil { if err != nil {
return a, nil, err return a, nil, err
} }
key, err := ks.storage.GetKey(a.Address, a.URL, auth) key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth)
return a, key, err return a, key, err
} }
@ -453,8 +455,8 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco
} }
func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
a := accounts.Account{Address: key.Address, URL: ks.storage.JoinPath(keyFileName(key.Address))} 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, key, passphrase); err != nil { if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil {
return accounts.Account{}, err return accounts.Account{}, err
} }
ks.cache.add(a) ks.cache.add(a)
@ -468,7 +470,7 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string)
if err != nil { if err != nil {
return err return err
} }
return ks.storage.StoreKey(a.URL, key, newPassphrase) return ks.storage.StoreKey(a.URL.Path, key, newPassphrase)
} }
// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores // ImportPreSaleKey decrypts the given Ethereum presale wallet and stores

View File

@ -52,7 +52,7 @@ func TestKeyStorePlain(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
k2, err := ks.GetKey(k1.Address, account.URL, pass) k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -73,7 +73,7 @@ func TestKeyStorePassphrase(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
k2, err := ks.GetKey(k1.Address, account.URL, pass) k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -94,7 +94,7 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, err = ks.GetKey(k1.Address, account.URL, "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) t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt)
} }
} }
@ -115,7 +115,7 @@ func TestImportPreSaleKey(t *testing.T) {
if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") {
t.Errorf("imported account has wrong address %x", account.Address) t.Errorf("imported account has wrong address %x", account.Address)
} }
if !strings.HasPrefix(account.URL, dir) { if !strings.HasPrefix(account.URL.Path, dir) {
t.Errorf("imported account file not in keystore directory: %q", account.URL) t.Errorf("imported account file not in keystore directory: %q", account.URL)
} }
} }

View File

@ -41,10 +41,10 @@ func TestKeyStore(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !strings.HasPrefix(a.URL, dir) { if !strings.HasPrefix(a.URL.Path, dir) {
t.Errorf("account file %s doesn't have dir prefix", a.URL) t.Errorf("account file %s doesn't have dir prefix", a.URL)
} }
stat, err := os.Stat(a.URL) stat, err := os.Stat(a.URL.Path)
if err != nil { if err != nil {
t.Fatalf("account file %s doesn't exist (%v)", a.URL, err) t.Fatalf("account file %s doesn't exist (%v)", a.URL, err)
} }
@ -60,7 +60,7 @@ func TestKeyStore(t *testing.T) {
if err := ks.Delete(a, "bar"); err != nil { if err := ks.Delete(a, "bar"); err != nil {
t.Errorf("Delete error: %v", err) t.Errorf("Delete error: %v", err)
} }
if common.FileExist(a.URL) { if common.FileExist(a.URL.Path) {
t.Errorf("account file %s should be gone after Delete", a.URL) t.Errorf("account file %s should be gone after Delete", a.URL)
} }
if ks.HasAddress(a.Address) { if ks.HasAddress(a.Address) {
@ -286,7 +286,7 @@ func TestWalletNotifications(t *testing.T) {
// Randomly add and remove account and make sure events and wallets are in sync // Randomly add and remove account and make sure events and wallets are in sync
live := make(map[common.Address]accounts.Account) live := make(map[common.Address]accounts.Account)
for i := 0; i < 1024; i++ { for i := 0; i < 256; i++ {
// Execute a creation or deletion and ensure event arrival // Execute a creation or deletion and ensure event arrival
if create := len(live) == 0 || rand.Int()%4 > 0; create { if create := len(live) == 0 || rand.Int()%4 > 0; create {
// Add a new account and ensure wallet notifications arrives // Add a new account and ensure wallet notifications arrives
@ -349,8 +349,6 @@ func TestWalletNotifications(t *testing.T) {
} }
} }
} }
// Sleep a bit to avoid same-timestamp keyfiles
time.Sleep(10 * time.Millisecond)
} }
} }

View File

@ -30,20 +30,21 @@ type keystoreWallet struct {
keystore *KeyStore // Keystore where the account originates from keystore *KeyStore // Keystore where the account originates from
} }
// Type implements accounts.Wallet, returning the textual type of the wallet.
func (w *keystoreWallet) Type() string {
return "secret-storage"
}
// URL implements accounts.Wallet, returning the URL of the account within. // URL implements accounts.Wallet, returning the URL of the account within.
func (w *keystoreWallet) URL() string { func (w *keystoreWallet) URL() accounts.URL {
return w.account.URL return w.account.URL
} }
// Status implements accounts.Wallet, always returning "open", since there is no // Status implements accounts.Wallet, always returning "open", since there is no
// concept of open/close for plain keystore accounts. // concept of open/close for plain keystore accounts.
func (w *keystoreWallet) Status() string { func (w *keystoreWallet) Status() string {
return "Open" 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 // Open implements accounts.Wallet, but is a noop for plain wallets since there
@ -63,7 +64,7 @@ func (w *keystoreWallet) Accounts() []accounts.Account {
// Contains implements accounts.Wallet, returning whether a particular account is // Contains implements accounts.Wallet, returning whether a particular account is
// or is not wrapped by this wallet instance. // or is not wrapped by this wallet instance.
func (w *keystoreWallet) Contains(account accounts.Account) bool { func (w *keystoreWallet) Contains(account accounts.Account) bool {
return account.Address == w.account.Address && (account.URL == "" || account.URL == w.account.URL) 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 // Derive implements accounts.Wallet, but is a noop for plain wallets since there
@ -81,7 +82,7 @@ func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte
if account.Address != w.account.Address { if account.Address != w.account.Address {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
} }
if account.URL != "" && account.URL != w.account.URL { if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
} }
// Account seems valid, request the keystore to sign // Account seems valid, request the keystore to sign
@ -97,7 +98,7 @@ func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction,
if account.Address != w.account.Address { if account.Address != w.account.Address {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
} }
if account.URL != "" && account.URL != w.account.URL { if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
} }
// Account seems valid, request the keystore to sign // Account seems valid, request the keystore to sign
@ -111,7 +112,7 @@ func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passph
if account.Address != w.account.Address { if account.Address != w.account.Address {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
} }
if account.URL != "" && account.URL != w.account.URL { if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
} }
// Account seems valid, request the keystore to sign // Account seems valid, request the keystore to sign
@ -125,7 +126,7 @@ func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphra
if account.Address != w.account.Address { if account.Address != w.account.Address {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
} }
if account.URL != "" && account.URL != w.account.URL { if account.URL != (accounts.URL{}) && account.URL != w.account.URL {
return nil, accounts.ErrUnknownAccount return nil, accounts.ErrUnknownAccount
} }
// Account seems valid, request the keystore to sign // Account seems valid, request the keystore to sign

View File

@ -38,8 +38,8 @@ func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accou
return accounts.Account{}, nil, err return accounts.Account{}, nil, err
} }
key.Id = uuid.NewRandom() key.Id = uuid.NewRandom()
a := accounts.Account{Address: key.Address, URL: keyStore.JoinPath(keyFileName(key.Address))} a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: keyStore.JoinPath(keyFileName(key.Address))}}
err = keyStore.StoreKey(a.URL, key, password) err = keyStore.StoreKey(a.URL.Path, key, password)
return a, key, err return a, key, err
} }

View File

@ -134,8 +134,12 @@ func (am *Manager) Wallet(url string) (Wallet, error) {
am.lock.RLock() am.lock.RLock()
defer am.lock.RUnlock() defer am.lock.RUnlock()
parsed, err := parseURL(url)
if err != nil {
return nil, err
}
for _, wallet := range am.Wallets() { for _, wallet := range am.Wallets() {
if wallet.URL() == url { if wallet.URL() == parsed {
return wallet, nil return wallet, nil
} }
} }
@ -169,7 +173,7 @@ func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription {
// The original slice is assumed to be already sorted by URL. // The original slice is assumed to be already sorted by URL.
func merge(slice []Wallet, wallets ...Wallet) []Wallet { func merge(slice []Wallet, wallets ...Wallet) []Wallet {
for _, wallet := range wallets { for _, wallet := range wallets {
n := sort.Search(len(slice), func(i int) bool { return slice[i].URL() >= wallet.URL() }) n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 })
if n == len(slice) { if n == len(slice) {
slice = append(slice, wallet) slice = append(slice, wallet)
continue continue
@ -183,7 +187,7 @@ func merge(slice []Wallet, wallets ...Wallet) []Wallet {
// cache and removes the ones specified. // cache and removes the ones specified.
func drop(slice []Wallet, wallets ...Wallet) []Wallet { func drop(slice []Wallet, wallets ...Wallet) []Wallet {
for _, wallet := range wallets { for _, wallet := range wallets {
n := sort.Search(len(slice), func(i int) bool { return slice[i].URL() >= wallet.URL() }) n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 })
if n == len(slice) { if n == len(slice) {
// Wallet not found, may happen during startup // Wallet not found, may happen during startup
continue continue

79
accounts/url.go Normal file
View File

@ -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)
}

View File

@ -32,6 +32,9 @@ import (
"github.com/karalabe/gousb/usb" "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. // ledgerDeviceIDs are the known device IDs that Ledger wallets use.
var ledgerDeviceIDs = []deviceID{ var ledgerDeviceIDs = []deviceID{
{Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue {Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue
@ -124,23 +127,24 @@ func (hub *LedgerHub) refreshWallets() {
for i := 0; i < len(devIDs); i++ { for i := 0; i < len(devIDs); i++ {
devID, busID := devIDs[i], busIDs[i] devID, busID := devIDs[i], busIDs[i]
url := fmt.Sprintf("ledger://%03d:%03d", busID>>8, busID&0xff)
url := accounts.URL{Scheme: LedgerScheme, Path: fmt.Sprintf("%03d:%03d", busID>>8, busID&0xff)}
// Drop wallets while they were in front of the next account // Drop wallets while they were in front of the next account
for len(hub.wallets) > 0 && hub.wallets[0].URL() < url { for len(hub.wallets) > 0 && hub.wallets[0].URL().Cmp(url) < 0 {
events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false}) events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false})
hub.wallets = hub.wallets[1:] hub.wallets = hub.wallets[1:]
} }
// If there are no more wallets or the account is before the next, wrap new wallet // If there are no more wallets or the account is before the next, wrap new wallet
if len(hub.wallets) == 0 || hub.wallets[0].URL() > url { if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 {
wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: url} wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: &url}
events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true})
wallets = append(wallets, wallet) wallets = append(wallets, wallet)
continue continue
} }
// If the account is the same as the first wallet, keep it // If the account is the same as the first wallet, keep it
if hub.wallets[0].URL() == url { if hub.wallets[0].URL().Cmp(url) == 0 {
wallets = append(wallets, hub.wallets[0]) wallets = append(wallets, hub.wallets[0])
hub.wallets = hub.wallets[1:] hub.wallets = hub.wallets[1:]
continue continue

View File

@ -72,10 +72,10 @@ const (
// ledgerWallet represents a live USB Ledger hardware wallet. // ledgerWallet represents a live USB Ledger hardware wallet.
type ledgerWallet struct { type ledgerWallet struct {
context *usb.Context // USB context to interface libusb through context *usb.Context // USB context to interface libusb through
hardwareID deviceID // USB identifiers to identify this device type hardwareID deviceID // USB identifiers to identify this device type
locationID uint16 // USB bus and address to identify this device instance locationID uint16 // USB bus and address to identify this device instance
url string // Textual URL uniquely identifying this wallet url *accounts.URL // Textual URL uniquely identifying this wallet
device *usb.Device // USB device advertising itself as a Ledger wallet device *usb.Device // USB device advertising itself as a Ledger wallet
input usb.Endpoint // Input endpoint to send data to this device input usb.Endpoint // Input endpoint to send data to this device
@ -90,14 +90,9 @@ type ledgerWallet struct {
lock sync.RWMutex lock sync.RWMutex
} }
// Type implements accounts.Wallet, returning the textual type of the wallet.
func (w *ledgerWallet) Type() string {
return "ledger"
}
// URL implements accounts.Wallet, returning the URL of the Ledger device. // URL implements accounts.Wallet, returning the URL of the Ledger device.
func (w *ledgerWallet) URL() string { func (w *ledgerWallet) URL() accounts.URL {
return w.url return *w.url
} }
// Status implements accounts.Wallet, always whether the Ledger is opened, closed // Status implements accounts.Wallet, always whether the Ledger is opened, closed
@ -113,9 +108,9 @@ func (w *ledgerWallet) Status() string {
return "Closed" return "Closed"
} }
if w.version == [3]byte{0, 0, 0} { if w.version == [3]byte{0, 0, 0} {
return "Ethereum app not started" return "Ethereum app offline"
} }
return fmt.Sprintf("Ethereum app v%d.%d.%d", w.version[0], w.version[1], w.version[2]) return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2])
} }
// Open implements accounts.Wallet, attempting to open a USB connection to the // Open implements accounts.Wallet, attempting to open a USB connection to the
@ -309,7 +304,7 @@ func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) {
} }
account := accounts.Account{ account := accounts.Account{
Address: address, Address: address,
URL: fmt.Sprintf("%s/%s", w.url, path), URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)},
} }
// If pinning was requested, track the account // If pinning was requested, track the account
if pin { if pin {

View File

@ -185,7 +185,7 @@ func accountList(ctx *cli.Context) error {
var index int var index int
for _, wallet := range stack.AccountManager().Wallets() { for _, wallet := range stack.AccountManager().Wallets() {
for _, account := range wallet.Accounts() { for _, account := range wallet.Accounts() {
fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, account.URL) fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL)
index++ index++
} }
} }

View File

@ -53,15 +53,15 @@ func TestAccountList(t *testing.T) {
defer geth.expectExit() defer geth.expectExit()
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
geth.expect(` geth.expect(`
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}\keystore\UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}\keystore\aaa Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}\keystore\aaa
Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}\keystore\zzz Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}\keystore\zzz
`) `)
} else { } else {
geth.expect(` geth.expect(`
Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} {{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 Account #0: {7ef5a6135f1fd6a02593eedc869c6d41d934aef8} keystore://{{.Datadir}}/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
Account #1: {f466859ead1932d743d622cb74fc058882e8648a} {{.Datadir}}/keystore/aaa Account #1: {f466859ead1932d743d622cb74fc058882e8648a} keystore://{{.Datadir}}/keystore/aaa
Account #2: {289d485d9771714cce91d3393d764e1311907acc} {{.Datadir}}/keystore/zzz Account #2: {289d485d9771714cce91d3393d764e1311907acc} keystore://{{.Datadir}}/keystore/zzz
`) `)
} }
} }
@ -247,12 +247,12 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
!! Unsupported terminal, password will be echoed. !! Unsupported terminal, password will be echoed.
Passphrase: {{.InputLine "foobar"}} Passphrase: {{.InputLine "foobar"}}
Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a: Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
{{keypath "1"}} keystore://{{keypath "1"}}
{{keypath "2"}} keystore://{{keypath "2"}}
Testing your passphrase against all of them... Testing your passphrase against all of them...
Your passphrase unlocked {{keypath "1"}} Your passphrase unlocked keystore://{{keypath "1"}}
In order to avoid this warning, you need to remove the following duplicate key files: In order to avoid this warning, you need to remove the following duplicate key files:
{{keypath "2"}} keystore://{{keypath "2"}}
`) `)
geth.expectExit() geth.expectExit()
@ -283,8 +283,8 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3
!! Unsupported terminal, password will be echoed. !! Unsupported terminal, password will be echoed.
Passphrase: {{.InputLine "wrong"}} Passphrase: {{.InputLine "wrong"}}
Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a: Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a:
{{keypath "1"}} keystore://{{keypath "1"}}
{{keypath "2"}} keystore://{{keypath "2"}}
Testing your passphrase against all of them... Testing your passphrase against all of them...
Fatal: None of the listed files could be unlocked. Fatal: None of the listed files could be unlocked.
`) `)

View File

@ -351,7 +351,7 @@ func decryptStoreAccount(ks *keystore.KeyStore, account string) *ecdsa.PrivateKe
if err != nil { if err != nil {
utils.Fatalf("Can't find swarm account key: %v", err) utils.Fatalf("Can't find swarm account key: %v", err)
} }
keyjson, err := ioutil.ReadFile(a.URL) keyjson, err := ioutil.ReadFile(a.URL.Path)
if err != nil { if err != nil {
utils.Fatalf("Can't load swarm account key: %v", err) utils.Fatalf("Can't load swarm account key: %v", err)
} }

View File

@ -228,7 +228,6 @@ func (s *PrivateAccountAPI) ListAccounts() []common.Address {
// rawWallet is a JSON representation of an accounts.Wallet interface, with its // rawWallet is a JSON representation of an accounts.Wallet interface, with its
// data contents extracted into plain fields. // data contents extracted into plain fields.
type rawWallet struct { type rawWallet struct {
Type string `json:"type"`
URL string `json:"url"` URL string `json:"url"`
Status string `json:"status"` Status string `json:"status"`
Accounts []accounts.Account `json:"accounts"` Accounts []accounts.Account `json:"accounts"`
@ -239,8 +238,7 @@ func (s *PrivateAccountAPI) ListWallets() []rawWallet {
var wallets []rawWallet var wallets []rawWallet
for _, wallet := range s.am.Wallets() { for _, wallet := range s.am.Wallets() {
wallets = append(wallets, rawWallet{ wallets = append(wallets, rawWallet{
Type: wallet.Type(), URL: wallet.URL().String(),
URL: wallet.URL(),
Status: wallet.Status(), Status: wallet.Status(),
Accounts: wallet.Accounts(), Accounts: wallet.Accounts(),
}) })

View File

@ -78,9 +78,9 @@ func (a *Account) GetAddress() *Address {
return &Address{a.account.Address} return &Address{a.account.Address}
} }
// GetFile retrieves the path of the file containing the account key. // GetURL retrieves the canonical URL of the account.
func (a *Account) GetFile() string { func (a *Account) GetURL() string {
return a.account.URL return a.account.URL.String()
} }
// KeyStore manages a key storage directory on disk. // KeyStore manages a key storage directory on disk.

View File

@ -454,12 +454,12 @@ func makeAccountManager(conf *Config) (*accounts.Manager, string, error) {
go func() { go func() {
for event := range changes { for event := range changes {
if event.Arrive { if event.Arrive {
glog.V(logger.Info).Infof("New %s wallet appeared: %s", event.Wallet.Type(), event.Wallet.URL()) glog.V(logger.Info).Infof("New wallet appeared: %s", event.Wallet.URL())
if err := event.Wallet.Open(""); err != nil { if err := event.Wallet.Open(""); err != nil {
glog.V(logger.Warn).Infof("Failed to open %s wallet %s: %v", event.Wallet.Type(), event.Wallet.URL(), err) glog.V(logger.Warn).Infof("Failed to open wallet %s: %v", event.Wallet.URL(), err)
} }
} else { } else {
glog.V(logger.Info).Infof("Old %s wallet disappeared: %s", event.Wallet.Type(), event.Wallet.URL()) glog.V(logger.Info).Infof("Old wallet disappeared: %s", event.Wallet.URL())
} }
} }
}() }()