Merge pull request #41 from farazdagi/feature/list-accounts

Updates eth.accounts and personal.listAccounts to rely on HD keys
This commit is contained in:
Roman Volosovskyi 2016-09-28 17:05:38 +03:00 committed by GitHub
commit 6d59ebf0af
9 changed files with 333 additions and 33 deletions

1
.gitignore vendored
View File

@ -24,6 +24,7 @@
# used by the Makefile
/build/_workspace/
/build/bin/
/vendor/github.com/karalabe/xgo
# travis
profile.tmp

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
@ -21,7 +22,7 @@ var (
ErrInvalidMasterKeyCreated = errors.New("can not create master extended key")
)
// createAccount creates an internal geth account
// CreateAccount creates an internal geth account
// BIP44-compatible keys are generated: CKD#1 is stored as account key, CKD#2 stored as sub-account root
// Public key of CKD#1 is returned, with CKD#2 securely encoded into account key file (to be used for
// sub-account derivations)
@ -48,7 +49,7 @@ func CreateAccount(password string) (address, pubKey, mnemonic string, err error
return address, pubKey, mnemonic, nil
}
// createChildAccount creates sub-account for an account identified by parent address.
// CreateChildAccount creates sub-account for an account identified by parent address.
// CKD#2 is used as root for master accounts (when parentAddress is "").
// Otherwise (when parentAddress != ""), child is derived directly from parent.
func CreateChildAccount(parentAddress, password string) (address, pubKey string, err error) {
@ -58,20 +59,20 @@ func CreateChildAccount(parentAddress, password string) (address, pubKey string,
return "", "", err
}
if parentAddress == "" { // by default derive from currently selected account
parentAddress = nodeManager.SelectedAddress
if parentAddress == "" && nodeManager.SelectedAccount != nil { // derive from selected account by default
parentAddress = string(nodeManager.SelectedAccount.Address.Hex())
}
if parentAddress == "" {
return "", "", ErrNoAccountSelected
}
// make sure that given password can decrypt key associated with a given parent address
account, err := utils.MakeAddress(accountManager, parentAddress)
if err != nil {
return "", "", ErrAddressToAccountMappingFailure
}
// make sure that given password can decrypt key associated with a given parent address
account, accountKey, err := accountManager.AccountDecryptedKey(account, password)
if err != nil {
return "", "", fmt.Errorf("%s: %v", ErrAccountToKeyMappingFailure.Error(), err)
@ -88,6 +89,7 @@ func CreateChildAccount(parentAddress, password string) (address, pubKey string,
return "", "", err
}
accountManager.IncSubAccountIndex(account, password)
accountKey.SubAccountIndex++
// import derived key into account keystore
address, pubKey, err = importExtendedKey(childKey, password)
@ -95,10 +97,15 @@ func CreateChildAccount(parentAddress, password string) (address, pubKey string,
return
}
// update in-memory selected account
if nodeManager.SelectedAccount != nil {
nodeManager.SelectedAccount.AccountKey = accountKey
}
return address, pubKey, nil
}
// recoverAccount re-creates master key using given details.
// RecoverAccount re-creates master key using given details.
// Once master key is re-generated, it is inserted into keystore (if not already there).
func RecoverAccount(password, mnemonic string) (address, pubKey string, err error) {
// re-create extended key (see BIP32)
@ -117,7 +124,7 @@ func RecoverAccount(password, mnemonic string) (address, pubKey string, err erro
return address, pubKey, nil
}
// selectAccount selects current account, by verifying that address has corresponding account which can be decrypted
// SelectAccount selects current account, by verifying that address has corresponding account which can be decrypted
// using provided password. Once verification is done, decrypted key is injected into Whisper (as a single identity,
// all previous identities are removed).
func SelectAccount(address, password string) error {
@ -146,13 +153,21 @@ func SelectAccount(address, password string) error {
return ErrWhisperIdentityInjectionFailure
}
// persist address for easier recovery of currently selected key (from Whisper)
nodeManager.SelectedAddress = address
// persist account key for easier recovery of currently selected key
subAccounts, err := findSubAccounts(accountKey.ExtendedKey, accountKey.SubAccountIndex)
if err != nil {
return err
}
nodeManager.SelectedAccount = &SelectedExtKey{
Address: account.Address,
AccountKey: accountKey,
SubAccounts: subAccounts,
}
return nil
}
// logout clears whisper identities
// Logout clears whisper identities
func Logout() error {
nodeManager := GetNodeManager()
whisperService, err := nodeManager.WhisperService()
@ -165,12 +180,12 @@ func Logout() error {
return fmt.Errorf("%s: %v", ErrWhisperClearIdentitiesFailure, err)
}
nodeManager.SelectedAddress = ""
nodeManager.SelectedAccount = nil
return nil
}
// unlockAccount unlocks an existing account for a certain duration and
// UnlockAccount unlocks an existing account for a certain duration and
// inject the account as a whisper identity if the account was created as
// a whisper enabled account
func UnlockAccount(address, password string, seconds int) error {
@ -201,3 +216,90 @@ func importExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, p
return
}
func onAccountsListRequest(entities []accounts.Account) []accounts.Account {
nodeManager := GetNodeManager()
if nodeManager.SelectedAccount == nil {
return []accounts.Account{}
}
refreshSelectedAccount()
filtered := make([]accounts.Account, 0)
for _, account := range entities {
// main account
if nodeManager.SelectedAccount.Address.Hex() == account.Address.Hex() {
filtered = append(filtered, account)
} else {
// sub accounts
for _, subAccount := range nodeManager.SelectedAccount.SubAccounts {
if subAccount.Address.Hex() == account.Address.Hex() {
filtered = append(filtered, account)
}
}
}
}
return filtered
}
// refreshSelectedAccount re-populates list of sub-accounts of the currently selected account (if any)
func refreshSelectedAccount() {
nodeManager := GetNodeManager()
if nodeManager.SelectedAccount == nil {
return
}
accountKey := nodeManager.SelectedAccount.AccountKey
if accountKey == nil {
return
}
// re-populate list of sub-accounts
subAccounts, err := findSubAccounts(accountKey.ExtendedKey, accountKey.SubAccountIndex)
if err != nil {
return
}
nodeManager.SelectedAccount = &SelectedExtKey{
Address: nodeManager.SelectedAccount.Address,
AccountKey: nodeManager.SelectedAccount.AccountKey,
SubAccounts: subAccounts,
}
}
// findSubAccounts traverses cached accounts and adds as a sub-accounts any
// that belong to the currently selected account.
// The extKey is CKD#2 := root of sub-accounts of the main account
func findSubAccounts(extKey *extkeys.ExtendedKey, subAccountIndex uint32) ([]accounts.Account, error) {
nodeManager := GetNodeManager()
accountManager, err := nodeManager.AccountManager()
if err != nil {
return []accounts.Account{}, err
}
subAccounts := make([]accounts.Account, 0)
if extKey.Depth == 5 { // CKD#2 level
// gather possible sub-account addresses
subAccountAddresses := make([]common.Address, 0)
for i := uint32(0); i < subAccountIndex; i++ {
childKey, err := extKey.Child(i)
if err != nil {
return []accounts.Account{}, err
}
subAccountAddresses = append(subAccountAddresses, crypto.PubkeyToAddress(childKey.ToECDSA().PublicKey))
}
// see if any of the gathered addresses actually exist in cached accounts list
for _, cachedAccount := range accountManager.Accounts() {
for _, possibleAddress := range subAccountAddresses {
if possibleAddress.Hex() == cachedAccount.Address.Hex() {
subAccounts = append(subAccounts, cachedAccount)
}
}
}
}
return subAccounts, nil
}

View File

@ -14,6 +14,116 @@ import (
"github.com/status-im/status-go/geth"
)
func TestAccountsList(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
t.Error(err)
return
}
les, err := geth.GetNodeManager().LightEthereumService()
if err != nil {
t.Errorf("expected LES service: %v", err)
}
accounts := les.StatusBackend.AccountManager().Accounts()
geth.Logout()
// make sure that we start with empty accounts list (nobody has logged in yet)
if len(accounts) != 0 {
t.Error("accounts returned, while there should be none (we haven't logged in yet)")
return
}
// create an account
address, _, _, err := geth.CreateAccount(newAccountPassword)
if err != nil {
t.Errorf("could not create account: %v", err)
return
}
// ensure that there is still no accounts returned
accounts = les.StatusBackend.AccountManager().Accounts()
if len(accounts) != 0 {
t.Error("accounts returned, while there should be none (we haven't logged in yet)")
return
}
// select account (sub-accounts will be created for this key)
err = geth.SelectAccount(address, newAccountPassword)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return
}
// at this point main account should show up
accounts = les.StatusBackend.AccountManager().Accounts()
if len(accounts) != 1 {
t.Error("exactly single account is expected (main account)")
return
}
if string(accounts[0].Address.Hex()) != "0x"+address {
t.Errorf("main account is not retured as the first key: got %s, expected %s",
accounts[0].Address.Hex(), "0x"+address)
return
}
// create sub-account 1
subAccount1, subPubKey1, err := geth.CreateChildAccount("", newAccountPassword)
if err != nil {
t.Errorf("cannot create sub-account: %v", err)
return
}
// now we expect to see both main account and sub-account 1
accounts = les.StatusBackend.AccountManager().Accounts()
if len(accounts) != 2 {
t.Error("exactly 2 accounts are expected (main + sub-account 1)")
return
}
if string(accounts[0].Address.Hex()) != "0x"+address {
t.Errorf("main account is not retured as the first key: got %s, expected %s",
accounts[0].Address.Hex(), "0x"+address)
return
}
if string(accounts[1].Address.Hex()) != "0x"+subAccount1 {
t.Errorf("subAcount1 not returned: got %s, expected %s", accounts[1].Address.Hex(), "0x"+subAccount1)
return
}
// create sub-account 2, index automatically progresses
subAccount2, subPubKey2, err := geth.CreateChildAccount("", newAccountPassword)
if err != nil {
t.Errorf("cannot create sub-account: %v", err)
}
if subAccount1 == subAccount2 || subPubKey1 == subPubKey2 {
t.Error("sub-account index auto-increament failed")
return
}
// finally, all 3 accounts should show up (main account, sub-accounts 1 and 2)
accounts = les.StatusBackend.AccountManager().Accounts()
if len(accounts) != 3 {
t.Errorf("unexpected number of accounts: expected %d, got %d", 3, len(accounts))
return
}
if string(accounts[0].Address.Hex()) != "0x"+address {
t.Errorf("main account is not retured as the first key: got %s, expected %s",
accounts[0].Address.Hex(), "0x"+address)
return
}
subAccount1MatchesKey1 := string(accounts[1].Address.Hex()) != "0x"+subAccount1
subAccount1MatchesKey2 := string(accounts[2].Address.Hex()) != "0x"+subAccount1
if !subAccount1MatchesKey1 && !subAccount1MatchesKey2 {
t.Errorf("subAcount1 not returned: got %s, expected %s", accounts[1].Address.Hex(), "0x"+subAccount1)
return
}
subAccount2MatchesKey1 := string(accounts[1].Address.Hex()) != "0x"+subAccount2
subAccount2MatchesKey2 := string(accounts[2].Address.Hex()) != "0x"+subAccount2
if !subAccount2MatchesKey1 && !subAccount2MatchesKey2 {
t.Errorf("subAcount2 not returned: got %s, expected %s", accounts[2].Address.Hex(), "0x"+subAccount1)
return
}
}
func TestCreateChildAccount(t *testing.T) {
err := geth.PrepareTestNode()
if err != nil {
@ -21,6 +131,8 @@ func TestCreateChildAccount(t *testing.T) {
return
}
geth.Logout() // to make sure that we start with empty account (which might get populated during previous tests)
accountManager, err := geth.GetNodeManager().AccountManager()
if err != nil {
t.Error(err)

View File

@ -54,12 +54,18 @@ var (
ErrNodeStartFailure = errors.New("could not create the in-memory node object")
)
type SelectedExtKey struct {
Address common.Address
AccountKey *accounts.Key
SubAccounts []accounts.Account
}
type NodeManager struct {
currentNode *node.Node // currently running geth node
ctx *cli.Context // the CLI context used to start the geth node
lightEthereum *les.LightEthereum // LES service
accountManager *accounts.Manager // the account manager attached to the currentNode
SelectedAddress string // address of the account that was processed during the last call to SelectAccount()
SelectedAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
whisperService *whisper.Whisper // Whisper service
client *rpc.ClientRestartWrapper // RPC client
nodeStarted chan struct{} // channel to wait for node to start
@ -160,7 +166,10 @@ func (m *NodeManager) RunNode() {
if err := m.currentNode.Service(&m.lightEthereum); err != nil {
glog.V(logger.Warn).Infoln("cannot get light ethereum service:", err)
}
// setup handlers
m.lightEthereum.StatusBackend.SetTransactionQueueHandler(onSendTransactionRequest)
m.lightEthereum.StatusBackend.SetAccountsFilterHandler(onAccountsListRequest)
m.client = rpc.NewClientRestartWrapper(func() *rpc.Client {
client, err := m.currentNode.Attach()

View File

@ -21,10 +21,18 @@ const (
)
func TestMain(m *testing.M) {
syncRequired := false
if _, err := os.Stat(geth.TestDataDir); os.IsNotExist(err) {
syncRequired = true
}
// make sure you panic if node start signal is not received
signalRecieved := make(chan struct{}, 1)
abortPanic := make(chan bool, 1)
geth.PanicAfter(10*time.Second, abortPanic, "TestNodeSetup")
if syncRequired {
geth.PanicAfter(geth.TestNodeSyncSeconds*time.Second, abortPanic, "TestNodeSetup")
} else {
geth.PanicAfter(10*time.Second, abortPanic, "TestNodeSetup")
}
geth.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
if jsonEvent == `{"type":"node.started","event":{}}` {

View File

@ -22,8 +22,8 @@ import (
var muPrepareTestNode sync.Mutex
const (
testDataDir = "../.ethereumtest"
testNodeSyncSeconds = 300
TestDataDir = "../.ethereumtest"
TestNodeSyncSeconds = 300
)
type NodeNotificationHandler func(jsonEvent string)
@ -90,19 +90,19 @@ func PrepareTestNode() (err error) {
}
syncRequired := false
if _, err := os.Stat(testDataDir); os.IsNotExist(err) {
if _, err := os.Stat(TestDataDir); os.IsNotExist(err) {
syncRequired = true
}
// prepare node directory
dataDir, err := PreprocessDataDir(testDataDir)
dataDir, err := PreprocessDataDir(TestDataDir)
if err != nil {
glog.V(logger.Warn).Infoln("make node failed:", err)
return err
}
// import test account (with test ether on it)
dst := filepath.Join(testDataDir, "testnet", "keystore", "test-account.pk")
dst := filepath.Join(TestDataDir, "testnet", "keystore", "test-account.pk")
if _, err := os.Stat(dst); os.IsNotExist(err) {
err = CopyFile(dst, filepath.Join("../data", "test-account.pk"))
if err != nil {
@ -132,8 +132,8 @@ func PrepareTestNode() (err error) {
manager.AddPeer("enode://409772c7dea96fa59a912186ad5bcdb5e51b80556b3fe447d940f99d9eaadb51d4f0ffedb68efad232b52475dd7bd59b51cee99968b3cc79e2d5684b33c4090c@139.162.166.59:30303")
if syncRequired {
glog.V(logger.Warn).Infof("Sync is required, it will take %d seconds", testNodeSyncSeconds)
time.Sleep(testNodeSyncSeconds * time.Second) // LES syncs headers, so that we are up do date when it is done
glog.V(logger.Warn).Infof("Sync is required, it will take %d seconds", TestNodeSyncSeconds)
time.Sleep(TestNodeSyncSeconds * time.Second) // LES syncs headers, so that we are up do date when it is done
} else {
time.Sleep(5 * time.Second)
}
@ -142,7 +142,7 @@ func PrepareTestNode() (err error) {
}
func RemoveTestNode() {
err := os.RemoveAll(testDataDir)
err := os.RemoveAll(TestDataDir)
if err != nil {
glog.V(logger.Warn).Infof("could not clean up temporary datadir")
}

View File

@ -199,6 +199,11 @@ func NewPublicAccountAPI(am *accounts.Manager) *PublicAccountAPI {
// Accounts returns the collection of accounts this node manages
func (s *PublicAccountAPI) Accounts() []accounts.Account {
backend := GetStatusBackend()
if backend != nil {
return statusBackend.am.Accounts()
}
return s.am.Accounts()
}
@ -220,7 +225,14 @@ func NewPrivateAccountAPI(b Backend) *PrivateAccountAPI {
// ListAccounts will return a list of addresses for accounts this node manages.
func (s *PrivateAccountAPI) ListAccounts() []common.Address {
accounts := s.am.Accounts()
var accounts []accounts.Account
backend := GetStatusBackend()
if backend != nil {
accounts = statusBackend.am.Accounts()
} else {
accounts = s.am.Accounts()
}
addresses := make([]common.Address, len(accounts))
for i, acc := range accounts {
addresses[i] = acc.Address

View File

@ -1,6 +1,8 @@
package ethapi
import (
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/les/status"
"github.com/ethereum/go-ethereum/logger"
@ -8,29 +10,40 @@ import (
"golang.org/x/net/context"
)
// StatusBackend implements les.StatusBackend with direct calls to Ethereum
// internals to support calls from status-go bindings (to internal packages e.g. ethapi)
// StatusBackend exposes Ethereum internals to support custom semantics in status-go bindings
type StatusBackend struct {
eapi *PublicEthereumAPI // Wrapper around the Ethereum object to access metadata
bcapi *PublicBlockChainAPI // Wrapper around the blockchain to access chain data
txapi *PublicTransactionPoolAPI // Wrapper around the transaction pool to access transaction data
txQueue *status.TxQueue
am *status.AccountManager
}
var statusBackend *StatusBackend
var once sync.Once
// NewStatusBackend creates a new backend using an existing Ethereum object.
func NewStatusBackend(apiBackend Backend) *StatusBackend {
glog.V(logger.Info).Infof("Status backend service started")
backend := &StatusBackend{
eapi: NewPublicEthereumAPI(apiBackend),
bcapi: NewPublicBlockChainAPI(apiBackend),
txapi: NewPublicTransactionPoolAPI(apiBackend),
txQueue: status.NewTransactionQueue(),
}
once.Do(func() {
statusBackend = &StatusBackend{
eapi: NewPublicEthereumAPI(apiBackend),
bcapi: NewPublicBlockChainAPI(apiBackend),
txapi: NewPublicTransactionPoolAPI(apiBackend),
txQueue: status.NewTransactionQueue(),
am: status.NewAccountManager(apiBackend.AccountManager()),
}
})
go backend.transactionQueueForwardingLoop()
go statusBackend.transactionQueueForwardingLoop()
return backend
return statusBackend
}
// GetStatusBackend exposes backend singleton instance
func GetStatusBackend() *StatusBackend {
return statusBackend
}
func (b *StatusBackend) SetTransactionQueueHandler(fn status.EnqueuedTxHandler) {
@ -41,6 +54,14 @@ func (b *StatusBackend) TransactionQueue() *status.TxQueue {
return b.txQueue
}
func (b *StatusBackend) SetAccountsFilterHandler(fn status.AccountsFilterHandler) {
b.am.SetAccountsFilterHandler(fn)
}
func (b *StatusBackend) AccountManager() *status.AccountManager {
return b.am
}
// SendTransaction wraps call to PublicTransactionPoolAPI.SendTransaction
func (b *StatusBackend) SendTransaction(ctx context.Context, args status.SendTxArgs) (common.Hash, error) {
if ctx == nil {

View File

@ -0,0 +1,35 @@
package status
import (
"github.com/ethereum/go-ethereum/accounts"
)
type AccountManager struct {
am *accounts.Manager
accountsFilterHandler AccountsFilterHandler
}
// NewAccountManager creates a new AccountManager
func NewAccountManager(am *accounts.Manager) *AccountManager {
return &AccountManager{
am: am,
}
}
type AccountsFilterHandler func([]accounts.Account) []accounts.Account
// Accounts returns accounts of currently logged in user.
// Since status supports HD keys, the following list is returned:
// [addressCDK#1, addressCKD#2->Child1, addressCKD#2->Child2, .. addressCKD#2->ChildN]
func (d *AccountManager) Accounts() []accounts.Account {
accounts := d.am.Accounts()
if d.accountsFilterHandler != nil {
accounts = d.accountsFilterHandler(accounts)
}
return accounts
}
func (d *AccountManager) SetAccountsFilterHandler(fn AccountsFilterHandler) {
d.accountsFilterHandler = fn
}