add multi-account support (#1500)
* add multiaccount support add multi account ImportPrivateKey and StoreAccount test derivation from normal keys * add multiaccount to mobile pkg * use multiaccount params structs from the mobile pkg * move multiaccount tests together with the other lib tests * fix codeclimate warning and temporarily increase methods threshold * split library_test_utils.go to avoid linter warnings
This commit is contained in:
parent
e93d994460
commit
dcb0fa5262
|
@ -18,7 +18,7 @@ checks:
|
||||||
# Classes defined with a high number of functions or methods.
|
# Classes defined with a high number of functions or methods.
|
||||||
method-count:
|
method-count:
|
||||||
config:
|
config:
|
||||||
threshold: 20
|
threshold: 21
|
||||||
# Excessive lines of code within a single function or method
|
# Excessive lines of code within a single function or method
|
||||||
method-lines:
|
method-lines:
|
||||||
config:
|
config:
|
||||||
|
|
|
@ -12,11 +12,13 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts"
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
gethcommon "github.com/ethereum/go-ethereum/common"
|
gethcommon "github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/account/generator"
|
||||||
"github.com/status-im/status-go/extkeys"
|
"github.com/status-im/status-go/extkeys"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,7 +44,8 @@ type Manager struct {
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
onboarding *Onboarding
|
accountsGenerator *generator.Generator
|
||||||
|
onboarding *Onboarding
|
||||||
|
|
||||||
selectedWalletAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
|
selectedWalletAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
|
||||||
selectedChatAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
|
selectedChatAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
|
||||||
|
@ -50,9 +53,19 @@ type Manager struct {
|
||||||
|
|
||||||
// NewManager returns new node account manager.
|
// NewManager returns new node account manager.
|
||||||
func NewManager(geth GethServiceProvider) *Manager {
|
func NewManager(geth GethServiceProvider) *Manager {
|
||||||
return &Manager{
|
manager := &Manager{
|
||||||
geth: geth,
|
geth: geth,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountsGenerator := generator.New(manager)
|
||||||
|
manager.accountsGenerator = accountsGenerator
|
||||||
|
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountsGenerator returns accountsGenerator.
|
||||||
|
func (m *Manager) AccountsGenerator() *generator.Generator {
|
||||||
|
return m.accountsGenerator
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAccount creates an internal geth account
|
// CreateAccount creates an internal geth account
|
||||||
|
@ -237,6 +250,8 @@ func (m *Manager) SelectAccount(walletAddress, chatAddress, password string) err
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
m.accountsGenerator.Reset()
|
||||||
|
|
||||||
selectedWalletAccount, err := m.unlockExtendedKey(walletAddress, password)
|
selectedWalletAccount, err := m.unlockExtendedKey(walletAddress, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -299,10 +314,48 @@ func (m *Manager) Logout() {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
m.accountsGenerator.Reset()
|
||||||
m.selectedWalletAccount = nil
|
m.selectedWalletAccount = nil
|
||||||
m.selectedChatAccount = nil
|
m.selectedChatAccount = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImportAccount imports the account specified with privateKey.
|
||||||
|
func (m *Manager) ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error) {
|
||||||
|
keyStore, err := m.geth.AccountKeyStore()
|
||||||
|
if err != nil {
|
||||||
|
return common.Address{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := keyStore.ImportECDSA(privateKey, password)
|
||||||
|
|
||||||
|
return account.Address, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
|
||||||
|
keyStore, err := m.geth.AccountKeyStore()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// imports extended key, create key file (if necessary)
|
||||||
|
account, err := keyStore.ImportSingleExtendedKey(extKey, password)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
address = account.Address.Hex()
|
||||||
|
|
||||||
|
// obtain public key to return
|
||||||
|
account, key, err := keyStore.AccountDecryptedKey(account, password)
|
||||||
|
if err != nil {
|
||||||
|
return address, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey = hexutil.Encode(crypto.FromECDSAPub(&key.PrivateKey.PublicKey))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// importExtendedKey processes incoming extended key, extracts required info and creates corresponding account key.
|
// importExtendedKey processes incoming extended key, extracts required info and creates corresponding account key.
|
||||||
// Once account key is formed, that key is put (if not already) into keystore i.e. key is *encoded* into key file.
|
// Once account key is formed, that key is put (if not already) into keystore i.e. key is *encoded* into key file.
|
||||||
func (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
|
func (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
|
||||||
|
@ -491,7 +544,6 @@ func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.
|
||||||
}
|
}
|
||||||
|
|
||||||
var key *keystore.Key
|
var key *keystore.Key
|
||||||
|
|
||||||
account, key, err = keyStore.AccountDecryptedKey(account, password)
|
account, key, err = keyStore.AccountDecryptedKey(account, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err)
|
err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err)
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/status-im/status-go/extkeys"
|
||||||
|
)
|
||||||
|
|
||||||
|
type account struct {
|
||||||
|
privateKey *ecdsa.PrivateKey
|
||||||
|
extendedKey *extkeys.ExtendedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *account) toAccountInfo() AccountInfo {
|
||||||
|
publicKeyHex := hexutil.Encode(crypto.FromECDSAPub(&a.privateKey.PublicKey))
|
||||||
|
addressHex := crypto.PubkeyToAddress(a.privateKey.PublicKey).Hex()
|
||||||
|
|
||||||
|
return AccountInfo{
|
||||||
|
PublicKey: publicKeyHex,
|
||||||
|
Address: addressHex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *account) toIdentifiedAccountInfo(id string) IdentifiedAccountInfo {
|
||||||
|
info := a.toAccountInfo()
|
||||||
|
return IdentifiedAccountInfo{
|
||||||
|
AccountInfo: info,
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *account) toGeneratedAccountInfo(id string, mnemonic string) GeneratedAccountInfo {
|
||||||
|
idInfo := a.toIdentifiedAccountInfo(id)
|
||||||
|
return GeneratedAccountInfo{
|
||||||
|
IdentifiedAccountInfo: idInfo,
|
||||||
|
Mnemonic: mnemonic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountInfo contains a PublicKey and an Address of an account.
|
||||||
|
type AccountInfo struct {
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdentifiedAccountInfo contains AccountInfo and the ID of an account.
|
||||||
|
type IdentifiedAccountInfo struct {
|
||||||
|
AccountInfo
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratedAccountInfo contains IdentifiedAccountInfo and the mnemonic of an account.
|
||||||
|
type GeneratedAccountInfo struct {
|
||||||
|
IdentifiedAccountInfo
|
||||||
|
Mnemonic string `json:"mnemonic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a GeneratedAccountInfo) toGeneratedAndDerived(derived map[string]AccountInfo) GeneratedAndDerivedAccountInfo {
|
||||||
|
return GeneratedAndDerivedAccountInfo{
|
||||||
|
GeneratedAccountInfo: a,
|
||||||
|
Derived: derived,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratedAndDerivedAccountInfo contains GeneratedAccountInfo and derived AccountInfo mapped by derivation path.
|
||||||
|
type GeneratedAndDerivedAccountInfo struct {
|
||||||
|
GeneratedAccountInfo
|
||||||
|
Derived map[string]AccountInfo `json:"derived"`
|
||||||
|
}
|
|
@ -0,0 +1,311 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts"
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/pborman/uuid"
|
||||||
|
"github.com/status-im/status-go/extkeys"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrAccountNotFoundByID is returned when the selected account doesn't exist in memory.
|
||||||
|
ErrAccountNotFoundByID = errors.New("account not found")
|
||||||
|
// ErrAccountCannotDeriveChildKeys is returned when trying to derive child accounts from a normal key.
|
||||||
|
ErrAccountCannotDeriveChildKeys = errors.New("selected account cannot derive child keys")
|
||||||
|
// ErrAccountManagerNotSet is returned when the account mananger instance is not set.
|
||||||
|
ErrAccountManagerNotSet = errors.New("account manager not set")
|
||||||
|
)
|
||||||
|
|
||||||
|
type AccountManager interface {
|
||||||
|
AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error)
|
||||||
|
ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error)
|
||||||
|
ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Generator struct {
|
||||||
|
am AccountManager
|
||||||
|
accounts map[string]*account
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(am AccountManager) *Generator {
|
||||||
|
return &Generator{
|
||||||
|
am: am,
|
||||||
|
accounts: make(map[string]*account),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) Generate(mnemonicPhraseLength int, n int, bip39Passphrase string) ([]GeneratedAccountInfo, error) {
|
||||||
|
entropyStrength, err := MnemonicPhraseLengthToEntropyStrength(mnemonicPhraseLength)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
infos := make([]GeneratedAccountInfo, 0)
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
mnemonic := extkeys.NewMnemonic()
|
||||||
|
mnemonicPhrase, err := mnemonic.MnemonicPhrase(entropyStrength, extkeys.EnglishLanguage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can not create mnemonic seed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := g.ImportMnemonic(mnemonicPhrase, bip39Passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
infos = append(infos, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
return infos, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) ImportPrivateKey(privateKeyHex string) (IdentifiedAccountInfo, error) {
|
||||||
|
privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x")
|
||||||
|
privateKey, err := crypto.HexToECDSA(privateKeyHex)
|
||||||
|
if err != nil {
|
||||||
|
return IdentifiedAccountInfo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
acc := &account{
|
||||||
|
privateKey: privateKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
id := g.addAccount(acc)
|
||||||
|
|
||||||
|
return acc.toIdentifiedAccountInfo(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) ImportJSONKey(json string, password string) (IdentifiedAccountInfo, error) {
|
||||||
|
key, err := keystore.DecryptKey([]byte(json), password)
|
||||||
|
if err != nil {
|
||||||
|
return IdentifiedAccountInfo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
acc := &account{
|
||||||
|
privateKey: key.PrivateKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
id := g.addAccount(acc)
|
||||||
|
|
||||||
|
return acc.toIdentifiedAccountInfo(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) ImportMnemonic(mnemonicPhrase string, bip39Passphrase string) (GeneratedAccountInfo, error) {
|
||||||
|
mnemonic := extkeys.NewMnemonic()
|
||||||
|
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, bip39Passphrase))
|
||||||
|
if err != nil {
|
||||||
|
return GeneratedAccountInfo{}, fmt.Errorf("can not create master extended key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acc := &account{
|
||||||
|
privateKey: masterExtendedKey.ToECDSA(),
|
||||||
|
extendedKey: masterExtendedKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
id := g.addAccount(acc)
|
||||||
|
|
||||||
|
return acc.toGeneratedAccountInfo(id, mnemonicPhrase), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) GenerateAndDeriveAddresses(mnemonicPhraseLength int, n int, bip39Passphrase string, pathStrings []string) ([]GeneratedAndDerivedAccountInfo, error) {
|
||||||
|
masterAccounts, err := g.Generate(mnemonicPhraseLength, n, bip39Passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accs := make([]GeneratedAndDerivedAccountInfo, n)
|
||||||
|
|
||||||
|
for i := 0; i < len(masterAccounts); i++ {
|
||||||
|
acc := masterAccounts[i]
|
||||||
|
derived, err := g.DeriveAddresses(acc.ID, pathStrings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accs[i] = acc.toGeneratedAndDerived(derived)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) DeriveAddresses(accountID string, pathStrings []string) (map[string]AccountInfo, error) {
|
||||||
|
acc, err := g.findAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pathAccounts, err := g.deriveChildAccounts(acc, pathStrings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pathAccountsInfo := make(map[string]AccountInfo)
|
||||||
|
|
||||||
|
for pathString, childAccount := range pathAccounts {
|
||||||
|
pathAccountsInfo[pathString] = childAccount.toAccountInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathAccountsInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) StoreAccount(accountID string, password string) (AccountInfo, error) {
|
||||||
|
if g.am == nil {
|
||||||
|
return AccountInfo{}, ErrAccountManagerNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
acc, err := g.findAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return AccountInfo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return g.store(acc, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) StoreDerivedAccounts(accountID string, password string, pathStrings []string) (map[string]AccountInfo, error) {
|
||||||
|
if g.am == nil {
|
||||||
|
return nil, ErrAccountManagerNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
acc, err := g.findAccount(accountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pathAccounts, err := g.deriveChildAccounts(acc, pathStrings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pathAccountsInfo := make(map[string]AccountInfo)
|
||||||
|
|
||||||
|
for pathString, childAccount := range pathAccounts {
|
||||||
|
info, err := g.store(childAccount, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pathAccountsInfo[pathString] = info
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathAccountsInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) LoadAccount(address string, password string) (IdentifiedAccountInfo, error) {
|
||||||
|
if g.am == nil {
|
||||||
|
return IdentifiedAccountInfo{}, ErrAccountManagerNotSet
|
||||||
|
}
|
||||||
|
|
||||||
|
_, key, err := g.am.AddressToDecryptedAccount(address, password)
|
||||||
|
if err != nil {
|
||||||
|
return IdentifiedAccountInfo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateKeystoreExtendedKey(key); err != nil {
|
||||||
|
return IdentifiedAccountInfo{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
acc := &account{
|
||||||
|
privateKey: key.PrivateKey,
|
||||||
|
extendedKey: key.ExtendedKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
id := g.addAccount(acc)
|
||||||
|
|
||||||
|
return acc.toIdentifiedAccountInfo(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) deriveChildAccounts(acc *account, pathStrings []string) (map[string]*account, error) {
|
||||||
|
pathAccounts := make(map[string]*account)
|
||||||
|
|
||||||
|
for _, pathString := range pathStrings {
|
||||||
|
childAccount, err := g.deriveChildAccount(acc, pathString)
|
||||||
|
if err != nil {
|
||||||
|
return pathAccounts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pathAccounts[pathString] = childAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathAccounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) deriveChildAccount(acc *account, pathString string) (*account, error) {
|
||||||
|
_, path, err := decodePath(pathString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if acc.extendedKey.IsZeroed() && len(path) == 0 {
|
||||||
|
return acc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if acc.extendedKey.IsZeroed() {
|
||||||
|
return nil, ErrAccountCannotDeriveChildKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
childExtendedKey, err := acc.extendedKey.Derive(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account{
|
||||||
|
privateKey: childExtendedKey.ToECDSA(),
|
||||||
|
extendedKey: childExtendedKey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) store(acc *account, password string) (AccountInfo, error) {
|
||||||
|
if acc.extendedKey != nil {
|
||||||
|
if _, _, err := g.am.ImportSingleExtendedKey(acc.extendedKey, password); err != nil {
|
||||||
|
return AccountInfo{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := g.am.ImportAccount(acc.privateKey, password); err != nil {
|
||||||
|
return AccountInfo{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Reset()
|
||||||
|
|
||||||
|
return acc.toAccountInfo(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) addAccount(acc *account) string {
|
||||||
|
g.Lock()
|
||||||
|
defer g.Unlock()
|
||||||
|
|
||||||
|
id := uuid.NewRandom().String()
|
||||||
|
g.accounts[id] = acc
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset resets the accounts map removing all the accounts from memory.
|
||||||
|
func (g *Generator) Reset() {
|
||||||
|
g.Lock()
|
||||||
|
defer g.Unlock()
|
||||||
|
|
||||||
|
g.accounts = make(map[string]*account)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Generator) findAccount(accountID string) (*account, error) {
|
||||||
|
g.Lock()
|
||||||
|
defer g.Unlock()
|
||||||
|
|
||||||
|
acc, ok := g.accounts[accountID]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrAccountNotFoundByID
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc, nil
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testAccount = struct {
|
||||||
|
mnemonic string
|
||||||
|
bip39Passphrase string
|
||||||
|
encriptionPassword string
|
||||||
|
extendedMasterKey string
|
||||||
|
bip44Key0 string
|
||||||
|
bip44PubKey0 string
|
||||||
|
bip44Address0 string
|
||||||
|
bip44Address1 string
|
||||||
|
}{
|
||||||
|
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||||
|
bip39Passphrase: "TREZOR",
|
||||||
|
encriptionPassword: "TEST_PASSWORD",
|
||||||
|
extendedMasterKey: "xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF",
|
||||||
|
bip44Key0: "0x62f1d86b246c81bdd8f6c166d56896a4a5e1eddbcaebe06480e5c0bc74c28224",
|
||||||
|
bip44PubKey0: "0x04986dee3b8afe24cb8ccb2ac23dac3f8c43d22850d14b809b26d6b8aa5a1f47784152cd2c7d9edd0ab20392a837464b5a750b2a7f3f06e6a5756b5211b6a6ed05",
|
||||||
|
bip44Address0: "0x9c32F71D4DB8Fb9e1A58B0a80dF79935e7256FA6",
|
||||||
|
bip44Address1: "0x7AF7283bd1462C3b957e8FAc28Dc19cBbF2FAdfe",
|
||||||
|
}
|
||||||
|
|
||||||
|
const testAccountJSONFile = `{
|
||||||
|
"address":"9c32f71d4db8fb9e1a58b0a80df79935e7256fa6",
|
||||||
|
"crypto":
|
||||||
|
{
|
||||||
|
"cipher":"aes-128-ctr","ciphertext":"8055b65d5e41ef467c0cfe52ce6beda7f8dbe689221c6c43be9e9401bf173004",
|
||||||
|
"cipherparams":{"iv":"738f002e5e5343e0bb0e1050e098f721"},
|
||||||
|
"kdf":"scrypt",
|
||||||
|
"kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"9a54fbe1439ac567bd05039f76907b2c2846364a38b2f6813bcdac5ab0ec9d18"},
|
||||||
|
"mac":"79d817cd21afd4944e70d804d7871d10cbd15f25c6755416f780f81c1588677e"
|
||||||
|
},
|
||||||
|
"id":"6202ced9-f0cd-42e4-bf21-6029cca0ea91",
|
||||||
|
"version":3
|
||||||
|
}`
|
||||||
|
|
||||||
|
func TestGenerator_Generate(t *testing.T) {
|
||||||
|
g := New(nil)
|
||||||
|
assert.Equal(t, 0, len(g.accounts))
|
||||||
|
|
||||||
|
accountsInfo, err := g.Generate(12, 5, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 5, len(g.accounts))
|
||||||
|
|
||||||
|
for _, info := range accountsInfo {
|
||||||
|
words := strings.Split(info.Mnemonic, " ")
|
||||||
|
assert.Equal(t, 12, len(words))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerator_ImportPrivateKey(t *testing.T) {
|
||||||
|
g := New(nil)
|
||||||
|
assert.Equal(t, 0, len(g.accounts))
|
||||||
|
|
||||||
|
info, err := g.ImportPrivateKey(testAccount.bip44Key0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(g.accounts))
|
||||||
|
|
||||||
|
assert.Equal(t, testAccount.bip44PubKey0, info.PublicKey)
|
||||||
|
assert.Equal(t, testAccount.bip44Address0, info.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerator_ImportMnemonic(t *testing.T) {
|
||||||
|
g := New(nil)
|
||||||
|
assert.Equal(t, 0, len(g.accounts))
|
||||||
|
|
||||||
|
info, err := g.ImportMnemonic(testAccount.mnemonic, testAccount.bip39Passphrase)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(g.accounts))
|
||||||
|
|
||||||
|
key := g.accounts[info.ID]
|
||||||
|
assert.Equal(t, testAccount.extendedMasterKey, key.extendedKey.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerator_ImportJSONKey(t *testing.T) {
|
||||||
|
g := New(nil)
|
||||||
|
assert.Equal(t, 0, len(g.accounts))
|
||||||
|
|
||||||
|
// wrong password
|
||||||
|
_, err := g.ImportJSONKey(testAccountJSONFile, "wrong-password")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// right password
|
||||||
|
info, err := g.ImportJSONKey(testAccountJSONFile, testAccount.encriptionPassword)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(g.accounts))
|
||||||
|
assert.Equal(t, testAccount.bip44Address0, info.Address)
|
||||||
|
|
||||||
|
key := g.accounts[info.ID]
|
||||||
|
keyHex := fmt.Sprintf("0x%x", crypto.FromECDSA(key.privateKey))
|
||||||
|
assert.Equal(t, testAccount.bip44Key0, keyHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerator_DeriveAddresses(t *testing.T) {
|
||||||
|
g := New(nil)
|
||||||
|
assert.Equal(t, 0, len(g.accounts))
|
||||||
|
|
||||||
|
info, err := g.ImportMnemonic(testAccount.mnemonic, testAccount.bip39Passphrase)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(g.accounts))
|
||||||
|
|
||||||
|
path0 := "m/44'/60'/0'/0/0"
|
||||||
|
path1 := "m/44'/60'/0'/0/1"
|
||||||
|
|
||||||
|
addresses, err := g.DeriveAddresses(info.ID, []string{path0, path1})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, testAccount.bip44Address0, addresses[path0].Address)
|
||||||
|
assert.Equal(t, testAccount.bip44Address1, addresses[path1].Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerator_DeriveAddresses_FromImportedPrivateKey(t *testing.T) {
|
||||||
|
g := New(nil)
|
||||||
|
assert.Equal(t, 0, len(g.accounts))
|
||||||
|
|
||||||
|
key, err := crypto.GenerateKey()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
hex := fmt.Sprintf("%#x", crypto.FromECDSA(key))
|
||||||
|
info, err := g.ImportPrivateKey(hex)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(g.accounts))
|
||||||
|
|
||||||
|
// normal imported accounts cannot derive child accounts,
|
||||||
|
// but only the address/pubblic key of the current key.
|
||||||
|
paths := []string{"", "m"}
|
||||||
|
for _, path := range paths {
|
||||||
|
addresses, err := g.DeriveAddresses(info.ID, []string{path})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expectedAddress := crypto.PubkeyToAddress(key.PublicKey).Hex()
|
||||||
|
assert.Equal(t, expectedAddress, addresses[path].Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cannot derive other child keys from a normal key
|
||||||
|
_, err = g.DeriveAddresses(info.ID, []string{"m/0/1/2"})
|
||||||
|
assert.Equal(t, ErrAccountCannotDeriveChildKeys, err)
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type startingPoint int
|
||||||
|
|
||||||
|
const (
|
||||||
|
tokenMaster = 0x6D // char m
|
||||||
|
tokenSeparator = 0x2F // char /
|
||||||
|
tokenHardened = 0x27 // char '
|
||||||
|
tokenDot = 0x2E // char .
|
||||||
|
|
||||||
|
hardenedStart = 0x80000000 // 2^31
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
startingPointMaster startingPoint = iota + 1
|
||||||
|
startingPointCurrent
|
||||||
|
startingPointParent
|
||||||
|
)
|
||||||
|
|
||||||
|
type parseFunc = func() error
|
||||||
|
|
||||||
|
type pathDecoder struct {
|
||||||
|
s string
|
||||||
|
r *strings.Reader
|
||||||
|
f parseFunc
|
||||||
|
pos int
|
||||||
|
path []uint32
|
||||||
|
start startingPoint
|
||||||
|
currentToken string
|
||||||
|
currentTokenHardened bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPathDecoder(path string) (*pathDecoder, error) {
|
||||||
|
d := &pathDecoder{
|
||||||
|
s: path,
|
||||||
|
r: strings.NewReader(path),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.reset(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *pathDecoder) reset() error {
|
||||||
|
_, err := d.r.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.pos = 0
|
||||||
|
d.start = startingPointCurrent
|
||||||
|
d.f = d.parseStart
|
||||||
|
d.path = make([]uint32, 0)
|
||||||
|
d.resetCurrentToken()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *pathDecoder) resetCurrentToken() {
|
||||||
|
d.currentToken = ""
|
||||||
|
d.currentTokenHardened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *pathDecoder) parse() (startingPoint, []uint32, error) {
|
||||||
|
for {
|
||||||
|
err := d.f()
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("error parsing derivation path %s; at position %d, %s", d.s, d.pos, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.start, d.path, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *pathDecoder) readByte() (byte, error) {
|
||||||
|
b, err := d.r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.pos++
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *pathDecoder) unreadByte() error {
|
||||||
|
err := d.r.UnreadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.pos--
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *pathDecoder) parseStart() error {
|
||||||
|
b, err := d.readByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == tokenMaster {
|
||||||
|
d.start = startingPointMaster
|
||||||
|
d.f = d.parseSeparator
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == tokenDot {
|
||||||
|
b2, err := d.readByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b2 == tokenDot {
|
||||||
|
d.f = d.parseSeparator
|
||||||
|
d.start = startingPointParent
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.f = d.parseSeparator
|
||||||
|
d.start = startingPointCurrent
|
||||||
|
return d.unreadByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
d.f = d.parseSegment
|
||||||
|
|
||||||
|
return d.unreadByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *pathDecoder) saveSegment() error {
|
||||||
|
if len(d.currentToken) > 0 {
|
||||||
|
i, err := strconv.ParseUint(d.currentToken, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= hardenedStart {
|
||||||
|
d.pos -= len(d.currentToken) - 1
|
||||||
|
return fmt.Errorf("index must be lower than 2^31, got %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.currentTokenHardened {
|
||||||
|
i += hardenedStart
|
||||||
|
}
|
||||||
|
|
||||||
|
d.path = append(d.path, uint32(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
d.f = d.parseSegment
|
||||||
|
d.resetCurrentToken()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *pathDecoder) parseSeparator() error {
|
||||||
|
b, err := d.readByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == tokenSeparator {
|
||||||
|
return d.saveSegment()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("expected %s, got %s", string(tokenSeparator), string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *pathDecoder) parseSegment() error {
|
||||||
|
b, err := d.readByte()
|
||||||
|
if err == io.EOF {
|
||||||
|
if len(d.currentToken) == 0 {
|
||||||
|
return fmt.Errorf("expected number, got EOF")
|
||||||
|
}
|
||||||
|
|
||||||
|
if newErr := d.saveSegment(); newErr != nil {
|
||||||
|
return newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.currentToken) > 0 && b == tokenSeparator {
|
||||||
|
return d.saveSegment()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(d.currentToken) > 0 && b == tokenHardened {
|
||||||
|
d.currentTokenHardened = true
|
||||||
|
d.f = d.parseSeparator
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if b < 0x30 || b > 0x39 {
|
||||||
|
return fmt.Errorf("expected number, got %s", string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
d.currentToken = fmt.Sprintf("%s%s", d.currentToken, string(b))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePath(str string) (startingPoint, []uint32, error) {
|
||||||
|
d, err := newPathDecoder(str)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.parse()
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodePath(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
path string
|
||||||
|
expectedPath []uint32
|
||||||
|
expectedStartingPoint startingPoint
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
expectedPath: []uint32{},
|
||||||
|
expectedStartingPoint: startingPointCurrent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "1",
|
||||||
|
expectedPath: []uint32{1},
|
||||||
|
expectedStartingPoint: startingPointCurrent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "..",
|
||||||
|
expectedPath: []uint32{},
|
||||||
|
expectedStartingPoint: startingPointParent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "m",
|
||||||
|
expectedPath: []uint32{},
|
||||||
|
expectedStartingPoint: startingPointMaster,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "m/1",
|
||||||
|
expectedPath: []uint32{1},
|
||||||
|
expectedStartingPoint: startingPointMaster,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "m/1/2",
|
||||||
|
expectedPath: []uint32{1, 2},
|
||||||
|
expectedStartingPoint: startingPointMaster,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "m/1/2'/3",
|
||||||
|
expectedPath: []uint32{1, 2147483650, 3},
|
||||||
|
expectedStartingPoint: startingPointMaster,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "m/",
|
||||||
|
err: fmt.Errorf("error parsing derivation path m/; at position 2, expected number, got EOF"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "m/1//2",
|
||||||
|
err: fmt.Errorf("error parsing derivation path m/1//2; at position 5, expected number, got /"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "m/1'2",
|
||||||
|
err: fmt.Errorf("error parsing derivation path m/1'2; at position 5, expected /, got 2"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "m/'/2",
|
||||||
|
err: fmt.Errorf("error parsing derivation path m/'/2; at position 3, expected number, got '"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "m/2147483648",
|
||||||
|
err: fmt.Errorf("error parsing derivation path m/2147483648; at position 3, index must be lower than 2^31, got 2147483648"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, s := range scenarios {
|
||||||
|
t.Run(fmt.Sprintf("scenario %d", i), func(t *testing.T) {
|
||||||
|
startingP, path, err := decodePath(s.path)
|
||||||
|
if s.err == nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, s.expectedStartingPoint, startingP)
|
||||||
|
assert.Equal(t, s.expectedPath, path)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, s.err, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/status-im/status-go/extkeys"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidKeystoreExtendedKey is returned when the decrypted keystore file
|
||||||
|
// contains some old Status keys.
|
||||||
|
// The old version used to store the BIP44 account at index 0 as PrivateKey,
|
||||||
|
// and the BIP44 account at index 1 as ExtendedKey.
|
||||||
|
// The current version stores the same key as PrivateKey and ExtendedKey.
|
||||||
|
ErrInvalidKeystoreExtendedKey = errors.New("PrivateKey and ExtendedKey are different")
|
||||||
|
ErrInvalidMnemonicPhraseLength = errors.New("invalid mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateKeystoreExtendedKey validates the keystore keys, checking that
|
||||||
|
// ExtendedKey is the extended key of PrivateKey.
|
||||||
|
func ValidateKeystoreExtendedKey(key *keystore.Key) error {
|
||||||
|
if key.ExtendedKey.IsZeroed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(crypto.FromECDSA(key.PrivateKey), crypto.FromECDSA(key.ExtendedKey.ToECDSA())) {
|
||||||
|
return ErrInvalidKeystoreExtendedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MnemonicPhraseLengthToEntropyStrength returns the entropy strength for a given mnemonic length
|
||||||
|
func MnemonicPhraseLengthToEntropyStrength(length int) (extkeys.EntropyStrength, error) {
|
||||||
|
if length < 12 || length > 24 || length%3 != 0 {
|
||||||
|
return 0, ErrInvalidMnemonicPhraseLength
|
||||||
|
}
|
||||||
|
|
||||||
|
bitsLength := length * 11
|
||||||
|
checksumLength := bitsLength % 32
|
||||||
|
|
||||||
|
return extkeys.EntropyStrength(bitsLength - checksumLength), nil
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
|
"github.com/status-im/status-go/extkeys"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateTestKey(t *testing.T) *extkeys.ExtendedKey {
|
||||||
|
mnemonic := extkeys.NewMnemonic()
|
||||||
|
mnemonicPhrase, err := mnemonic.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, ""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return masterExtendedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateKeystoreExtendedKey(t *testing.T) {
|
||||||
|
extendedKey1 := generateTestKey(t)
|
||||||
|
extendedKey2 := generateTestKey(t)
|
||||||
|
|
||||||
|
// new keystore file format
|
||||||
|
key := &keystore.Key{
|
||||||
|
PrivateKey: extendedKey1.ToECDSA(),
|
||||||
|
ExtendedKey: extendedKey1,
|
||||||
|
}
|
||||||
|
assert.NoError(t, ValidateKeystoreExtendedKey(key))
|
||||||
|
|
||||||
|
// old keystore file format where the extended key was
|
||||||
|
// from another derivation path and not the same of the private key
|
||||||
|
oldKey := &keystore.Key{
|
||||||
|
PrivateKey: extendedKey1.ToECDSA(),
|
||||||
|
ExtendedKey: extendedKey2,
|
||||||
|
}
|
||||||
|
assert.Error(t, ValidateKeystoreExtendedKey(oldKey))
|
||||||
|
|
||||||
|
// normal key where we don't have an extended key
|
||||||
|
normalKey := &keystore.Key{
|
||||||
|
PrivateKey: extendedKey1.ToECDSA(),
|
||||||
|
ExtendedKey: nil,
|
||||||
|
}
|
||||||
|
assert.NoError(t, ValidateKeystoreExtendedKey(normalKey))
|
||||||
|
}
|
|
@ -1,19 +1,15 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
|
"github.com/status-im/status-go/account/generator"
|
||||||
"github.com/status-im/status-go/extkeys"
|
"github.com/status-im/status-go/extkeys"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrInvalidMnemonicPhraseLength is returned if the requested mnemonic length is invalid.
|
|
||||||
// Valid lengths are 12, 15, 18, 21, and 24.
|
|
||||||
var ErrInvalidMnemonicPhraseLength = errors.New("mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
|
|
||||||
|
|
||||||
// OnboardingAccount is returned during onboarding and contains its ID and the mnemonic to re-generate the same account Info keys.
|
// OnboardingAccount is returned during onboarding and contains its ID and the mnemonic to re-generate the same account Info keys.
|
||||||
type OnboardingAccount struct {
|
type OnboardingAccount struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
@ -64,7 +60,7 @@ func (o *Onboarding) Account(id string) (*OnboardingAccount, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Onboarding) generateAccount(mnemonicPhraseLength int) (*OnboardingAccount, error) {
|
func (o *Onboarding) generateAccount(mnemonicPhraseLength int) (*OnboardingAccount, error) {
|
||||||
entropyStrength, err := mnemonicPhraseLengthToEntropyStrenght(mnemonicPhraseLength)
|
entropyStrength, err := generator.MnemonicPhraseLengthToEntropyStrength(mnemonicPhraseLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -115,14 +111,3 @@ func (o *Onboarding) deriveAccount(masterExtendedKey *extkeys.ExtendedKey, purpo
|
||||||
|
|
||||||
return address.Hex(), publicKeyHex, nil
|
return address.Hex(), publicKeyHex, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mnemonicPhraseLengthToEntropyStrenght(length int) (extkeys.EntropyStrength, error) {
|
|
||||||
if length < 12 || length > 24 || length%3 != 0 {
|
|
||||||
return 0, ErrInvalidMnemonicPhraseLength
|
|
||||||
}
|
|
||||||
|
|
||||||
bitsLength := length * 11
|
|
||||||
checksumLength := bitsLength % 32
|
|
||||||
|
|
||||||
return extkeys.EntropyStrength(bitsLength - checksumLength), nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,35 +4,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/status-im/status-go/extkeys"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMnemonicPhraseLengthToEntropyStrenght(t *testing.T) {
|
|
||||||
scenarios := []struct {
|
|
||||||
phraseLength int
|
|
||||||
expectedStrength extkeys.EntropyStrength
|
|
||||||
expectedError error
|
|
||||||
}{
|
|
||||||
{12, 128, nil},
|
|
||||||
{15, 160, nil},
|
|
||||||
{18, 192, nil},
|
|
||||||
{21, 224, nil},
|
|
||||||
{24, 256, nil},
|
|
||||||
// invalid
|
|
||||||
{11, 0, ErrInvalidMnemonicPhraseLength},
|
|
||||||
{14, 0, ErrInvalidMnemonicPhraseLength},
|
|
||||||
{25, 0, ErrInvalidMnemonicPhraseLength},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range scenarios {
|
|
||||||
strength, err := mnemonicPhraseLengthToEntropyStrenght(s.phraseLength)
|
|
||||||
assert.Equal(t, s.expectedError, err)
|
|
||||||
assert.Equal(t, s.expectedStrength, strength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOnboarding(t *testing.T) {
|
func TestOnboarding(t *testing.T) {
|
||||||
count := 2
|
count := 2
|
||||||
wordsCount := 24
|
wordsCount := 24
|
||||||
|
|
|
@ -10,7 +10,8 @@ import (
|
||||||
|
|
||||||
// errors
|
// errors
|
||||||
var (
|
var (
|
||||||
ErrInvalidAccountAddressOrKey = errors.New("cannot parse address or key to valid account address")
|
ErrInvalidAccountAddressOrKey = errors.New("cannot parse address or key to valid account address")
|
||||||
|
ErrInvalidMnemonicPhraseLength = errors.New("invalid mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Info contains wallet and chat addresses and public keys of an account.
|
// Info contains wallet and chat addresses and public keys of an account.
|
||||||
|
|
|
@ -6,6 +6,9 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/status-im/status-go/account/generator"
|
||||||
|
"github.com/status-im/status-go/extkeys"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,3 +68,27 @@ func (suite *AccountUtilsTestSuite) TestHex() {
|
||||||
func TestAccountUtilsTestSuite(t *testing.T) {
|
func TestAccountUtilsTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(AccountUtilsTestSuite))
|
suite.Run(t, new(AccountUtilsTestSuite))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMnemonicPhraseLengthToEntropyStrength(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
phraseLength int
|
||||||
|
expectedStrength extkeys.EntropyStrength
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{12, 128, nil},
|
||||||
|
{15, 160, nil},
|
||||||
|
{18, 192, nil},
|
||||||
|
{21, 224, nil},
|
||||||
|
{24, 256, nil},
|
||||||
|
// invalid
|
||||||
|
{11, 0, ErrInvalidMnemonicPhraseLength},
|
||||||
|
{14, 0, ErrInvalidMnemonicPhraseLength},
|
||||||
|
{25, 0, ErrInvalidMnemonicPhraseLength},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
strength, err := generator.MnemonicPhraseLengthToEntropyStrength(s.phraseLength)
|
||||||
|
assert.Equal(t, s.expectedError, err)
|
||||||
|
assert.Equal(t, s.expectedStrength, strength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -486,6 +486,7 @@ func (b *StatusBackend) Logout() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.statusNode.Config().BrowsersConfig.Enabled {
|
if b.statusNode.Config().BrowsersConfig.Enabled {
|
||||||
svc, err := b.statusNode.BrowsersService()
|
svc, err := b.statusNode.BrowsersService()
|
||||||
switch err {
|
switch err {
|
||||||
|
|
|
@ -365,9 +365,14 @@ func (k *ExtendedKey) Neuter() (*ExtendedKey, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsZeroed returns true if key is nil or empty
|
||||||
|
func (k *ExtendedKey) IsZeroed() bool {
|
||||||
|
return k == nil || len(k.KeyData) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// String returns the extended key as a human-readable base58-encoded string.
|
// String returns the extended key as a human-readable base58-encoded string.
|
||||||
func (k *ExtendedKey) String() string {
|
func (k *ExtendedKey) String() string {
|
||||||
if k == nil || len(k.KeyData) == 0 {
|
if k.IsZeroed() {
|
||||||
return EmptyExtendedKeyString
|
return EmptyExtendedKeyString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -30,6 +30,6 @@ require (
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.3
|
replace github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.5
|
||||||
|
|
||||||
replace github.com/NaySoftware/go-fcm => github.com/status-im/go-fcm v1.0.0-status
|
replace github.com/NaySoftware/go-fcm => github.com/status-im/go-fcm v1.0.0-status
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -440,6 +440,8 @@ github.com/status-im/doubleratchet v2.0.0+incompatible h1:s77lF1lDubK0RKftxN2vH8
|
||||||
github.com/status-im/doubleratchet v2.0.0+incompatible/go.mod h1:1sqR0+yhiM/bd+wrdX79AOt2csZuJOni0nUDzKNuqOU=
|
github.com/status-im/doubleratchet v2.0.0+incompatible/go.mod h1:1sqR0+yhiM/bd+wrdX79AOt2csZuJOni0nUDzKNuqOU=
|
||||||
github.com/status-im/go-ethereum v1.8.27-status.3 h1:h+CsF2Z/HyERLo5qTQnrK0RXHuDJ605hG2BbqNOLAAc=
|
github.com/status-im/go-ethereum v1.8.27-status.3 h1:h+CsF2Z/HyERLo5qTQnrK0RXHuDJ605hG2BbqNOLAAc=
|
||||||
github.com/status-im/go-ethereum v1.8.27-status.3/go.mod h1:Ulij8LMpMvXnbnPcmDqrpI+iXoXSjxItuY/wmbasTZU=
|
github.com/status-im/go-ethereum v1.8.27-status.3/go.mod h1:Ulij8LMpMvXnbnPcmDqrpI+iXoXSjxItuY/wmbasTZU=
|
||||||
|
github.com/status-im/go-ethereum v1.8.27-status.5 h1:FS+1KwA97yWh9BtHse0BLfEWtldboSiTMA7nCavxtzc=
|
||||||
|
github.com/status-im/go-ethereum v1.8.27-status.5/go.mod h1:Ulij8LMpMvXnbnPcmDqrpI+iXoXSjxItuY/wmbasTZU=
|
||||||
github.com/status-im/go-fcm v1.0.0-status h1:eUNKm4ooAXdEf9/GaTYeTELna5aVMOEbzjbm2irQ0gY=
|
github.com/status-im/go-fcm v1.0.0-status h1:eUNKm4ooAXdEf9/GaTYeTELna5aVMOEbzjbm2irQ0gY=
|
||||||
github.com/status-im/go-fcm v1.0.0-status/go.mod h1:0JGzul9SfemcbTdw0mkzLR42j+BTNci5aQWlI0o/uk8=
|
github.com/status-im/go-fcm v1.0.0-status/go.mod h1:0JGzul9SfemcbTdw0mkzLR42j+BTNci5aQWlI0o/uk8=
|
||||||
github.com/status-im/go-multiaddr-ethv4 v1.2.0 h1:OT84UsUzTCwguqCpJqkrCMiL4VZ1SvUtH9a5MsZupBk=
|
github.com/status-im/go-multiaddr-ethv4 v1.2.0 h1:OT84UsUzTCwguqCpJqkrCMiL4VZ1SvUtH9a5MsZupBk=
|
||||||
|
|
|
@ -0,0 +1,261 @@
|
||||||
|
// +build e2e_test
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"C"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
mobile "github.com/status-im/status-go/mobile"
|
||||||
|
)
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/account/generator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func checkMultiAccountResponse(t *testing.T, respJSON *C.char, resp interface{}) {
|
||||||
|
var e struct {
|
||||||
|
Error *string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
json.Unmarshal([]byte(C.GoString(respJSON)), &e)
|
||||||
|
if e.Error != nil {
|
||||||
|
t.Errorf("unexpected response error: %s", *e.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(respJSON)), resp); err != nil {
|
||||||
|
t.Fatalf("error unmarshaling response to expected struct: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMultiAccountGenerateDeriveAndStore(t *testing.T) bool { //nolint: gocyclo
|
||||||
|
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||||
|
if err := statusBackend.Logout(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := C.CString(`{
|
||||||
|
"n": 2,
|
||||||
|
"mnemonicPhraseLength": 24,
|
||||||
|
"bip39Passphrase": ""
|
||||||
|
}`)
|
||||||
|
|
||||||
|
// generate 2 random accounts
|
||||||
|
rawResp := MultiAccountGenerate(params)
|
||||||
|
var generateResp []generator.GeneratedAccountInfo
|
||||||
|
// check there's no error in the response
|
||||||
|
checkMultiAccountResponse(t, rawResp, &generateResp)
|
||||||
|
if len(generateResp) != 2 {
|
||||||
|
t.Errorf("expected 2 accounts created, got %d", len(generateResp))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
bip44DerivationPath := "m/44'/60'/0'/0/0"
|
||||||
|
eip1581DerivationPath := "m/43'/60'/1581'/0'/0"
|
||||||
|
paths := []string{bip44DerivationPath, eip1581DerivationPath}
|
||||||
|
|
||||||
|
// derive 2 child accounts for each account without storing them
|
||||||
|
for i := 0; i < len(generateResp); i++ {
|
||||||
|
info := generateResp[i]
|
||||||
|
mnemonicLength := len(strings.Split(info.Mnemonic, " "))
|
||||||
|
|
||||||
|
if mnemonicLength != 24 {
|
||||||
|
t.Errorf("expected mnemonic to have 24 words, got %d", mnemonicLength)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := testMultiAccountDeriveAddresses(t, info.ID, paths); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// store 2 derived child accounts from the first account.
|
||||||
|
// after that all the generated account should be remove from memory.
|
||||||
|
if ok := testMultiAccountStoreDerived(t, generateResp[0].ID, paths); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMultiAccountDeriveAddresses(t *testing.T, accountID string, paths []string) bool { //nolint: gocyclo
|
||||||
|
params := mobile.MultiAccountDeriveAddressesParams{
|
||||||
|
AccountID: accountID,
|
||||||
|
Paths: paths,
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsJSON, err := json.Marshal(¶ms)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error encoding MultiAccountDeriveAddressesParams")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// derive addresses from account accountID
|
||||||
|
rawResp := MultiAccountDeriveAddresses(C.CString(string(paramsJSON)))
|
||||||
|
var deriveResp map[string]generator.AccountInfo
|
||||||
|
// check the response doesn't have errors
|
||||||
|
checkMultiAccountResponse(t, rawResp, &deriveResp)
|
||||||
|
if len(deriveResp) != 2 {
|
||||||
|
t.Errorf("expected 2 derived accounts info, got %d", len(deriveResp))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that we have an address for each derivation path we used.
|
||||||
|
for _, path := range paths {
|
||||||
|
if _, ok := deriveResp[path]; !ok {
|
||||||
|
t.Errorf("results doesn't contain account info for path %s", path)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMultiAccountStoreDerived(t *testing.T, accountID string, paths []string) bool { //nolint: gocyclo
|
||||||
|
password := "test-multiaccount-password"
|
||||||
|
|
||||||
|
params := mobile.MultiAccountStoreDerivedParams{
|
||||||
|
MultiAccountDeriveAddressesParams: mobile.MultiAccountDeriveAddressesParams{
|
||||||
|
AccountID: accountID,
|
||||||
|
Paths: paths,
|
||||||
|
},
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsJSON, err := json.Marshal(params)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error encoding MultiAccountStoreDerivedParams")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// store one child account for each derivation path.
|
||||||
|
rawResp := MultiAccountStoreDerived(C.CString(string(paramsJSON)))
|
||||||
|
var storeResp map[string]generator.AccountInfo
|
||||||
|
|
||||||
|
// check that we don't have errors in the response
|
||||||
|
checkMultiAccountResponse(t, rawResp, &storeResp)
|
||||||
|
addresses := make([]string, 0)
|
||||||
|
for _, info := range storeResp {
|
||||||
|
addresses = append(addresses, info.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(addresses) != 2 {
|
||||||
|
t.Errorf("expected 2 addresses, got %d", len(addresses))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each stored account, check that we can decrypt it with the password we used.
|
||||||
|
dir := statusBackend.StatusNode().Config().DataDir
|
||||||
|
for _, address := range addresses {
|
||||||
|
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, address, password)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to verify password on stored derived account")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo
|
||||||
|
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||||
|
if err := statusBackend.Logout(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := []string{"m/0", "m/1"}
|
||||||
|
params := mobile.MultiAccountGenerateAndDeriveAddressesParams{
|
||||||
|
MultiAccountGenerateParams: mobile.MultiAccountGenerateParams{
|
||||||
|
N: 2,
|
||||||
|
MnemonicPhraseLength: 12,
|
||||||
|
},
|
||||||
|
Paths: paths,
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsJSON, err := json.Marshal(¶ms)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error encoding MultiAccountGenerateAndDeriveParams")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate 2 random accounts and derive 2 accounts from each one.
|
||||||
|
rawResp := MultiAccountGenerateAndDeriveAddresses(C.CString(string(paramsJSON)))
|
||||||
|
var generateResp []generator.GeneratedAndDerivedAccountInfo
|
||||||
|
// check there's no error in the response
|
||||||
|
checkMultiAccountResponse(t, rawResp, &generateResp)
|
||||||
|
if len(generateResp) != 2 {
|
||||||
|
t.Errorf("expected 2 accounts created, got %d", len(generateResp))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that for each account we have the 2 derived addresses
|
||||||
|
for _, info := range generateResp {
|
||||||
|
for _, path := range paths {
|
||||||
|
if _, ok := info.Derived[path]; !ok {
|
||||||
|
t.Errorf("results doesn't contain account info for path %s", path)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo
|
||||||
|
// to make sure that we start with empty account (which might have gotten populated during previous tests)
|
||||||
|
if err := statusBackend.Logout(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := crypto.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed generating key")
|
||||||
|
}
|
||||||
|
|
||||||
|
hex := fmt.Sprintf("%#x", crypto.FromECDSA(key))
|
||||||
|
importParams := mobile.MultiAccountImportPrivateKeyParams{
|
||||||
|
PrivateKey: hex,
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsJSON, err := json.Marshal(&importParams)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error encoding MultiAccountImportPrivateKeyParams")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// import raw private key
|
||||||
|
rawResp := MultiAccountImportPrivateKey(C.CString(string(paramsJSON)))
|
||||||
|
var importResp generator.IdentifiedAccountInfo
|
||||||
|
// check the response doesn't have errors
|
||||||
|
checkMultiAccountResponse(t, rawResp, &importResp)
|
||||||
|
|
||||||
|
// prepare StoreAccount params
|
||||||
|
password := "test-multiaccount-imported-key-password"
|
||||||
|
storeParams := mobile.MultiAccountStoreAccountParams{
|
||||||
|
AccountID: importResp.ID,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
paramsJSON, err = json.Marshal(storeParams)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error encoding MultiAccountStoreParams")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the imported private key
|
||||||
|
rawResp = MultiAccountStoreAccount(C.CString(string(paramsJSON)))
|
||||||
|
var storeResp generator.AccountInfo
|
||||||
|
// check the response doesn't have errors
|
||||||
|
checkMultiAccountResponse(t, rawResp, &storeResp)
|
||||||
|
|
||||||
|
dir := statusBackend.StatusNode().Config().DataDir
|
||||||
|
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, storeResp.Address, password)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to verify password on stored derived account")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -146,6 +146,18 @@ func testExportedAPI(t *testing.T, done chan struct{}) {
|
||||||
"failed single transaction",
|
"failed single transaction",
|
||||||
testFailedTransaction,
|
testFailedTransaction,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"MultiAccount - Generate/Derive/StoreDerived",
|
||||||
|
testMultiAccountGenerateDeriveAndStore,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"MultiAccount - GenerateAndDerive",
|
||||||
|
testMultiAccountGenerateAndDerive,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"MultiAccount - Import/Store",
|
||||||
|
testMultiAccountImportStore,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
@ -1018,16 +1030,3 @@ func testValidateNodeConfig(t *testing.T, config string, fn func(*testing.T, API
|
||||||
|
|
||||||
fn(t, resp)
|
fn(t, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PanicAfter throws panic() after waitSeconds, unless abort channel receives
|
|
||||||
// notification.
|
|
||||||
func PanicAfter(waitSeconds time.Duration, abort chan struct{}, desc string) {
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-abort:
|
|
||||||
return
|
|
||||||
case <-time.After(waitSeconds):
|
|
||||||
panic("whatever you were doing takes toooo long: " + desc)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// #include <stdlib.h>
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
mobile "github.com/status-im/status-go/mobile"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MultiAccountGenerate generates account in memory without storing them.
|
||||||
|
//export MultiAccountGenerate
|
||||||
|
func MultiAccountGenerate(paramsJSON *C.char) *C.char {
|
||||||
|
var p mobile.MultiAccountGenerateParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().Generate(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.CString(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountGenerateAndDeriveAddresses combines Generate and DeriveAddresses in one call.
|
||||||
|
//export MultiAccountGenerateAndDeriveAddresses
|
||||||
|
func MultiAccountGenerateAndDeriveAddresses(paramsJSON *C.char) *C.char {
|
||||||
|
var p mobile.MultiAccountGenerateAndDeriveAddressesParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().GenerateAndDeriveAddresses(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase, p.Paths)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.CString(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountDeriveAddresses derive addresses from an account selected by ID, without storing them.
|
||||||
|
//export MultiAccountDeriveAddresses
|
||||||
|
func MultiAccountDeriveAddresses(paramsJSON *C.char) *C.char {
|
||||||
|
var p mobile.MultiAccountDeriveAddressesParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().DeriveAddresses(p.AccountID, p.Paths)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.CString(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountStoreDerived derive accounts from the specified key and store them encrypted with the specified password.
|
||||||
|
//export MultiAccountStoreDerived
|
||||||
|
func MultiAccountStoreDerived(paramsJSON *C.char) *C.char {
|
||||||
|
var p mobile.MultiAccountStoreDerivedParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreDerivedAccounts(p.AccountID, p.Password, p.Paths)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.CString(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountImportPrivateKey imports a raw private key without storing it.
|
||||||
|
//export MultiAccountImportPrivateKey
|
||||||
|
func MultiAccountImportPrivateKey(paramsJSON *C.char) *C.char {
|
||||||
|
var p mobile.MultiAccountImportPrivateKeyParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().ImportPrivateKey(p.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.CString(string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountStoreAccount stores the select account.
|
||||||
|
//export MultiAccountStoreAccount
|
||||||
|
func MultiAccountStoreAccount(paramsJSON *C.char) *C.char {
|
||||||
|
var p mobile.MultiAccountStoreAccountParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreAccount(p.AccountID, p.Password)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return C.CString(string(out))
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
package statusgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MultiAccountGenerateParams are the params sent to MultiAccountGenerate.
|
||||||
|
type MultiAccountGenerateParams struct {
|
||||||
|
N int `json:"n"`
|
||||||
|
MnemonicPhraseLength int `json:"mnemonicPhraseLength"`
|
||||||
|
Bip39Passphrase string `json:"bip39Passphrase"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountGenerateAndDeriveAddressesParams are the params sent to MultiAccountGenerateAndDeriveAddresses.
|
||||||
|
type MultiAccountGenerateAndDeriveAddressesParams struct {
|
||||||
|
MultiAccountGenerateParams
|
||||||
|
Paths []string `json:"paths"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountDeriveAddressesParams are the params sent to MultiAccountDeriveAddresses.
|
||||||
|
type MultiAccountDeriveAddressesParams struct {
|
||||||
|
AccountID string `json:"accountID"`
|
||||||
|
Paths []string `json:"paths"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountStoreDerivedParams are the params sent to MultiAccountStoreDerived.
|
||||||
|
type MultiAccountStoreDerivedParams struct {
|
||||||
|
MultiAccountDeriveAddressesParams
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountStoreAccountParams are the params sent to MultiAccountStoreAccount.
|
||||||
|
type MultiAccountStoreAccountParams struct {
|
||||||
|
AccountID string `json:"accountID"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountImportPrivateKeyParams are the params sent to MultiAccountImportPrivateKey.
|
||||||
|
type MultiAccountImportPrivateKeyParams struct {
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountGenerate generates account in memory without storing them.
|
||||||
|
func MultiAccountGenerate(paramsJSON string) string {
|
||||||
|
var p MultiAccountGenerateParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().Generate(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountGenerateAndDeriveAddresses combines Generate and DeriveAddresses in one call.
|
||||||
|
func MultiAccountGenerateAndDeriveAddresses(paramsJSON string) string {
|
||||||
|
var p MultiAccountGenerateAndDeriveAddressesParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().GenerateAndDeriveAddresses(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase, p.Paths)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountDeriveAddresses derive addresses from an account selected by ID, without storing them.
|
||||||
|
func MultiAccountDeriveAddresses(paramsJSON string) string {
|
||||||
|
var p MultiAccountDeriveAddressesParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().DeriveAddresses(p.AccountID, p.Paths)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountStoreDerived derive accounts from the specified key and store them encrypted with the specified password.
|
||||||
|
func MultiAccountStoreDerived(paramsJSON string) string {
|
||||||
|
var p MultiAccountStoreDerivedParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreDerivedAccounts(p.AccountID, p.Password, p.Paths)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountImportPrivateKey imports a raw private key without storing it.
|
||||||
|
func MultiAccountImportPrivateKey(paramsJSON string) string {
|
||||||
|
var p MultiAccountImportPrivateKeyParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().ImportPrivateKey(p.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAccountStoreAccount stores the select account.
|
||||||
|
func MultiAccountStoreAccount(paramsJSON string) string {
|
||||||
|
var p MultiAccountStoreAccountParams
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreAccount(p.AccountID, p.Password)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return makeJSONResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(out)
|
||||||
|
}
|
|
@ -112,6 +112,14 @@ func activateServices(stack *node.Node, config *params.NodeConfig, db *leveldb.D
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := activateNodeServices(stack, config, db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func activateNodeServices(stack *node.Node, config *params.NodeConfig, db *leveldb.DB) error {
|
||||||
// start Whisper service.
|
// start Whisper service.
|
||||||
if err := activateShhService(stack, config, db); err != nil {
|
if err := activateShhService(stack, config, db); err != nil {
|
||||||
return fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)
|
return fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/status-im/status-go/account"
|
"github.com/status-im/status-go/account"
|
||||||
|
"github.com/status-im/status-go/extkeys"
|
||||||
e2e "github.com/status-im/status-go/t/e2e"
|
e2e "github.com/status-im/status-go/t/e2e"
|
||||||
. "github.com/status-im/status-go/t/utils"
|
. "github.com/status-im/status-go/t/utils"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
@ -82,6 +83,58 @@ func (s *AccountsTestSuite) TestAccountsList() {
|
||||||
s.False(!subAccount2MatchesKey1 && !subAccount2MatchesKey2, "subAcount2 not returned")
|
s.False(!subAccount2MatchesKey1 && !subAccount2MatchesKey2, "subAcount2 not returned")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AccountsTestSuite) TestImportSingleExtendedKey() {
|
||||||
|
s.StartTestBackend()
|
||||||
|
defer s.StopTestBackend()
|
||||||
|
|
||||||
|
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
|
||||||
|
s.NoError(err)
|
||||||
|
s.NotNil(keyStore)
|
||||||
|
|
||||||
|
// create a master extended key
|
||||||
|
mn := extkeys.NewMnemonic()
|
||||||
|
mnemonic, err := mn.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
|
||||||
|
s.NoError(err)
|
||||||
|
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, ""))
|
||||||
|
s.NoError(err)
|
||||||
|
derivedExtendedKey, err := extKey.EthBIP44Child(0)
|
||||||
|
s.NoError(err)
|
||||||
|
|
||||||
|
// import single extended key
|
||||||
|
password := "test-password-1"
|
||||||
|
addr, _, err := s.Backend.AccountManager().ImportSingleExtendedKey(derivedExtendedKey, password)
|
||||||
|
s.NoError(err)
|
||||||
|
|
||||||
|
_, key, err := s.Backend.AccountManager().AddressToDecryptedAccount(addr, password)
|
||||||
|
s.NoError(err)
|
||||||
|
|
||||||
|
s.Equal(crypto.FromECDSA(key.PrivateKey), crypto.FromECDSA(key.ExtendedKey.ToECDSA()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccountsTestSuite) TestImportAccount() {
|
||||||
|
s.StartTestBackend()
|
||||||
|
defer s.StopTestBackend()
|
||||||
|
|
||||||
|
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
|
||||||
|
s.NoError(err)
|
||||||
|
s.NotNil(keyStore)
|
||||||
|
|
||||||
|
// create a private key
|
||||||
|
privateKey, err := crypto.GenerateKey()
|
||||||
|
s.NoError(err)
|
||||||
|
|
||||||
|
// import as normal account
|
||||||
|
password := "test-password-2"
|
||||||
|
addr, err := s.Backend.AccountManager().ImportAccount(privateKey, password)
|
||||||
|
s.Require().NoError(err)
|
||||||
|
|
||||||
|
_, key, err := s.Backend.AccountManager().AddressToDecryptedAccount(addr.String(), password)
|
||||||
|
s.NoError(err)
|
||||||
|
|
||||||
|
s.Equal(crypto.FromECDSA(privateKey), crypto.FromECDSA(key.PrivateKey))
|
||||||
|
s.True(key.ExtendedKey.IsZeroed())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AccountsTestSuite) TestCreateChildAccount() {
|
func (s *AccountsTestSuite) TestCreateChildAccount() {
|
||||||
s.StartTestBackend()
|
s.StartTestBackend()
|
||||||
defer s.StopTestBackend()
|
defer s.StopTestBackend()
|
||||||
|
|
|
@ -47,8 +47,12 @@ type Key struct {
|
||||||
// we only store privkey as pubkey/address can be derived from it
|
// we only store privkey as pubkey/address can be derived from it
|
||||||
// privkey in this struct is always in plaintext
|
// privkey in this struct is always in plaintext
|
||||||
PrivateKey *ecdsa.PrivateKey
|
PrivateKey *ecdsa.PrivateKey
|
||||||
// extended key is the root node for new hardened children i.e. sub-accounts
|
// ExtendedKey is the extended key of the PrivateKey itself, and it's used
|
||||||
|
// to derive child keys.
|
||||||
ExtendedKey *extkeys.ExtendedKey
|
ExtendedKey *extkeys.ExtendedKey
|
||||||
|
// SubAccountIndex is DEPRECATED
|
||||||
|
// It was use in Status to keep track of the number of sub-account created
|
||||||
|
// before having multi-account support.
|
||||||
// next index to be used for sub-account child derivation
|
// next index to be used for sub-account child derivation
|
||||||
SubAccountIndex uint32
|
SubAccountIndex uint32
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import (
|
||||||
"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"
|
||||||
"github.com/ethereum/go-ethereum/event"
|
"github.com/ethereum/go-ethereum/event"
|
||||||
|
"github.com/pborman/uuid"
|
||||||
"github.com/status-im/status-go/extkeys"
|
"github.com/status-im/status-go/extkeys"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -459,14 +460,49 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco
|
||||||
return ks.importKey(key, passphrase)
|
return ks.importKey(key, passphrase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImportSingleExtendedKey imports an extended key setting it in both the PrivateKey and ExtendedKey fields
|
||||||
|
// of the Key struct.
|
||||||
|
// ImportExtendedKey is used in older version of Status where PrivateKey is set to be the BIP44 key at index 0,
|
||||||
|
// and ExtendedKey is the extended key of the BIP44 key at index 1.
|
||||||
|
func (ks *KeyStore) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
|
||||||
|
privateKeyECDSA := extKey.ToECDSA()
|
||||||
|
id := uuid.NewRandom()
|
||||||
|
key := &Key{
|
||||||
|
Id: id,
|
||||||
|
Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey),
|
||||||
|
PrivateKey: privateKeyECDSA,
|
||||||
|
ExtendedKey: extKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// if account is already imported, return cached version
|
||||||
|
if ks.cache.hasAddress(key.Address) {
|
||||||
|
a := accounts.Account{
|
||||||
|
Address: key.Address,
|
||||||
|
}
|
||||||
|
ks.cache.maybeReload()
|
||||||
|
ks.cache.mu.Lock()
|
||||||
|
a, err := ks.cache.find(a)
|
||||||
|
ks.cache.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
zeroKey(key.PrivateKey)
|
||||||
|
return a, err
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ks.importKey(key, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
// ImportExtendedKey stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts)
|
// ImportExtendedKey stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts)
|
||||||
// If key file is not found, it is created. Key is encrypted with the given passphrase.
|
// If key file is not found, it is created. Key is encrypted with the given passphrase.
|
||||||
|
// Deprecated: status-go is now using ImportSingleExtendedKey
|
||||||
func (ks *KeyStore) ImportExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
|
func (ks *KeyStore) ImportExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
|
||||||
return ks.ImportExtendedKeyForPurpose(extkeys.KeyPurposeWallet, extKey, passphrase)
|
return ks.ImportExtendedKeyForPurpose(extkeys.KeyPurposeWallet, extKey, passphrase)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportExtendedKeyForPurpose stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts)
|
// ImportExtendedKeyForPurpose stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts)
|
||||||
// If key file is not found, it is created. Key is encrypted with the given passphrase.
|
// If key file is not found, it is created. Key is encrypted with the given passphrase.
|
||||||
|
// Deprecated: status-go is now using ImportSingleExtendedKey
|
||||||
func (ks *KeyStore) ImportExtendedKeyForPurpose(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
|
func (ks *KeyStore) ImportExtendedKeyForPurpose(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
|
||||||
key, err := newKeyForPurposeFromExtendedKey(keyPurpose, extKey)
|
key, err := newKeyForPurposeFromExtendedKey(keyPurpose, extKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -198,6 +198,9 @@ func (s *dialstate) newTasks(nRunning int, peers map[enode.ID]*Peer, now time.Ti
|
||||||
case errNotWhitelisted, errSelf:
|
case errNotWhitelisted, errSelf:
|
||||||
log.Warn("Removing static dial candidate", "id", t.dest.ID, "addr", &net.TCPAddr{IP: t.dest.IP(), Port: t.dest.TCP()}, "err", err)
|
log.Warn("Removing static dial candidate", "id", t.dest.ID, "addr", &net.TCPAddr{IP: t.dest.IP(), Port: t.dest.TCP()}, "err", err)
|
||||||
delete(s.static, t.dest.ID())
|
delete(s.static, t.dest.ID())
|
||||||
|
case errRecentlyDialed:
|
||||||
|
expiry := s.hist.expiry(t.dest.ID())
|
||||||
|
log.Debug("peer was recently dialed", "enode", t.dest.String(), "expires at", expiry, "after", expiry.Sub(time.Now()))
|
||||||
case nil:
|
case nil:
|
||||||
s.dialing[id] = t.flags
|
s.dialing[id] = t.flags
|
||||||
newtasks = append(newtasks, t)
|
newtasks = append(newtasks, t)
|
||||||
|
@ -411,6 +414,16 @@ func (h dialHistory) contains(id enode.ID) bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h dialHistory) expiry(id enode.ID) time.Time {
|
||||||
|
for _, v := range h {
|
||||||
|
if v.id == id {
|
||||||
|
return v.exp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *dialHistory) expire(now time.Time) {
|
func (h *dialHistory) expire(now time.Time) {
|
||||||
for h.Len() > 0 && h.min().exp.Before(now) {
|
for h.Len() > 0 && h.min().exp.Before(now) {
|
||||||
heap.Pop(h)
|
heap.Pop(h)
|
||||||
|
|
|
@ -693,6 +693,7 @@ running:
|
||||||
// ephemeral static peer list. Add it to the dialer,
|
// ephemeral static peer list. Add it to the dialer,
|
||||||
// it will keep the node connected.
|
// it will keep the node connected.
|
||||||
srv.log.Trace("Adding static node", "node", n)
|
srv.log.Trace("Adding static node", "node", n)
|
||||||
|
dialstate.removeStatic(n)
|
||||||
dialstate.addStatic(n)
|
dialstate.addStatic(n)
|
||||||
case n := <-srv.removestatic:
|
case n := <-srv.removestatic:
|
||||||
// This channel is used by RemovePeer to send a
|
// This channel is used by RemovePeer to send a
|
||||||
|
|
|
@ -24,7 +24,7 @@ github.com/davecgh/go-spew/spew
|
||||||
github.com/deckarep/golang-set
|
github.com/deckarep/golang-set
|
||||||
# github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712
|
# github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712
|
||||||
github.com/edsrzf/mmap-go
|
github.com/edsrzf/mmap-go
|
||||||
# github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.3
|
# github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.5
|
||||||
github.com/ethereum/go-ethereum/accounts
|
github.com/ethereum/go-ethereum/accounts
|
||||||
github.com/ethereum/go-ethereum/accounts/keystore
|
github.com/ethereum/go-ethereum/accounts/keystore
|
||||||
github.com/ethereum/go-ethereum/common
|
github.com/ethereum/go-ethereum/common
|
||||||
|
|
Loading…
Reference in New Issue