diff --git a/.gitignore b/.gitignore index f5f106634..ac97ccd35 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ # used by the Makefile /build/_workspace/ /build/bin/ +/vendor/github.com/karalabe/xgo # travis profile.tmp diff --git a/geth/accounts.go b/geth/accounts.go index a5bf351ba..4812c7692 100644 --- a/geth/accounts.go +++ b/geth/accounts.go @@ -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 +} diff --git a/geth/accounts_test.go b/geth/accounts_test.go index 2892d6a72..71c32638e 100644 --- a/geth/accounts_test.go +++ b/geth/accounts_test.go @@ -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) diff --git a/geth/node.go b/geth/node.go index e35e0fda7..ee47354a6 100644 --- a/geth/node.go +++ b/geth/node.go @@ -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() diff --git a/geth/node_test.go b/geth/node_test.go index a9959d425..819591e58 100644 --- a/geth/node_test.go +++ b/geth/node_test.go @@ -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":{}}` { diff --git a/geth/utils.go b/geth/utils.go index 4feeaaccb..821b22517 100644 --- a/geth/utils.go +++ b/geth/utils.go @@ -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") } diff --git a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go index 5a6a4d697..24b0d2482 100644 --- a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go +++ b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go @@ -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 diff --git a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/status_backend.go b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/status_backend.go index 2ad00d062..31409350c 100644 --- a/vendor/github.com/ethereum/go-ethereum/internal/ethapi/status_backend.go +++ b/vendor/github.com/ethereum/go-ethereum/internal/ethapi/status_backend.go @@ -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 { diff --git a/vendor/github.com/ethereum/go-ethereum/les/status/accounts.go b/vendor/github.com/ethereum/go-ethereum/les/status/accounts.go new file mode 100644 index 000000000..f081180d5 --- /dev/null +++ b/vendor/github.com/ethereum/go-ethereum/les/status/accounts.go @@ -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 +}