Commit f8f428cc authored by Péter Szilágyi's avatar Péter Szilágyi Committed by GitHub

Merge pull request #3592 from karalabe/hw-wallets

accounts: initial support for Ledger hardware wallets
parents e23e8692 e99c7881
...@@ -22,7 +22,7 @@ import ( ...@@ -22,7 +22,7 @@ import (
"io" "io"
"io/ioutil" "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/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
...@@ -35,7 +35,7 @@ func NewTransactor(keyin io.Reader, passphrase string) (*TransactOpts, error) { ...@@ -35,7 +35,7 @@ func NewTransactor(keyin io.Reader, passphrase string) (*TransactOpts, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
key, err := accounts.DecryptKey(json, passphrase) key, err := keystore.DecryptKey(json, passphrase)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
// 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
}
// 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)
}
// 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
}
// 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)
}
}
}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License // 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/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accounts package keystore
import ( import (
"bufio" "bufio"
...@@ -28,6 +28,7 @@ import ( ...@@ -28,6 +28,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/logger/glog"
...@@ -38,23 +39,23 @@ import ( ...@@ -38,23 +39,23 @@ import (
// exist yet, the code will attempt to create a watcher at most this often. // exist yet, the code will attempt to create a watcher at most this often.
const minReloadInterval = 2 * time.Second const minReloadInterval = 2 * time.Second
type accountsByFile []Account type accountsByURL []accounts.Account
func (s accountsByFile) Len() int { return len(s) } func (s accountsByURL) Len() int { return len(s) }
func (s accountsByFile) Less(i, j int) bool { return s[i].File < s[j].File } func (s accountsByURL) Less(i, j int) bool { return s[i].URL.Cmp(s[j].URL) < 0 }
func (s accountsByFile) 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
// an address for which more than one file exists. // an address for which more than one file exists.
type AmbiguousAddrError struct { type AmbiguousAddrError struct {
Addr common.Address Addr common.Address
Matches []Account Matches []accounts.Account
} }
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.File files += a.URL.Path
if i < len(err.Matches)-1 { if i < len(err.Matches)-1 {
files += ", " files += ", "
} }
...@@ -62,60 +63,63 @@ func (err *AmbiguousAddrError) Error() string { ...@@ -62,60 +63,63 @@ func (err *AmbiguousAddrError) Error() string {
return fmt.Sprintf("multiple keys match address (%s)", files) return fmt.Sprintf("multiple keys match address (%s)", files)
} }
// addrCache is a live index of all accounts in the keystore. // accountCache is a live index of all accounts in the keystore.
type addrCache struct { type accountCache struct {
keydir string keydir string
watcher *watcher watcher *watcher
mu sync.Mutex mu sync.Mutex
all accountsByFile all accountsByURL
byAddr map[common.Address][]Account byAddr map[common.Address][]accounts.Account
throttle *time.Timer throttle *time.Timer
notify chan struct{}
} }
func newAddrCache(keydir string) *addrCache { func newAccountCache(keydir string) (*accountCache, chan struct{}) {
ac := &addrCache{ ac := &accountCache{
keydir: keydir, keydir: keydir,
byAddr: make(map[common.Address][]Account), byAddr: make(map[common.Address][]accounts.Account),
notify: make(chan struct{}, 1),
} }
ac.watcher = newWatcher(ac) ac.watcher = newWatcher(ac)
return ac return ac, ac.notify
} }
func (ac *addrCache) accounts() []Account { func (ac *accountCache) accounts() []accounts.Account {
ac.maybeReload() ac.maybeReload()
ac.mu.Lock() ac.mu.Lock()
defer ac.mu.Unlock() defer ac.mu.Unlock()
cpy := make([]Account, len(ac.all)) cpy := make([]accounts.Account, len(ac.all))
copy(cpy, ac.all) copy(cpy, ac.all)
return cpy return cpy
} }
func (ac *addrCache) hasAddress(addr common.Address) bool { func (ac *accountCache) hasAddress(addr common.Address) bool {
ac.maybeReload() ac.maybeReload()
ac.mu.Lock() ac.mu.Lock()
defer ac.mu.Unlock() defer ac.mu.Unlock()
return len(ac.byAddr[addr]) > 0 return len(ac.byAddr[addr]) > 0
} }
func (ac *addrCache) add(newAccount Account) { 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].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 { if i < len(ac.all) && ac.all[i] == newAccount {
return return
} }
// newAccount is not in the cache. // 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:]) copy(ac.all[i+1:], ac.all[i:])
ac.all[i] = newAccount ac.all[i] = newAccount
ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], 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). // 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() ac.mu.Lock()
defer ac.mu.Unlock() defer ac.mu.Unlock()
ac.all = removeAccount(ac.all, removed) ac.all = removeAccount(ac.all, removed)
if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 {
delete(ac.byAddr, removed.Address) delete(ac.byAddr, removed.Address)
...@@ -124,7 +128,7 @@ func (ac *addrCache) delete(removed Account) { ...@@ -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 { for i := range slice {
if slice[i] == elem { if slice[i] == elem {
return append(slice[:i], slice[i+1:]...) return append(slice[:i], slice[i+1:]...)
...@@ -134,43 +138,44 @@ func removeAccount(slice []Account, elem Account) []Account { ...@@ -134,43 +138,44 @@ func removeAccount(slice []Account, elem Account) []Account {
} }
// find returns the cached account for address if there is a unique match. // 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. // 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. // Limit search to address candidates if possible.
matches := ac.all matches := ac.all
if (a.Address != common.Address{}) { if (a.Address != common.Address{}) {
matches = ac.byAddr[a.Address] matches = ac.byAddr[a.Address]
} }
if a.File != "" { 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.File, filepath.Separator) { if !strings.ContainsRune(a.URL.Path, filepath.Separator) {
a.File = filepath.Join(ac.keydir, a.File) a.URL.Path = filepath.Join(ac.keydir, a.URL.Path)
} }
for i := range matches { for i := range matches {
if matches[i].File == a.File { if matches[i].URL == a.URL {
return matches[i], nil return matches[i], nil
} }
} }
if (a.Address == common.Address{}) { if (a.Address == common.Address{}) {
return Account{}, ErrNoMatch return accounts.Account{}, ErrNoMatch
} }
} }
switch len(matches) { switch len(matches) {
case 1: case 1:
return matches[0], nil return matches[0], nil
case 0: case 0:
return Account{}, ErrNoMatch return accounts.Account{}, ErrNoMatch
default: 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) copy(err.Matches, matches)
return Account{}, err return accounts.Account{}, err
} }
} }
func (ac *addrCache) maybeReload() { func (ac *accountCache) maybeReload() {
ac.mu.Lock() ac.mu.Lock()
defer ac.mu.Unlock() defer ac.mu.Unlock()
if ac.watcher.running { if ac.watcher.running {
return // A watcher is running and will keep the cache up-to-date. return // A watcher is running and will keep the cache up-to-date.
} }
...@@ -188,18 +193,22 @@ func (ac *addrCache) maybeReload() { ...@@ -188,18 +193,22 @@ func (ac *addrCache) maybeReload() {
ac.throttle.Reset(minReloadInterval) ac.throttle.Reset(minReloadInterval)
} }
func (ac *addrCache) close() { func (ac *accountCache) close() {
ac.mu.Lock() ac.mu.Lock()
ac.watcher.close() ac.watcher.close()
if ac.throttle != nil { if ac.throttle != nil {
ac.throttle.Stop() ac.throttle.Stop()
} }
if ac.notify != nil {
close(ac.notify)
ac.notify = nil
}
ac.mu.Unlock() ac.mu.Unlock()
} }
// reload caches addresses of existing accounts. // reload caches addresses of existing accounts.
// Callers must hold ac.mu. // Callers must hold ac.mu.
func (ac *addrCache) reload() { func (ac *accountCache) reload() {
accounts, err := ac.scan() accounts, err := ac.scan()
if err != nil && glog.V(logger.Debug) { if err != nil && glog.V(logger.Debug) {
glog.Errorf("can't load keys: %v", err) glog.Errorf("can't load keys: %v", err)
...@@ -212,10 +221,14 @@ func (ac *addrCache) reload() { ...@@ -212,10 +221,14 @@ func (ac *addrCache) reload() {
for _, a := range accounts { for _, a := range accounts {
ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a) 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)) 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) files, err := ioutil.ReadDir(ac.keydir)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -223,7 +236,7 @@ func (ac *addrCache) scan() ([]Account, error) { ...@@ -223,7 +236,7 @@ func (ac *addrCache) scan() ([]Account, error) {
var ( var (
buf = new(bufio.Reader) buf = new(bufio.Reader)
addrs []Account addrs []accounts.Account
keyJSON struct { keyJSON struct {
Address string `json:"address"` Address string `json:"address"`
} }
...@@ -250,7 +263,7 @@ func (ac *addrCache) scan() ([]Account, error) { ...@@ -250,7 +263,7 @@ func (ac *addrCache) scan() ([]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, Account{Address: addr, File: path}) addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}})
} }
fd.Close() fd.Close()
} }
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License // 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/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accounts package keystore
import ( import (
"bytes" "bytes"
...@@ -29,6 +29,7 @@ import ( ...@@ -29,6 +29,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1" "github.com/ethereum/go-ethereum/crypto/secp256k1"
...@@ -175,13 +176,13 @@ func newKey(rand io.Reader) (*Key, error) { ...@@ -175,13 +176,13 @@ func newKey(rand io.Reader) (*Key, error) {
return newKeyFromECDSA(privateKeyECDSA), nil 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) key, err := newKey(rand)
if err != nil { if err != nil {
return nil, Account{}, err return nil, accounts.Account{}, err
} }
a := Account{Address: key.Address, File: 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.File, 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
} }
......
...@@ -23,7 +23,7 @@ The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-St ...@@ -23,7 +23,7 @@ The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-St
*/ */
package accounts package keystore
import ( import (
"bytes" "bytes"
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License // 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/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accounts package keystore
import ( import (
"io/ioutil" "io/ioutil"
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License // 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/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accounts package keystore
import ( import (
"encoding/json" "encoding/json"
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License // 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/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accounts package keystore
import ( import (
"crypto/rand" "crypto/rand"
...@@ -30,7 +30,7 @@ import ( ...@@ -30,7 +30,7 @@ import (
"github.com/ethereum/go-ethereum/crypto" "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") d, err := ioutil.TempDir("", "geth-keystore-test")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
...@@ -44,7 +44,7 @@ func tmpKeyStore(t *testing.T, encrypted bool) (dir string, ks keyStore) { ...@@ -44,7 +44,7 @@ func tmpKeyStore(t *testing.T, encrypted bool) (dir string, ks keyStore) {
} }
func TestKeyStorePlain(t *testing.T) { func TestKeyStorePlain(t *testing.T) {
dir, ks := tmpKeyStore(t, false) dir, ks := tmpKeyStoreIface(t, false)
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
pass := "" // not used but required by API pass := "" // not used but required by API
...@@ -52,7 +52,7 @@ func TestKeyStorePlain(t *testing.T) { ...@@ -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.File, pass) k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -65,7 +65,7 @@ func TestKeyStorePlain(t *testing.T) { ...@@ -65,7 +65,7 @@ func TestKeyStorePlain(t *testing.T) {
} }
func TestKeyStorePassphrase(t *testing.T) { func TestKeyStorePassphrase(t *testing.T) {
dir, ks := tmpKeyStore(t, true) dir, ks := tmpKeyStoreIface(t, true)
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
pass := "foo" pass := "foo"
...@@ -73,7 +73,7 @@ func TestKeyStorePassphrase(t *testing.T) { ...@@ -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.File, pass) k2, err := ks.GetKey(k1.Address, account.URL.Path, pass)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -86,7 +86,7 @@ func TestKeyStorePassphrase(t *testing.T) { ...@@ -86,7 +86,7 @@ func TestKeyStorePassphrase(t *testing.T) {
} }
func TestKeyStorePassphraseDecryptionFail(t *testing.T) { func TestKeyStorePassphraseDecryptionFail(t *testing.T) {
dir, ks := tmpKeyStore(t, true) dir, ks := tmpKeyStoreIface(t, true)
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
pass := "foo" pass := "foo"
...@@ -94,13 +94,13 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { ...@@ -94,13 +94,13 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) 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) t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt)
} }
} }
func TestImportPreSaleKey(t *testing.T) { func TestImportPreSaleKey(t *testing.T) {
dir, ks := tmpKeyStore(t, true) dir, ks := tmpKeyStoreIface(t, true)
defer os.RemoveAll(dir) defer os.RemoveAll(dir)
// file content of a presale key file generated with: // file content of a presale key file generated with:
...@@ -115,8 +115,8 @@ func TestImportPreSaleKey(t *testing.T) { ...@@ -115,8 +115,8 @@ 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.File, dir) { if !strings.HasPrefix(account.URL.Path, dir) {
t.Errorf("imported account file not in keystore directory: %q", account.File) t.Errorf("imported account file not in keystore directory: %q", account.URL)
} }
} }
...@@ -142,19 +142,19 @@ func TestV3_PBKDF2_1(t *testing.T) { ...@@ -142,19 +142,19 @@ func TestV3_PBKDF2_1(t *testing.T) {
func TestV3_PBKDF2_2(t *testing.T) { func TestV3_PBKDF2_2(t *testing.T) {
t.Parallel() t.Parallel()
tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
testDecryptV3(tests["test1"], t) testDecryptV3(tests["test1"], t)
} }
func TestV3_PBKDF2_3(t *testing.T) { func TestV3_PBKDF2_3(t *testing.T) {
t.Parallel() 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) testDecryptV3(tests["python_generated_test_with_odd_iv"], t)
} }
func TestV3_PBKDF2_4(t *testing.T) { func TestV3_PBKDF2_4(t *testing.T) {
t.Parallel() t.Parallel()
tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
testDecryptV3(tests["evilnonce"], t) testDecryptV3(tests["evilnonce"], t)
} }
...@@ -166,7 +166,7 @@ func TestV3_Scrypt_1(t *testing.T) { ...@@ -166,7 +166,7 @@ func TestV3_Scrypt_1(t *testing.T) {
func TestV3_Scrypt_2(t *testing.T) { func TestV3_Scrypt_2(t *testing.T) {
t.Parallel() t.Parallel()
tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t)
testDecryptV3(tests["test2"], t) testDecryptV3(tests["test2"], t)
} }
......
// 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)
}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License // 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/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package accounts package keystore
import ( import (
"crypto/aes" "crypto/aes"
...@@ -25,20 +25,21 @@ import ( ...@@ -25,20 +25,21 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/pborman/uuid" "github.com/pborman/uuid"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
) )
// creates a Key and stores that in the given KeyStore by decrypting a presale key JSON // 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) key, err := decryptPreSaleKey(keyJSON, password)
if err != nil { if err != nil {
return Account{}, nil, err return accounts.Account{}, nil, err
} }
key.Id = uuid.NewRandom() key.Id = uuid.NewRandom()
a := Account{Address: key.Address, File: 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.File, key, password) err = keyStore.StoreKey(a.URL.Path, key, password)
return a, key, err return a, key, err
} }
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
// +build darwin,!ios freebsd linux,!arm64 netbsd solaris // +build darwin,!ios freebsd linux,!arm64 netbsd solaris
package accounts package keystore
import ( import (
"time" "time"
...@@ -27,14 +27,14 @@ import ( ...@@ -27,14 +27,14 @@ import (
) )
type watcher struct { type watcher struct {
ac *addrCache ac *accountCache
starting bool starting bool
running bool running bool
ev chan notify.EventInfo ev chan notify.EventInfo
quit chan struct{} quit chan struct{}
} }
func newWatcher(ac *addrCache) *watcher { func newWatcher(ac *accountCache) *watcher {
return &watcher{ return &watcher{
ac: ac, ac: ac,
ev: make(chan notify.EventInfo, 10), ev: make(chan notify.EventInfo, 10),
......
...@@ -19,10 +19,10 @@ ...@@ -19,10 +19,10 @@
// This is the fallback implementation of directory watching. // This is the fallback implementation of directory watching.
// It is used on unsupported platforms. // It is used on unsupported platforms.
package accounts package keystore
type watcher struct{ running bool } type watcher struct{ running bool }
func newWatcher(*addrCache) *watcher { return new(watcher) } func newWatcher(*accountCache) *watcher { return new(watcher) }
func (*watcher) start() {} func (*watcher) start() {}
func (*watcher) close() {} func (*watcher) close() {}
// 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
}
// 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)
}
// 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()
}
}
This diff is collapsed.
// 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
}
// 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
}
...@@ -236,6 +236,14 @@ func goToolArch(arch string, subcmd string, args ...string) *exec.Cmd { ...@@ -236,6 +236,14 @@ func goToolArch(arch string, subcmd string, args ...string) *exec.Cmd {
gocmd := filepath.Join(runtime.GOROOT(), "bin", "go") gocmd := filepath.Join(runtime.GOROOT(), "bin", "go")
cmd := exec.Command(gocmd, subcmd) cmd := exec.Command(gocmd, subcmd)
cmd.Args = append(cmd.Args, args...) cmd.Args = append(cmd.Args, args...)
if subcmd == "build" || subcmd == "install" || subcmd == "test" {
// Go CGO has a Windows linker error prior to 1.8 (https://github.com/golang/go/issues/8756).
// Work around issue by allowing multiple definitions for <1.8 builds.
if runtime.GOOS == "windows" && runtime.Version() < "go1.8" {
cmd.Args = append(cmd.Args, []string{"-ldflags", "-extldflags -Wl,--allow-multiple-definition"}...)
}
}
cmd.Env = []string{ cmd.Env = []string{
"GO15VENDOREXPERIMENT=1", "GO15VENDOREXPERIMENT=1",
"GOPATH=" + build.GOPATH(), "GOPATH=" + build.GOPATH(),
......
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
"io/ioutil" "io/ioutil"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/console" "github.com/ethereum/go-ethereum/console"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
...@@ -180,31 +181,36 @@ nodes. ...@@ -180,31 +181,36 @@ nodes.
func accountList(ctx *cli.Context) error { func accountList(ctx *cli.Context) error {
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
for i, acct := range stack.AccountManager().Accounts() {
fmt.Printf("Account #%d: {%x} %s\n", i, acct.Address, acct.File) var index int
for _, wallet := range stack.AccountManager().Wallets() {
for _, account := range wallet.Accounts() {
fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL)
index++
}
} }
return nil return nil
} }
// tries unlocking the specified account a few times. // tries unlocking the specified account a few times.
func unlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i int, passwords []string) (accounts.Account, string) { func unlockAccount(ctx *cli.Context, ks *keystore.KeyStore, address string, i int, passwords []string) (accounts.Account, string) {
account, err := utils.MakeAddress(accman, address) account, err := utils.MakeAddress(ks, address)
if err != nil { if err != nil {
utils.Fatalf("Could not list accounts: %v", err) utils.Fatalf("Could not list accounts: %v", err)
} }
for trials := 0; trials < 3; trials++ { for trials := 0; trials < 3; trials++ {
prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3) prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3)
password := getPassPhrase(prompt, false, i, passwords) password := getPassPhrase(prompt, false, i, passwords)
err = accman.Unlock(account, password) err = ks.Unlock(account, password)
if err == nil { if err == nil {
glog.V(logger.Info).Infof("Unlocked account %x", account.Address) glog.V(logger.Info).Infof("Unlocked account %x", account.Address)
return account, password return account, password
} }
if err, ok := err.(*accounts.AmbiguousAddrError); ok { if err, ok := err.(*keystore.AmbiguousAddrError); ok {
glog.V(logger.Info).Infof("Unlocked account %x", account.Address) glog.V(logger.Info).Infof("Unlocked account %x", account.Address)
return ambiguousAddrRecovery(accman, err, password), password return ambiguousAddrRecovery(ks, err, password), password
} }
if err != accounts.ErrDecrypt { if err != keystore.ErrDecrypt {
// No need to prompt again if the error is not decryption-related. // No need to prompt again if the error is not decryption-related.
break break
} }
...@@ -244,15 +250,15 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) ...@@ -244,15 +250,15 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string)
return password return password
} }
func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrError, auth string) accounts.Account { func ambiguousAddrRecovery(ks *keystore.KeyStore, err *keystore.AmbiguousAddrError, auth string) accounts.Account {
fmt.Printf("Multiple key files exist for address %x:\n", err.Addr) fmt.Printf("Multiple key files exist for address %x:\n", err.Addr)
for _, a := range err.Matches { for _, a := range err.Matches {
fmt.Println(" ", a.File) fmt.Println(" ", a.URL)
} }
fmt.Println("Testing your passphrase against all of them...") fmt.Println("Testing your passphrase against all of them...")
var match *accounts.Account var match *accounts.Account
for _, a := range err.Matches { for _, a := range err.Matches {
if err := am.Unlock(a, auth); err == nil { if err := ks.Unlock(a, auth); err == nil {
match = &a match = &a
break break
} }
...@@ -260,11 +266,11 @@ func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrErro ...@@ -260,11 +266,11 @@ func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrErro
if match == nil { if match == nil {
utils.Fatalf("None of the listed files could be unlocked.") utils.Fatalf("None of the listed files could be unlocked.")
} }
fmt.Printf("Your passphrase unlocked %s\n", match.File) fmt.Printf("Your passphrase unlocked %s\n", match.URL)
fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:") fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:")
for _, a := range err.Matches { for _, a := range err.Matches {
if a != *match { if a != *match {
fmt.Println(" ", a.File) fmt.Println(" ", a.URL)
} }
} }
return *match return *match
...@@ -275,7 +281,8 @@ func accountCreate(ctx *cli.Context) error { ...@@ -275,7 +281,8 @@ func accountCreate(ctx *cli.Context) error {
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
account, err := stack.AccountManager().NewAccount(password) ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
account, err := ks.NewAccount(password)
if err != nil { if err != nil {
utils.Fatalf("Failed to create account: %v", err) utils.Fatalf("Failed to create account: %v", err)
} }
...@@ -290,9 +297,11 @@ func accountUpdate(ctx *cli.Context) error { ...@@ -290,9 +297,11 @@ func accountUpdate(ctx *cli.Context) error {
utils.Fatalf("No accounts specified to update") utils.Fatalf("No accounts specified to update")
} }
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
account, oldPassword := unlockAccount(ctx, stack.AccountManager(), ctx.Args().First(), 0, nil) ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
account, oldPassword := unlockAccount(ctx, ks, ctx.Args().First(), 0, nil)
newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil) newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil)
if err := stack.AccountManager().Update(account, oldPassword, newPassword); err != nil { if err := ks.Update(account, oldPassword, newPassword); err != nil {
utils.Fatalf("Could not update the account: %v", err) utils.Fatalf("Could not update the account: %v", err)
} }
return nil return nil
...@@ -310,7 +319,9 @@ func importWallet(ctx *cli.Context) error { ...@@ -310,7 +319,9 @@ func importWallet(ctx *cli.Context) error {
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx)) passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx))
acct, err := stack.AccountManager().ImportPreSaleKey(keyJson, passphrase)
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
acct, err := ks.ImportPreSaleKey(keyJson, passphrase)
if err != nil { if err != nil {
utils.Fatalf("%v", err) utils.Fatalf("%v", err)
} }
...@@ -329,7 +340,9 @@ func accountImport(ctx *cli.Context) error { ...@@ -329,7 +340,9 @@ func accountImport(ctx *cli.Context) error {
} }
stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) stack := utils.MakeNode(ctx, clientIdentifier, gitCommit)
passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
acct, err := stack.AccountManager().ImportECDSA(key, passphrase)
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
acct, err := ks.ImportECDSA(key, passphrase)
if err != nil { if err != nil {
utils.Fatalf("Could not create the account: %v", err) utils.Fatalf("Could not create the account: %v", err)
} }
......
...@@ -35,7 +35,7 @@ import ( ...@@ -35,7 +35,7 @@ import (
func tmpDatadirWithKeystore(t *testing.T) string { func tmpDatadirWithKeystore(t *testing.T) string {
datadir := tmpdir(t) datadir := tmpdir(t)
keystore := filepath.Join(datadir, "keystore") keystore := filepath.Join(datadir, "keystore")
source := filepath.Join("..", "..", "accounts", "testdata", "keystore") source := filepath.Join("..", "..", "accounts", "keystore", "testdata", "keystore")
if err := cp.CopyAll(keystore, source); err != nil { if err := cp.CopyAll(keystore, source); err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -53,15 +53,15 @@ func TestAccountList(t *testing.T) { ...@@ -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
`) `)
} }
} }
...@@ -230,7 +230,7 @@ Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase) ...@@ -230,7 +230,7 @@ Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase)
} }
func TestUnlockFlagAmbiguous(t *testing.T) { func TestUnlockFlagAmbiguous(t *testing.T) {
store := filepath.Join("..", "..", "accounts", "testdata", "dupes") store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes")
geth := runGeth(t, geth := runGeth(t,
"--keystore", store, "--nat", "none", "--nodiscover", "--dev", "--keystore", store, "--nat", "none", "--nodiscover", "--dev",
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a", "--unlock", "f466859ead1932d743d622cb74fc058882e8648a",
...@@ -247,12 +247,12 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 ...@@ -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()
...@@ -267,7 +267,7 @@ In order to avoid this warning, you need to remove the following duplicate key f ...@@ -267,7 +267,7 @@ In order to avoid this warning, you need to remove the following duplicate key f
} }
func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) { func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) {
store := filepath.Join("..", "..", "accounts", "testdata", "dupes") store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes")
geth := runGeth(t, geth := runGeth(t,
"--keystore", store, "--nat", "none", "--nodiscover", "--dev", "--keystore", store, "--nat", "none", "--nodiscover", "--dev",
"--unlock", "f466859ead1932d743d622cb74fc058882e8648a") "--unlock", "f466859ead1932d743d622cb74fc058882e8648a")
...@@ -283,8 +283,8 @@ Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 ...@@ -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.
`) `)
......
...@@ -25,11 +25,14 @@ import ( ...@@ -25,11 +25,14 @@ import (
"strings" "strings"
"time" "time"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/console" "github.com/ethereum/go-ethereum/console"
"github.com/ethereum/go-ethereum/contracts/release" "github.com/ethereum/go-ethereum/contracts/release"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/internal/debug" "github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/logger/glog"
...@@ -245,14 +248,50 @@ func startNode(ctx *cli.Context, stack *node.Node) { ...@@ -245,14 +248,50 @@ func startNode(ctx *cli.Context, stack *node.Node) {
utils.StartNode(stack) utils.StartNode(stack)
// Unlock any account specifically requested // Unlock any account specifically requested
accman := stack.AccountManager() ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
passwords := utils.MakePasswordList(ctx) passwords := utils.MakePasswordList(ctx)
accounts := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",") unlocks := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",")
for i, account := range accounts { for i, account := range unlocks {
if trimmed := strings.TrimSpace(account); trimmed != "" { if trimmed := strings.TrimSpace(account); trimmed != "" {
unlockAccount(ctx, accman, trimmed, i, passwords) unlockAccount(ctx, ks, trimmed, i, passwords)
} }
} }
// Register wallet event handlers to open and auto-derive wallets
events := make(chan accounts.WalletEvent, 16)
stack.AccountManager().Subscribe(events)
go func() {
// Create an chain state reader for self-derivation
rpcClient, err := stack.Attach()
if err != nil {
utils.Fatalf("Failed to attach to self: %v", err)
}
stateReader := ethclient.NewClient(rpcClient)
// Open and self derive any wallets already attached
for _, wallet := range stack.AccountManager().Wallets() {
if err := wallet.Open(""); err != nil {
glog.V(logger.Warn).Infof("Failed to open wallet %s: %v", wallet.URL(), err)
} else {
wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
}
}
// Listen for wallet event till termination
for event := range events {
if event.Arrive {
if err := event.Wallet.Open(""); err != nil {
glog.V(logger.Info).Infof("New wallet appeared: %s, failed to open: %s", event.Wallet.URL(), err)
} else {
glog.V(logger.Info).Infof("New wallet appeared: %s, %s", event.Wallet.URL(), event.Wallet.Status())
event.Wallet.SelfDerive(accounts.DefaultBaseDerivationPath, stateReader)
}
} else {
glog.V(logger.Info).Infof("Old wallet dropped: %s", event.Wallet.URL())
event.Wallet.Close()
}
}
}()
// Start auxiliary services if enabled // Start auxiliary services if enabled
if ctx.GlobalBool(utils.MiningEnabledFlag.Name) { if ctx.GlobalBool(utils.MiningEnabledFlag.Name) {
var ethereum *eth.Ethereum var ethereum *eth.Ethereum
......
...@@ -23,6 +23,7 @@ import ( ...@@ -23,6 +23,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
...@@ -99,17 +100,18 @@ func MakeSystemNode(privkey string, test *tests.BlockTest) (*node.Node, error) { ...@@ -99,17 +100,18 @@ func MakeSystemNode(privkey string, test *tests.BlockTest) (*node.Node, error) {
return nil, err return nil, err
} }
// Create the keystore and inject an unlocked account if requested // Create the keystore and inject an unlocked account if requested
accman := stack.AccountManager() ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
if len(privkey) > 0 { if len(privkey) > 0 {
key, err := crypto.HexToECDSA(privkey) key, err := crypto.HexToECDSA(privkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
a, err := accman.ImportECDSA(key, "") a, err := ks.ImportECDSA(key, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := accman.Unlock(a, ""); err != nil { if err := ks.Unlock(a, ""); err != nil {
return nil, err return nil, err
} }
} }
......
...@@ -26,6 +26,7 @@ import ( ...@@ -26,6 +26,7 @@ import (
"strings" "strings"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/console" "github.com/ethereum/go-ethereum/console"
...@@ -336,29 +337,36 @@ func getAccount(ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey { ...@@ -336,29 +337,36 @@ func getAccount(ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey {
return key return key
} }
// Otherwise try getting it from the keystore. // Otherwise try getting it from the keystore.
return decryptStoreAccount(stack.AccountManager(), keyid) am := stack.AccountManager()
ks := am.Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
return decryptStoreAccount(ks, keyid)
} }
func decryptStoreAccount(accman *accounts.Manager, account string) *ecdsa.PrivateKey { func decryptStoreAccount(ks *keystore.KeyStore, account string) *ecdsa.PrivateKey {
var a accounts.Account var a accounts.Account
var err error var err error
if common.IsHexAddress(account) { if common.IsHexAddress(account) {
a, err = accman.Find(accounts.Account{Address: common.HexToAddress(account)}) a, err = ks.Find(accounts.Account{Address: common.HexToAddress(account)})
} else if ix, ixerr := strconv.Atoi(account); ixerr == nil { } else if ix, ixerr := strconv.Atoi(account); ixerr == nil && ix > 0 {
a, err = accman.AccountByIndex(ix) if accounts := ks.Accounts(); len(accounts) > ix {
a = accounts[ix]
} else {
err = fmt.Errorf("index %d higher than number of accounts %d", ix, len(accounts))
}
} else { } else {
utils.Fatalf("Can't find swarm account key %s", account) utils.Fatalf("Can't find swarm account key %s", account)
} }
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.File) 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)
} }
for i := 1; i <= 3; i++ { for i := 1; i <= 3; i++ {
passphrase := promptPassphrase(fmt.Sprintf("Unlocking swarm account %s [%d/3]", a.Address.Hex(), i)) passphrase := promptPassphrase(fmt.Sprintf("Unlocking swarm account %s [%d/3]", a.Address.Hex(), i))
key, err := accounts.DecryptKey(keyjson, passphrase) key, err := keystore.DecryptKey(keyjson, passphrase)
if err == nil { if err == nil {
return key.PrivateKey return key.PrivateKey
} }
......
...@@ -30,6 +30,7 @@ import ( ...@@ -30,6 +30,7 @@ import (
"github.com/ethereum/ethash" "github.com/ethereum/ethash"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
...@@ -587,23 +588,27 @@ func MakeDatabaseHandles() int { ...@@ -587,23 +588,27 @@ func MakeDatabaseHandles() int {
// MakeAddress converts an account specified directly as a hex encoded string or // MakeAddress converts an account specified directly as a hex encoded string or
// a key index in the key store to an internal account representation. // a key index in the key store to an internal account representation.
func MakeAddress(accman *accounts.Manager, account string) (accounts.Account, error) { func MakeAddress(ks *keystore.KeyStore, account string) (accounts.Account, error) {
// If the specified account is a valid address, return it // If the specified account is a valid address, return it
if common.IsHexAddress(account) { if common.IsHexAddress(account) {
return accounts.Account{Address: common.HexToAddress(account)}, nil return accounts.Account{Address: common.HexToAddress(account)}, nil
} }
// Otherwise try to interpret the account as a keystore index // Otherwise try to interpret the account as a keystore index
index, err := strconv.Atoi(account) index, err := strconv.Atoi(account)
if err != nil { if err != nil || index < 0 {
return accounts.Account{}, fmt.Errorf("invalid account address or index %q", account) return accounts.Account{}, fmt.Errorf("invalid account address or index %q", account)
} }
return accman.AccountByIndex(index) accs := ks.Accounts()
if len(accs) <= index {
return accounts.Account{}, fmt.Errorf("index %d higher than number of accounts %d", index, len(accs))
}
return accs[index], nil
} }
// MakeEtherbase retrieves the etherbase either from the directly specified // MakeEtherbase retrieves the etherbase either from the directly specified
// command line flags or from the keystore if CLI indexed. // command line flags or from the keystore if CLI indexed.
func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address { func MakeEtherbase(ks *keystore.KeyStore, ctx *cli.Context) common.Address {
accounts := accman.Accounts() accounts := ks.Accounts()
if !ctx.GlobalIsSet(EtherbaseFlag.Name) && len(accounts) == 0 { if !ctx.GlobalIsSet(EtherbaseFlag.Name) && len(accounts) == 0 {
glog.V(logger.Error).Infoln("WARNING: No etherbase set and no accounts found as default") glog.V(logger.Error).Infoln("WARNING: No etherbase set and no accounts found as default")
return common.Address{} return common.Address{}
...@@ -613,7 +618,7 @@ func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address { ...@@ -613,7 +618,7 @@ func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address {
return common.Address{} return common.Address{}
} }
// If the specified etherbase is a valid address, return it // If the specified etherbase is a valid address, return it
account, err := MakeAddress(accman, etherbase) account, err := MakeAddress(ks, etherbase)
if err != nil { if err != nil {
Fatalf("Option %q: %v", EtherbaseFlag.Name, err) Fatalf("Option %q: %v", EtherbaseFlag.Name, err)
} }
...@@ -721,9 +726,10 @@ func RegisterEthService(ctx *cli.Context, stack *node.Node, extra []byte) { ...@@ -721,9 +726,10 @@ func RegisterEthService(ctx *cli.Context, stack *node.Node, extra []byte) {
if networks > 1 { if networks > 1 {
Fatalf("The %v flags are mutually exclusive", netFlags) Fatalf("The %v flags are mutually exclusive", netFlags)
} }
ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
ethConf := &eth.Config{ ethConf := &eth.Config{
Etherbase: MakeEtherbase(stack.AccountManager(), ctx), Etherbase: MakeEtherbase(ks, ctx),
ChainConfig: MakeChainConfig(ctx, stack), ChainConfig: MakeChainConfig(ctx, stack),
FastSync: ctx.GlobalBool(FastSyncFlag.Name), FastSync: ctx.GlobalBool(FastSyncFlag.Name),
LightMode: ctx.GlobalBool(LightModeFlag.Name), LightMode: ctx.GlobalBool(LightModeFlag.Name),
......
...@@ -361,15 +361,15 @@ func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) { ...@@ -361,15 +361,15 @@ func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) {
} }
func (s *Ethereum) Etherbase() (eb common.Address, err error) { func (s *Ethereum) Etherbase() (eb common.Address, err error) {
eb = s.etherbase if s.etherbase != (common.Address{}) {
if (eb == common.Address{}) { return s.etherbase, nil
firstAccount, err := s.AccountManager().AccountByIndex(0) }
eb = firstAccount.Address if wallets := s.AccountManager().Wallets(); len(wallets) > 0 {
if err != nil { if accounts := wallets[0].Accounts(); len(accounts) > 0 {
return eb, fmt.Errorf("etherbase address must be explicitly specified") return accounts[0].Address, nil
} }
} }
return eb, nil return common.Address{}, fmt.Errorf("etherbase address must be explicitly specified")
} }
// set in js console via admin interface or wrapper from cli flags // set in js console via admin interface or wrapper from cli flags
......
This diff is collapsed.
...@@ -24,13 +24,14 @@ package guide ...@@ -24,13 +24,14 @@ package guide
import ( import (
"io/ioutil" "io/ioutil"
"math/big"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
"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"
) )
// Tests that the account management snippets work correctly. // Tests that the account management snippets work correctly.
...@@ -42,59 +43,59 @@ func TestAccountManagement(t *testing.T) { ...@@ -42,59 +43,59 @@ func TestAccountManagement(t *testing.T) {
} }
defer os.RemoveAll(workdir) defer os.RemoveAll(workdir)
// Create an encrypted keystore manager with standard crypto parameters // Create an encrypted keystore with standard crypto parameters
am := accounts.NewManager(filepath.Join(workdir, "keystore"), accounts.StandardScryptN, accounts.StandardScryptP) ks := keystore.NewKeyStore(filepath.Join(workdir, "keystore"), keystore.StandardScryptN, keystore.StandardScryptP)
// Create a new account with the specified encryption passphrase // Create a new account with the specified encryption passphrase
newAcc, err := am.NewAccount("Creation password") newAcc, err := ks.NewAccount("Creation password")
if err != nil { if err != nil {
t.Fatalf("Failed to create new account: %v", err) t.Fatalf("Failed to create new account: %v", err)
} }
// Export the newly created account with a different passphrase. The returned // Export the newly created account with a different passphrase. The returned
// data from this method invocation is a JSON encoded, encrypted key-file // data from this method invocation is a JSON encoded, encrypted key-file
jsonAcc, err := am.Export(newAcc, "Creation password", "Export password") jsonAcc, err := ks.Export(newAcc, "Creation password", "Export password")
if err != nil { if err != nil {
t.Fatalf("Failed to export account: %v", err) t.Fatalf("Failed to export account: %v", err)
} }
// Update the passphrase on the account created above inside the local keystore // Update the passphrase on the account created above inside the local keystore
if err := am.Update(newAcc, "Creation password", "Update password"); err != nil { if err := ks.Update(newAcc, "Creation password", "Update password"); err != nil {
t.Fatalf("Failed to update account: %v", err) t.Fatalf("Failed to update account: %v", err)
} }
// Delete the account updated above from the local keystore // Delete the account updated above from the local keystore
if err := am.Delete(newAcc, "Update password"); err != nil { if err := ks.Delete(newAcc, "Update password"); err != nil {
t.Fatalf("Failed to delete account: %v", err) t.Fatalf("Failed to delete account: %v", err)
} }
// Import back the account we've exported (and then deleted) above with yet // Import back the account we've exported (and then deleted) above with yet
// again a fresh passphrase // again a fresh passphrase
if _, err := am.Import(jsonAcc, "Export password", "Import password"); err != nil { if _, err := ks.Import(jsonAcc, "Export password", "Import password"); err != nil {
t.Fatalf("Failed to import account: %v", err) t.Fatalf("Failed to import account: %v", err)
} }
// Create a new account to sign transactions with // Create a new account to sign transactions with
signer, err := am.NewAccount("Signer password") signer, err := ks.NewAccount("Signer password")
if err != nil { if err != nil {
t.Fatalf("Failed to create signer account: %v", err) t.Fatalf("Failed to create signer account: %v", err)
} }
txHash := common.HexToHash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") tx, chain := new(types.Transaction), big.NewInt(1)
// Sign a transaction with a single authorization // Sign a transaction with a single authorization
if _, err := am.SignWithPassphrase(signer, "Signer password", txHash.Bytes()); err != nil { if _, err := ks.SignTxWithPassphrase(signer, "Signer password", tx, chain); err != nil {
t.Fatalf("Failed to sign with passphrase: %v", err) t.Fatalf("Failed to sign with passphrase: %v", err)
} }
// Sign a transaction with multiple manually cancelled authorizations // Sign a transaction with multiple manually cancelled authorizations
if err := am.Unlock(signer, "Signer password"); err != nil { if err := ks.Unlock(signer, "Signer password"); err != nil {
t.Fatalf("Failed to unlock account: %v", err) t.Fatalf("Failed to unlock account: %v", err)
} }
if _, err := am.Sign(signer.Address, txHash.Bytes()); err != nil { if _, err := ks.SignTx(signer, tx, chain); err != nil {
t.Fatalf("Failed to sign with unlocked account: %v", err) t.Fatalf("Failed to sign with unlocked account: %v", err)
} }
if err := am.Lock(signer.Address); err != nil { if err := ks.Lock(signer.Address); err != nil {
t.Fatalf("Failed to lock account: %v", err) t.Fatalf("Failed to lock account: %v", err)
} }
// Sign a transaction with multiple automatically cancelled authorizations // Sign a transaction with multiple automatically cancelled authorizations
if err := am.TimedUnlock(signer, "Signer password", time.Second); err != nil { if err := ks.TimedUnlock(signer, "Signer password", time.Second); err != nil {
t.Fatalf("Failed to time unlock account: %v", err) t.Fatalf("Failed to time unlock account: %v", err)
} }
if _, err := am.Sign(signer.Address, txHash.Bytes()); err != nil { if _, err := ks.SignTx(signer, tx, chain); err != nil {
t.Fatalf("Failed to sign with time unlocked account: %v", err) t.Fatalf("Failed to sign with time unlocked account: %v", err)
} }
} }
...@@ -448,6 +448,18 @@ web3._extend({ ...@@ -448,6 +448,18 @@ web3._extend({
name: 'ecRecover', name: 'ecRecover',
call: 'personal_ecRecover', call: 'personal_ecRecover',
params: 2 params: 2
}),
new web3._extend.Method({
name: 'deriveAccount',
call: 'personal_deriveAccount',
params: 3
})
],
properties:
[
new web3._extend.Property({
name: 'listWallets',
getter: 'personal_listWallets'
}) })
] ]
}) })
......
...@@ -386,8 +386,11 @@ func (self *worker) makeCurrent(parent *types.Block, header *types.Header) error ...@@ -386,8 +386,11 @@ func (self *worker) makeCurrent(parent *types.Block, header *types.Header) error
work.family.Add(ancestor.Hash()) work.family.Add(ancestor.Hash())
work.ancestors.Add(ancestor.Hash()) work.ancestors.Add(ancestor.Hash())
} }
accounts := self.eth.AccountManager().Accounts() wallets := self.eth.AccountManager().Wallets()
accounts := make([]accounts.Account, 0, len(wallets))
for _, wallet := range wallets {
accounts = append(accounts, wallet.Accounts()...)
}
// Keep track of transactions which return errors so they can be removed // Keep track of transactions which return errors so they can be removed
work.tcount = 0 work.tcount = 0
work.ownedAccounts = accountAddressesSet(accounts) work.ownedAccounts = accountAddressesSet(accounts)
......
...@@ -24,24 +24,25 @@ import ( ...@@ -24,24 +24,25 @@ import (
"time" "time"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
) )
const ( const (
// StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB // StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB
// memory and taking approximately 1s CPU time on a modern processor. // memory and taking approximately 1s CPU time on a modern processor.
StandardScryptN = int(accounts.StandardScryptN) StandardScryptN = int(keystore.StandardScryptN)
// StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB // StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB
// memory and taking approximately 1s CPU time on a modern processor. // memory and taking approximately 1s CPU time on a modern processor.
StandardScryptP = int(accounts.StandardScryptP) StandardScryptP = int(keystore.StandardScryptP)
// LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB // LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB
// memory and taking approximately 100ms CPU time on a modern processor. // memory and taking approximately 100ms CPU time on a modern processor.
LightScryptN = int(accounts.LightScryptN) LightScryptN = int(keystore.LightScryptN)
// LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB // LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB
// memory and taking approximately 100ms CPU time on a modern processor. // memory and taking approximately 100ms CPU time on a modern processor.
LightScryptP = int(accounts.LightScryptP) LightScryptP = int(keystore.LightScryptP)
) )
// Account represents a stored key. // Account represents a stored key.
...@@ -77,59 +78,75 @@ func (a *Account) GetAddress() *Address { ...@@ -77,59 +78,75 @@ 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.File return a.account.URL.String()
} }
// AccountManager manages a key storage directory on disk. // KeyStore manages a key storage directory on disk.
type AccountManager struct{ manager *accounts.Manager } type KeyStore struct{ keystore *keystore.KeyStore }
// NewAccountManager creates a manager for the given directory. // NewKeyStore creates a keystore for the given directory.
func NewAccountManager(keydir string, scryptN, scryptP int) *AccountManager { func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore {
return &AccountManager{manager: accounts.NewManager(keydir, scryptN, scryptP)} return &KeyStore{keystore: keystore.NewKeyStore(keydir, scryptN, scryptP)}
} }
// HasAddress reports whether a key with the given address is present. // HasAddress reports whether a key with the given address is present.
func (am *AccountManager) HasAddress(address *Address) bool { func (ks *KeyStore) HasAddress(address *Address) bool {
return am.manager.HasAddress(address.address) return ks.keystore.HasAddress(address.address)
} }
// GetAccounts returns all key files present in the directory. // GetAccounts returns all key files present in the directory.
func (am *AccountManager) GetAccounts() *Accounts { func (ks *KeyStore) GetAccounts() *Accounts {
return &Accounts{am.manager.Accounts()} return &Accounts{ks.keystore.Accounts()}
} }
// DeleteAccount deletes the key matched by account if the passphrase is correct. // DeleteAccount deletes the key matched by account if the passphrase is correct.
// If a contains no filename, the address must match a unique key. // If a contains no filename, the address must match a unique key.
func (am *AccountManager) DeleteAccount(account *Account, passphrase string) error { func (ks *KeyStore) DeleteAccount(account *Account, passphrase string) error {
return am.manager.Delete(accounts.Account{ return ks.keystore.Delete(account.account, passphrase)
Address: account.account.Address,
File: account.account.File,
}, passphrase)
} }
// Sign calculates a ECDSA signature for the given hash. The produced signature // 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. // is in the [R || S || V] format where V is 0 or 1.
func (am *AccountManager) Sign(address *Address, hash []byte) (signature []byte, _ error) { func (ks *KeyStore) SignHash(address *Address, hash []byte) (signature []byte, _ error) {
return am.manager.Sign(address.address, hash) return ks.keystore.SignHash(accounts.Account{Address: address.address}, hash)
} }
// SignPassphrase signs hash if the private key matching the given address can // SignTx signs the given transaction with the requested account.
func (ks *KeyStore) SignTx(account *Account, tx *Transaction, chainID *BigInt) (*Transaction, error) {
signed, err := ks.keystore.SignTx(account.account, tx.tx, chainID.bigint)
if err != nil {
return nil, err
}
return &Transaction{signed}, nil
}
// SignHashPassphrase signs hash if the private key matching the given address can
// be decrypted with the given passphrase. The produced signature is in the // be decrypted with the given passphrase. The produced signature is in the
// [R || S || V] format where V is 0 or 1. // [R || S || V] format where V is 0 or 1.
func (am *AccountManager) SignPassphrase(account *Account, passphrase string, hash []byte) (signature []byte, _ error) { func (ks *KeyStore) SignHashPassphrase(account *Account, passphrase string, hash []byte) (signature []byte, _ error) {
return am.manager.SignWithPassphrase(account.account, passphrase, hash) return ks.keystore.SignHashWithPassphrase(account.account, passphrase, hash)
}
// SignTxPassphrase signs the transaction if the private key matching the
// given address can be decrypted with the given passphrase.
func (ks *KeyStore) SignTxPassphrase(account *Account, passphrase string, tx *Transaction, chainID *BigInt) (*Transaction, error) {
signed, err := ks.keystore.SignTxWithPassphrase(account.account, passphrase, tx.tx, chainID.bigint)
if err != nil {
return nil, err
}
return &Transaction{signed}, nil
} }
// Unlock unlocks the given account indefinitely. // Unlock unlocks the given account indefinitely.
func (am *AccountManager) Unlock(account *Account, passphrase string) error { func (ks *KeyStore) Unlock(account *Account, passphrase string) error {
return am.manager.TimedUnlock(account.account, passphrase, 0) return ks.keystore.TimedUnlock(account.account, passphrase, 0)
} }
// Lock removes the private key with the given address from memory. // Lock removes the private key with the given address from memory.
func (am *AccountManager) Lock(address *Address) error { func (ks *KeyStore) Lock(address *Address) error {
return am.manager.Lock(address.address) return ks.keystore.Lock(address.address)
} }
// TimedUnlock unlocks the given account with the passphrase. The account stays // TimedUnlock unlocks the given account with the passphrase. The account stays
...@@ -139,14 +156,14 @@ func (am *AccountManager) Lock(address *Address) error { ...@@ -139,14 +156,14 @@ func (am *AccountManager) Lock(address *Address) error {
// If the account address is already unlocked for a duration, TimedUnlock extends or // If the account address is already unlocked for a duration, TimedUnlock extends or
// shortens the active unlock timeout. If the address was previously unlocked // shortens the active unlock timeout. If the address was previously unlocked
// indefinitely the timeout is not altered. // indefinitely the timeout is not altered.
func (am *AccountManager) TimedUnlock(account *Account, passphrase string, timeout int64) error { func (ks *KeyStore) TimedUnlock(account *Account, passphrase string, timeout int64) error {
return am.manager.TimedUnlock(account.account, passphrase, time.Duration(timeout)) return ks.keystore.TimedUnlock(account.account, passphrase, time.Duration(timeout))
} }
// NewAccount generates a new key and stores it into the key directory, // NewAccount generates a new key and stores it into the key directory,
// encrypting it with the passphrase. // encrypting it with the passphrase.
func (am *AccountManager) NewAccount(passphrase string) (*Account, error) { func (ks *KeyStore) NewAccount(passphrase string) (*Account, error) {
account, err := am.manager.NewAccount(passphrase) account, err := ks.keystore.NewAccount(passphrase)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -154,13 +171,13 @@ func (am *AccountManager) NewAccount(passphrase string) (*Account, error) { ...@@ -154,13 +171,13 @@ func (am *AccountManager) NewAccount(passphrase string) (*Account, error) {
} }
// ExportKey exports as a JSON key, encrypted with newPassphrase. // ExportKey exports as a JSON key, encrypted with newPassphrase.
func (am *AccountManager) ExportKey(account *Account, passphrase, newPassphrase string) (key []byte, _ error) { func (ks *KeyStore) ExportKey(account *Account, passphrase, newPassphrase string) (key []byte, _ error) {
return am.manager.Export(account.account, passphrase, newPassphrase) return ks.keystore.Export(account.account, passphrase, newPassphrase)
} }
// ImportKey stores the given encrypted JSON key into the key directory. // ImportKey stores the given encrypted JSON key into the key directory.
func (am *AccountManager) ImportKey(keyJSON []byte, passphrase, newPassphrase string) (account *Account, _ error) { func (ks *KeyStore) ImportKey(keyJSON []byte, passphrase, newPassphrase string) (account *Account, _ error) {
acc, err := am.manager.Import(keyJSON, passphrase, newPassphrase) acc, err := ks.keystore.Import(keyJSON, passphrase, newPassphrase)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -168,14 +185,14 @@ func (am *AccountManager) ImportKey(keyJSON []byte, passphrase, newPassphrase st ...@@ -168,14 +185,14 @@ func (am *AccountManager) ImportKey(keyJSON []byte, passphrase, newPassphrase st
} }
// UpdateAccount changes the passphrase of an existing account. // UpdateAccount changes the passphrase of an existing account.
func (am *AccountManager) UpdateAccount(account *Account, passphrase, newPassphrase string) error { func (ks *KeyStore) UpdateAccount(account *Account, passphrase, newPassphrase string) error {
return am.manager.Update(account.account, passphrase, newPassphrase) return ks.keystore.Update(account.account, passphrase, newPassphrase)
} }
// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores // 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. // a key file in the key directory. The key file is encrypted with the same passphrase.
func (am *AccountManager) ImportPreSaleKey(keyJSON []byte, passphrase string) (ccount *Account, _ error) { func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (ccount *Account, _ error) {
account, err := am.manager.ImportPreSaleKey(keyJSON, passphrase) account, err := ks.keystore.ImportPreSaleKey(keyJSON, passphrase)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -43,42 +43,46 @@ public class AndroidTest extends InstrumentationTestCase { ...@@ -43,42 +43,46 @@ public class AndroidTest extends InstrumentationTestCase {
public AndroidTest() {} public AndroidTest() {}
public void testAccountManagement() { public void testAccountManagement() {
// Create an encrypted keystore manager with light crypto parameters. // Create an encrypted keystore with light crypto parameters.
AccountManager am = new AccountManager(getInstrumentation().getContext().getFilesDir() + "/keystore", Geth.LightScryptN, Geth.LightScryptP); KeyStore ks = new KeyStore(getInstrumentation().getContext().getFilesDir() + "/keystore", Geth.LightScryptN, Geth.LightScryptP);
try { try {
// Create a new account with the specified encryption passphrase. // Create a new account with the specified encryption passphrase.
Account newAcc = am.newAccount("Creation password"); Account newAcc = ks.newAccount("Creation password");
// Export the newly created account with a different passphrase. The returned // Export the newly created account with a different passphrase. The returned
// data from this method invocation is a JSON encoded, encrypted key-file. // data from this method invocation is a JSON encoded, encrypted key-file.
byte[] jsonAcc = am.exportKey(newAcc, "Creation password", "Export password"); byte[] jsonAcc = ks.exportKey(newAcc, "Creation password", "Export password");
// Update the passphrase on the account created above inside the local keystore. // Update the passphrase on the account created above inside the local keystore.
am.updateAccount(newAcc, "Creation password", "Update password"); ks.updateAccount(newAcc, "Creation password", "Update password");
// Delete the account updated above from the local keystore. // Delete the account updated above from the local keystore.
am.deleteAccount(newAcc, "Update password"); ks.deleteAccount(newAcc, "Update password");
// Import back the account we've exported (and then deleted) above with yet // Import back the account we've exported (and then deleted) above with yet
// again a fresh passphrase. // again a fresh passphrase.
Account impAcc = am.importKey(jsonAcc, "Export password", "Import password"); Account impAcc = ks.importKey(jsonAcc, "Export password", "Import password");
// Create a new account to sign transactions with // Create a new account to sign transactions with
Account signer = am.newAccount("Signer password"); Account signer = ks.newAccount("Signer password");
Hash txHash = new Hash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
Transaction tx = new Transaction(
1, new Address("0x0000000000000000000000000000000000000000"),
new BigInt(0), new BigInt(0), new BigInt(1), null); // Random empty transaction
BigInt chain = new BigInt(1); // Chain identifier of the main net
// Sign a transaction with a single authorization // Sign a transaction with a single authorization
byte[] signature = am.signPassphrase(signer, "Signer password", txHash.getBytes()); Transaction signed = ks.signTxPassphrase(signer, "Signer password", tx, chain);
// Sign a transaction with multiple manually cancelled authorizations // Sign a transaction with multiple manually cancelled authorizations
am.unlock(signer, "Signer password"); ks.unlock(signer, "Signer password");
signature = am.sign(signer.getAddress(), txHash.getBytes()); signed = ks.signTx(signer, tx, chain);
am.lock(signer.getAddress()); ks.lock(signer.getAddress());
// Sign a transaction with multiple automatically cancelled authorizations // Sign a transaction with multiple automatically cancelled authorizations
am.timedUnlock(signer, "Signer password", 1000000000); ks.timedUnlock(signer, "Signer password", 1000000000);
signature = am.sign(signer.getAddress(), txHash.getBytes()); signed = ks.signTx(signer, tx, chain);
} catch (Exception e) { } catch (Exception e) {
fail(e.toString()); fail(e.toString());
} }
......
...@@ -132,6 +132,11 @@ type Transaction struct { ...@@ -132,6 +132,11 @@ type Transaction struct {
tx *types.Transaction tx *types.Transaction
} }
// NewTransaction creates a new transaction with the given properties.
func NewTransaction(nonce int64, to *Address, amount, gasLimit, gasPrice *BigInt, data []byte) *Transaction {
return &Transaction{types.NewTransaction(uint64(nonce), to.address, amount.bigint, gasLimit.bigint, gasPrice.bigint, data)}
}
func (tx *Transaction) GetData() []byte { return tx.tx.Data() } func (tx *Transaction) GetData() []byte { return tx.tx.Data() }
func (tx *Transaction) GetGas() int64 { return tx.tx.Gas().Int64() } func (tx *Transaction) GetGas() int64 { return tx.tx.Gas().Int64() }
func (tx *Transaction) GetGasPrice() *BigInt { return &BigInt{tx.tx.GasPrice()} } func (tx *Transaction) GetGasPrice() *BigInt { return &BigInt{tx.tx.GasPrice()} }
......
This diff is collapsed.
...@@ -13,6 +13,7 @@ github.com/golang/snappy d9eb7a3 ...@@ -13,6 +13,7 @@ github.com/golang/snappy d9eb7a3
github.com/hashicorp/golang-lru 0a025b7 github.com/hashicorp/golang-lru 0a025b7
github.com/huin/goupnp 679507a github.com/huin/goupnp 679507a
github.com/jackpal/go-nat-pmp v1.0.1-4-g1fa385a github.com/jackpal/go-nat-pmp v1.0.1-4-g1fa385a
github.com/karalabe/gousb ffa821b
github.com/maruel/panicparse ad66119 github.com/maruel/panicparse ad66119
github.com/mattn/go-colorable v0.0.6-9-gd228849 github.com/mattn/go-colorable v0.0.6-9-gd228849
github.com/mattn/go-isatty 30a891c github.com/mattn/go-isatty 30a891c
......
language: go
matrix:
include:
- os: linux
dist: trusty
go: 1.7.4
- os: osx
go: 1.7.4
script:
- go test -v -test.run='BCD|Parse' ./...
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
#ifndef LIBUSB_POLL_POSIX_H
#define LIBUSB_POLL_POSIX_H
#define usbi_write write
#define usbi_read read
#define usbi_close close
#define usbi_poll poll
int usbi_pipe(int pipefd[2]);
#endif /* LIBUSB_POLL_POSIX_H */
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment