status-go/api/geth_backend.go

1187 lines
33 KiB
Go
Raw Normal View History

package api
import (
"context"
"database/sql"
"errors"
"fmt"
"math/big"
2020-07-13 10:45:36 +00:00
"os"
"path/filepath"
2021-08-13 07:24:33 +00:00
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
2020-10-29 07:46:02 +00:00
signercore "github.com/ethereum/go-ethereum/signer/core"
2020-01-02 09:10:19 +00:00
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/appdatabase"
2021-05-14 10:55:42 +00:00
"github.com/status-im/status-go/connection"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/logutils"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/node"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/personal"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/signal"
"github.com/status-im/status-go/transactions"
)
var (
// ErrWhisperClearIdentitiesFailure clearing whisper identities has failed.
ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities")
// ErrWhisperIdentityInjectionFailure injecting whisper identities has failed.
ErrWhisperIdentityInjectionFailure = errors.New("failed to inject identity into Whisper")
// ErrWakuIdentityInjectionFailure injecting whisper identities has failed.
ErrWakuIdentityInjectionFailure = errors.New("failed to inject identity into waku")
// ErrUnsupportedRPCMethod is for methods not supported by the RPC interface
ErrUnsupportedRPCMethod = errors.New("method is unsupported by RPC interface")
// ErrRPCClientUnavailable is returned if an RPC client can't be retrieved.
// This is a normal situation when a node is stopped.
ErrRPCClientUnavailable = errors.New("JSON-RPC client is unavailable")
// ErrDBNotAvailable is returned if a method is called before the DB is available for usage
ErrDBNotAvailable = errors.New("DB is unavailable")
)
var _ StatusBackend = (*GethStatusBackend)(nil)
// GethStatusBackend implements the Status.im service over go-ethereum
type GethStatusBackend struct {
mu sync.Mutex
// rootDataDir is the same for all networks.
rootDataDir string
appDB *sql.DB
statusNode *node.StatusNode
personalAPI *personal.PublicAPI
multiaccountsDB *multiaccounts.Database
2020-12-09 14:03:43 +00:00
account *multiaccounts.Account
accountManager *account.GethManager
transactor *transactions.Transactor
2021-05-14 10:55:42 +00:00
connectionState connection.State
appState appState
selectedAccountKeyID string
log log.Logger
allowAllRPC bool // used only for tests, disables api method restrictions
}
// NewGethStatusBackend create a new GethStatusBackend instance
func NewGethStatusBackend() *GethStatusBackend {
2019-11-27 12:22:23 +00:00
defer log.Info("Status backend initialized", "backend", "geth", "version", params.Version, "commit", params.GitCommit)
2021-07-07 06:11:09 +00:00
backend := &GethStatusBackend{}
backend.initialize()
return backend
}
func (b *GethStatusBackend) initialize() {
statusNode := node.New()
2019-11-27 12:22:23 +00:00
accountManager := account.NewGethManager()
transactor := transactions.NewTransactor()
personalAPI := personal.NewAPI()
2021-07-07 06:11:09 +00:00
b.statusNode = statusNode
b.accountManager = accountManager
b.transactor = transactor
b.personalAPI = personalAPI
b.statusNode.SetMultiaccountsDB(b.multiaccountsDB)
b.log = log.New("package", "status-go/api.GethStatusBackend")
}
// StatusNode returns reference to node manager
func (b *GethStatusBackend) StatusNode() *node.StatusNode {
return b.statusNode
}
// AccountManager returns reference to account manager
func (b *GethStatusBackend) AccountManager() *account.GethManager {
return b.accountManager
}
// Transactor returns reference to a status transactor
func (b *GethStatusBackend) Transactor() *transactions.Transactor {
return b.transactor
}
// SelectedAccountKeyID returns a Whisper key ID of the selected chat key pair.
func (b *GethStatusBackend) SelectedAccountKeyID() string {
return b.selectedAccountKeyID
}
// IsNodeRunning confirm that node is running
func (b *GethStatusBackend) IsNodeRunning() bool {
return b.statusNode.IsRunning()
}
// StartNode start Status node, fails if node is already started
func (b *GethStatusBackend) StartNode(config *params.NodeConfig) error {
b.mu.Lock()
defer b.mu.Unlock()
if err := b.startNode(config); err != nil {
signal.SendNodeCrashed(err)
return err
}
return nil
}
func (b *GethStatusBackend) UpdateRootDataDir(datadir string) {
b.mu.Lock()
defer b.mu.Unlock()
b.rootDataDir = datadir
}
func (b *GethStatusBackend) OpenAccounts() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB != nil {
return nil
}
db, err := multiaccounts.InitializeDB(filepath.Join(b.rootDataDir, "accounts.sql"))
if err != nil {
2020-10-13 12:32:47 +00:00
b.log.Error("failed to initialize accounts db", "err", err)
return err
}
b.multiaccountsDB = db
2021-07-07 06:11:09 +00:00
// Probably we should iron out a bit better how to create/dispose of the status-service
b.statusNode.SetMultiaccountsDB(db)
return nil
}
func (b *GethStatusBackend) GetAccounts() ([]multiaccounts.Account, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB == nil {
return nil, errors.New("accounts db wasn't initialized")
}
return b.multiaccountsDB.GetAccounts()
}
func (b *GethStatusBackend) SaveAccount(account multiaccounts.Account) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB == nil {
return errors.New("accounts db wasn't initialized")
}
return b.multiaccountsDB.SaveAccount(account)
}
2020-07-13 10:45:36 +00:00
func (b *GethStatusBackend) DeleteMulticcount(keyUID string, keyStoreDir string) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB == nil {
return errors.New("accounts db wasn't initialized")
}
err := b.multiaccountsDB.DeleteAccount(keyUID)
if err != nil {
return err
}
dbFiles := []string{
filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql-shm", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql-wal", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db-shm", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db-wal", keyUID)),
2020-07-13 10:45:36 +00:00
}
for _, path := range dbFiles {
if _, err := os.Stat(path); err == nil {
err = os.Remove(path)
if err != nil {
return err
}
}
}
return os.RemoveAll(keyStoreDir)
}
2021-08-13 07:24:33 +00:00
func (b *GethStatusBackend) DeleteImportedKey(address, password, keyStoreDir string) error {
b.mu.Lock()
defer b.mu.Unlock()
err := filepath.Walk(keyStoreDir, func(path string, fileInfo os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.Contains(fileInfo.Name(), address) {
_, err := b.accountManager.VerifyAccountPassword(keyStoreDir, "0x"+address, password)
if err != nil {
b.log.Error("failed to verify account", "account", address, "error", err)
return err
}
return os.Remove(path)
}
return nil
})
return err
}
func (b *GethStatusBackend) ensureAppDBOpened(account multiaccounts.Account, password string) (err error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.appDB != nil {
return nil
}
if len(b.rootDataDir) == 0 {
return errors.New("root datadir wasn't provided")
}
// Migrate file path to fix issue https://github.com/status-im/status-go/issues/2027
oldPath := filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", account.KeyUID))
newPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", account.KeyUID))
_, err = os.Stat(oldPath)
if err == nil {
err := os.Rename(oldPath, newPath)
if err != nil {
return err
}
2020-09-25 07:31:55 +00:00
// rename journals as well, but ignore errors
_ = os.Rename(oldPath+"-shm", newPath+"-shm")
_ = os.Rename(oldPath+"-wal", newPath+"-wal")
}
b.appDB, err = appdatabase.InitializeDB(newPath, password)
if err != nil {
2020-10-13 12:32:47 +00:00
b.log.Error("failed to initialize db", "err", err)
return err
}
b.statusNode.SetAppDB(b.appDB)
return nil
}
// StartNodeWithKey instead of loading addresses from database this method derives address from key
// and uses it in application.
// TODO: we should use a proper struct with optional values instead of duplicating the regular functions
// with small variants for keycard, this created too many bugs
func (b *GethStatusBackend) startNodeWithKey(acc multiaccounts.Account, password string, keyHex string) error {
err := b.ensureAppDBOpened(acc, password)
if err != nil {
return err
}
conf, err := b.loadNodeConfig()
if err != nil {
return err
}
logSettings := logutils.LogSettings{
Enabled: conf.LogEnabled,
MobileSystem: conf.LogMobileSystem,
Level: conf.LogLevel,
File: conf.LogFile,
MaxSize: conf.LogMaxSize,
MaxBackups: conf.LogMaxBackups,
CompressRotated: conf.LogCompressRotated,
}
if err := logutils.OverrideRootLogWithConfig(logSettings, false); err != nil {
return err
}
2020-12-09 14:03:43 +00:00
b.account = &acc
accountsDB := accounts.NewDB(b.appDB)
walletAddr, err := accountsDB.GetWalletAddress()
if err != nil {
return err
}
watchAddrs, err := accountsDB.GetAddresses()
if err != nil {
return err
}
chatKey, err := ethcrypto.HexToECDSA(keyHex)
if err != nil {
return err
}
err = b.StartNode(conf)
if err != nil {
return err
}
2021-07-12 11:18:46 +00:00
if err := b.accountManager.SetChatAccount(chatKey); err != nil {
return err
}
_, err = b.accountManager.SelectedChatAccount()
if err != nil {
return err
}
b.accountManager.SetAccountAddresses(walletAddr, watchAddrs...)
err = b.injectAccountsIntoServices()
if err != nil {
return err
}
err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
if err != nil {
return err
}
return nil
}
func (b *GethStatusBackend) StartNodeWithKey(acc multiaccounts.Account, password string, keyHex string) error {
err := b.startNodeWithKey(acc, password, keyHex)
if err != nil {
// Stop node for clean up
_ = b.StopNode()
}
signal.SendLoggedIn(err)
return err
}
func (b *GethStatusBackend) startNodeWithAccount(acc multiaccounts.Account, password string) error {
err := b.ensureAppDBOpened(acc, password)
if err != nil {
return err
}
conf, err := b.loadNodeConfig()
if err != nil {
return err
}
logSettings := logutils.LogSettings{
Enabled: conf.LogEnabled,
MobileSystem: conf.LogMobileSystem,
Level: conf.LogLevel,
File: conf.LogFile,
MaxSize: conf.LogMaxSize,
MaxBackups: conf.LogMaxBackups,
CompressRotated: conf.LogCompressRotated,
}
if err := logutils.OverrideRootLogWithConfig(logSettings, false); err != nil {
return err
}
2020-12-09 14:03:43 +00:00
b.account = &acc
accountsDB := accounts.NewDB(b.appDB)
chatAddr, err := accountsDB.GetChatAddress()
if err != nil {
return err
}
walletAddr, err := accountsDB.GetWalletAddress()
if err != nil {
return err
}
status-im/status-react#9203 Faster tx fetching with less request *** How it worked before this PR on multiaccount creation: - On multiacc creation we scanned chain for eth and erc20 transfers. For each address of a new empty multiaccount this scan required 1. two `eth_getBalance` requests to find out that there is no any balance change between zero and the last block, for eth transfers 2. and `chain-size/100000` (currently ~100) `eth_getLogs` requests, for erc20 transfers - For some reason we scanned an address of the chat account as well, and also accounts were not deduplicated. So even for an empty multiacc we scanned chain twice for each chat and main wallet addresses, in result app had to execute about 400 requests. - As mentioned above, `eth_getBalance` requests were used to check if there were any eth transfers, and that caused empty history in case if user already used all available eth (so that both zero and latest blocks show 0 eth for an address). There might have been transactions but we wouldn't fetch/show them. - There was no upper limit for the number of rpc requests during the scan, so it could require indefinite number of requests; the scanning algorithm was written so that we persisted the whole history of transactions or tried to scan form the beginning again in case of failure, giving up only after 10 minutes of failures. In result addresses with sufficient number of transactions would never be fully scanned and during these 10 minutes app could use gigabytes of internet data. - Failures were caused by `eth_getBlockByNumber`/`eth_getBlockByHash` requests. These requests return significantly bigger responses than `eth_getBalance`/`eth_transactionsCount` and it is likely that execution of thousands of them in parallel caused failures for accounts with hundreds of transactions. Even for an account with 12k we could successfully determine blocks with transaction in a few minutes using `eth_getBalance` requests, but `eth_getBlock...` couldn't be processed for this acc. - There was no caching for for `eth_getBalance` requests, and this caused in average 3-4 times more such requests than is needed. *** How it works now on multiaccount creation: - On multiacc creation we scan chain for last ~30 eth transactions and then check erc20 in the range where these eth transactions were found. For an empty address in multiacc this means: 1. two `eth_getBalance` transactions to determine that there was no balance change between zero and the last block; two `eth_transactionsCount` requests to determine there are no outgoing transactions for this address; total 4 requests for eth transfers 2. 20 `eth_getLogs` for erc20 transfers. This number can be lowered, but that's not a big deal - Deduplication of addresses is added and also we don't scan chat account, so a new multiacc requires ~25 (we also request latest block number and probably execute a few other calls) request to determine that multiacc is empty (comparing to ~400 before) - In case if address contains transactions we: 1. determine the range which contains 20-25 outgoing eth/erc20 transactions. This usually requires up to 10 `eth_transactionCount` requests 2. then we scan chain for eth transfers using `eth_getBalance` and `eth_transactionCount` (for double checking zero balances) 3. we make sure that we do not scan db for more than 30 blocks with transfers. That's important for accounts with mostly incoming transactions, because the range found on the first step might contain any number of incoming transfers, but only 20-25 outgoing transactions 4. when we found ~30 blocks in a given range, we update initial range `from` block using the oldest found block 5. and now we scan db for erc20transfers using `eth_getLogs` `oldest-found-eth-block`-`latest-block`, we make not more than 20 calls 6. when all blocks which contain incoming/outgoing transfers for a given address are found, we save these blocks to db and mark that transfers from these blocks are still to be fetched 7. Then we select latest ~30 (the number can be adjusted) blocks from these which were found and fetch transfers, this requires 3-4 requests per transfer. 8. we persist scanned range so that we know were to start next time 9. we dispatch an event which tells client that transactions are found 10. client fetches latest 20 transfers - when user presses "fetch more" button we check if app's db contains next 20 transfers, if not we scan chain again and return transfers after small fixes
2019-12-18 11:01:46 +00:00
watchAddrs, err := accountsDB.GetWalletAddresses()
if err != nil {
return err
}
login := account.LoginParams{
Password: password,
ChatAddress: chatAddr,
WatchAddresses: watchAddrs,
MainAccount: walletAddr,
}
err = b.StartNode(conf)
if err != nil {
return err
}
err = b.SelectAccount(login)
if err != nil {
return err
}
err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
if err != nil {
return err
}
return nil
}
func (b *GethStatusBackend) MigrateKeyStoreDir(acc multiaccounts.Account, password, oldDir, newDir string) error {
err := b.ensureAppDBOpened(acc, password)
if err != nil {
return err
}
accountDB := accounts.NewDB(b.appDB)
accounts, err := accountDB.GetAccounts()
if err != nil {
return err
}
settings, err := accountDB.GetSettings()
if err != nil {
return err
}
addresses := []string{settings.EIP1581Address.Hex(), settings.WalletRootAddress.Hex()}
for _, account := range accounts {
addresses = append(addresses, account.Address.Hex())
}
err = b.accountManager.MigrateKeyStoreDir(oldDir, newDir, addresses)
if err != nil {
return err
}
return nil
}
func (b *GethStatusBackend) StartNodeWithAccount(acc multiaccounts.Account, password string) error {
err := b.startNodeWithAccount(acc, password)
if err != nil {
// Stop node for clean up
_ = b.StopNode()
}
signal.SendLoggedIn(err)
return err
}
2021-01-07 11:15:02 +00:00
func (b *GethStatusBackend) ExportUnencryptedDatabase(acc multiaccounts.Account, password, directory string) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.appDB != nil {
return nil
}
if len(b.rootDataDir) == 0 {
return errors.New("root datadir wasn't provided")
}
// Migrate file path to fix issue https://github.com/status-im/status-go/issues/2027
oldPath := filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", acc.KeyUID))
newPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", acc.KeyUID))
_, err := os.Stat(oldPath)
if err == nil {
err := os.Rename(oldPath, newPath)
if err != nil {
return err
}
// rename journals as well, but ignore errors
_ = os.Rename(oldPath+"-shm", newPath+"-shm")
_ = os.Rename(oldPath+"-wal", newPath+"-wal")
}
err = appdatabase.DecryptDatabase(newPath, directory, password)
if err != nil {
b.log.Error("failed to initialize db", "err", err)
return err
}
return nil
}
func (b *GethStatusBackend) ImportUnencryptedDatabase(acc multiaccounts.Account, password, databasePath string) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.appDB != nil {
return nil
}
if len(b.rootDataDir) == 0 {
return errors.New("root datadir wasn't provided")
}
path := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", acc.KeyUID))
err := appdatabase.EncryptDatabase(databasePath, path, password)
if err != nil {
b.log.Error("failed to initialize db", "err", err)
return err
}
return nil
}
func (b *GethStatusBackend) ChangeDatabasePassword(keyUID string, password string, newPassword string) error {
dbPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", keyUID))
config := b.StatusNode().Config()
keyDir := config.KeyStoreDir
err := b.accountManager.ReEncryptKeyStoreDir(keyDir, password, newPassword)
if err != nil {
return fmt.Errorf("ReEncryptKeyStoreDir error: %v", err)
}
err = appdatabase.ChangeDatabasePassword(dbPath, password, newPassword)
if err != nil {
// couldn't change db password so undo keystore changes to mainitain consistency
_ = b.accountManager.ReEncryptKeyStoreDir(keyDir, newPassword, password)
return err
}
return nil
}
2021-07-20 11:48:10 +00:00
func (b *GethStatusBackend) ConvertToKeycardAccount(keyStoreDir string, account multiaccounts.Account, settings accounts.Settings, password string, newPassword string) error {
err := b.multiaccountsDB.UpdateAccountKeycardPairing(account)
if err != nil {
return err
}
err = b.ensureAppDBOpened(account, password)
if err != nil {
return err
}
accountDB := accounts.NewDB(b.appDB)
err = accountDB.SaveSetting("keycard-instance_uid", settings.KeycardInstanceUID)
if err != nil {
return err
}
err = accountDB.SaveSetting("keycard-paired_on", settings.KeycardPAiredOn)
if err != nil {
return err
}
err = accountDB.SaveSetting("keycard-pairing", settings.KeycardPairing)
if err != nil {
return err
}
err = accountDB.SaveSetting("mnemonic", nil)
if err != nil {
return err
}
err = accountDB.DeleteSeedAndKeyAccounts()
if err != nil {
return err
}
dbPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", account.KeyUID))
err = appdatabase.ChangeDatabasePassword(dbPath, password, newPassword)
if err != nil {
return err
}
err = b.closeAppDB()
if err != nil {
return err
}
return os.RemoveAll(keyStoreDir)
}
func (b *GethStatusBackend) VerifyDatabasePassword(keyUID string, password string) error {
err := b.ensureAppDBOpened(multiaccounts.Account{KeyUID: keyUID}, password)
if err != nil {
return err
}
accountsDB := accounts.NewDB(b.appDB)
_, err = accountsDB.GetWalletAddress()
if err != nil {
return err
}
err = b.closeAppDB()
if err != nil {
return err
}
return nil
}
func (b *GethStatusBackend) SaveAccountAndStartNodeWithKey(acc multiaccounts.Account, password string, settings accounts.Settings, nodecfg *params.NodeConfig, subaccs []accounts.Account, keyHex string) error {
err := b.SaveAccount(acc)
if err != nil {
return err
}
err = b.ensureAppDBOpened(acc, password)
if err != nil {
return err
}
err = b.saveAccountsAndSettings(settings, nodecfg, subaccs)
if err != nil {
return err
}
return b.StartNodeWithKey(acc, password, keyHex)
}
// StartNodeWithAccountAndConfig is used after account and config was generated.
// In current setup account name and config is generated on the client side. Once/if it will be generated on
// status-go side this flow can be simplified.
func (b *GethStatusBackend) StartNodeWithAccountAndConfig(
account multiaccounts.Account,
password string,
settings accounts.Settings,
nodecfg *params.NodeConfig,
subaccs []accounts.Account,
) error {
err := b.SaveAccount(account)
if err != nil {
return err
}
err = b.ensureAppDBOpened(account, password)
if err != nil {
return err
}
err = b.saveAccountsAndSettings(settings, nodecfg, subaccs)
if err != nil {
return err
}
return b.StartNodeWithAccount(account, password)
}
func (b *GethStatusBackend) saveAccountsAndSettings(settings accounts.Settings, nodecfg *params.NodeConfig, subaccs []accounts.Account) error {
b.mu.Lock()
defer b.mu.Unlock()
accdb := accounts.NewDB(b.appDB)
err := accdb.CreateSettings(settings, *nodecfg)
if err != nil {
return err
}
return accdb.SaveAccounts(subaccs)
}
func (b *GethStatusBackend) loadNodeConfig() (*params.NodeConfig, error) {
b.mu.Lock()
defer b.mu.Unlock()
var conf params.NodeConfig
accountDB := accounts.NewDB(b.appDB)
err := accountDB.GetNodeConfig(&conf)
if err != nil {
return nil, err
}
// Start WakuV1 if WakuV2 is not enabled
conf.WakuConfig.Enabled = !conf.WakuV2Config.Enabled
2020-09-16 07:31:01 +00:00
// NodeConfig.Version should be taken from params.Version
// which is set at the compile time.
// What's cached is usually outdated so we overwrite it here.
conf.Version = params.Version
conf.DataDir = filepath.Join(b.rootDataDir, conf.DataDir)
conf.ShhextConfig.BackupDisabledDataDir = filepath.Join(b.rootDataDir, conf.ShhextConfig.BackupDisabledDataDir)
if len(conf.LogDir) == 0 {
conf.LogFile = filepath.Join(b.rootDataDir, conf.LogFile)
} else {
conf.LogFile = filepath.Join(conf.LogDir, conf.LogFile)
}
conf.KeyStoreDir = filepath.Join(b.rootDataDir, conf.KeyStoreDir)
return &conf, nil
}
func (b *GethStatusBackend) GetNodeConfig() (*params.NodeConfig, error) {
if b.appDB == nil {
return nil, ErrDBNotAvailable
}
return b.loadNodeConfig()
}
func (b *GethStatusBackend) startNode(config *params.NodeConfig) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("node crashed on start: %v", err)
}
}()
b.log.Debug("starting node with config", "config", config)
// Update config with some defaults.
if err := config.UpdateWithDefaults(); err != nil {
return err
}
b.log.Debug("updated config with defaults", "config", config)
// Start by validating configuration
if err := config.Validate(); err != nil {
return err
}
manager := b.accountManager.GetManager()
if manager == nil {
return errors.New("ethereum accounts.Manager is nil")
}
if err = b.statusNode.StartWithOptions(config, node.StartOptions{
// The peers discovery protocols are started manually after
// `node.ready` signal is sent.
// It was discussed in https://github.com/status-im/status-go/pull/1333.
StartDiscovery: false,
AccountsManager: manager,
}); err != nil {
return
}
signal.SendNodeStarted()
b.transactor.SetNetworkID(config.NetworkID)
b.transactor.SetRPC(b.statusNode.RPCClient(), rpc.DefaultCallTimeout)
2021-07-12 09:44:26 +00:00
b.personalAPI.SetRPC(b.statusNode.RPCClient(), rpc.DefaultCallTimeout)
if err = b.registerHandlers(); err != nil {
b.log.Error("Handler registration failed", "err", err)
return
}
b.log.Info("Handlers registered")
// Handle a case when a node is stopped and resumed.
// If there is no account selected, an error is returned.
if _, err := b.accountManager.SelectedChatAccount(); err == nil {
if err := b.injectAccountsIntoServices(); err != nil {
return err
}
} else if err != account.ErrNoAccountSelected {
return err
}
signal.SendNodeReady()
if err := b.statusNode.StartDiscovery(); err != nil {
return err
}
return nil
}
// StopNode stop Status node. Stopped node cannot be resumed.
func (b *GethStatusBackend) StopNode() error {
b.mu.Lock()
defer b.mu.Unlock()
return b.stopNode()
}
func (b *GethStatusBackend) stopNode() error {
2021-07-07 06:11:09 +00:00
if b.statusNode == nil || !b.IsNodeRunning() {
return nil
}
defer signal.SendNodeStopped()
2021-07-07 06:11:09 +00:00
return b.statusNode.Stop()
}
// RestartNode restart running Status node, fails if node is not running
func (b *GethStatusBackend) RestartNode() error {
b.mu.Lock()
defer b.mu.Unlock()
if !b.IsNodeRunning() {
return node.ErrNoRunningNode
}
newcfg := *(b.statusNode.Config())
if err := b.stopNode(); err != nil {
return err
}
return b.startNode(&newcfg)
}
// ResetChainData remove chain data from data directory.
// Node is stopped, and new node is started, with clean data directory.
func (b *GethStatusBackend) ResetChainData() error {
b.mu.Lock()
defer b.mu.Unlock()
newcfg := *(b.statusNode.Config())
if err := b.stopNode(); err != nil {
return err
}
// config is cleaned when node is stopped
if err := b.statusNode.ResetChainData(&newcfg); err != nil {
return err
}
signal.SendChainDataRemoved()
return b.startNode(&newcfg)
}
// CallRPC executes public RPC requests on node's in-proc RPC server.
func (b *GethStatusBackend) CallRPC(inputJSON string) (string, error) {
client := b.statusNode.RPCClient()
if client == nil {
return "", ErrRPCClientUnavailable
}
return client.CallRaw(inputJSON), nil
}
// CallPrivateRPC executes public and private RPC requests on node's in-proc RPC server.
func (b *GethStatusBackend) CallPrivateRPC(inputJSON string) (string, error) {
client := b.statusNode.RPCClient()
if client == nil {
return "", ErrRPCClientUnavailable
}
return client.CallRaw(inputJSON), nil
}
// SendTransaction creates a new transaction and waits until it's complete.
func (b *GethStatusBackend) SendTransaction(sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error) {
verifiedAccount, err := b.getVerifiedWalletAccount(sendArgs.From.String(), password)
if err != nil {
return hash, err
}
hash, err = b.transactor.SendTransaction(sendArgs, verifiedAccount)
if err != nil {
return
}
go b.statusNode.RPCFiltersService().TriggerTransactionSentToUpstreamEvent(hash)
return
}
func (b *GethStatusBackend) SendTransactionWithSignature(sendArgs transactions.SendTxArgs, sig []byte) (hash types.Hash, err error) {
hash, err = b.transactor.SendTransactionWithSignature(sendArgs, sig)
if err != nil {
return
}
go b.statusNode.RPCFiltersService().TriggerTransactionSentToUpstreamEvent(hash)
return
}
// HashTransaction validate the transaction and returns new sendArgs and the transaction hash.
func (b *GethStatusBackend) HashTransaction(sendArgs transactions.SendTxArgs) (transactions.SendTxArgs, types.Hash, error) {
return b.transactor.HashTransaction(sendArgs)
}
// SignMessage checks the pwd vs the selected account and passes on the signParams
// to personalAPI for message signature
func (b *GethStatusBackend) SignMessage(rpcParams personal.SignParams) (types.HexBytes, error) {
verifiedAccount, err := b.getVerifiedWalletAccount(rpcParams.Address, rpcParams.Password)
if err != nil {
return types.HexBytes{}, err
}
return b.personalAPI.Sign(rpcParams, verifiedAccount)
}
// Recover calls the personalAPI to return address associated with the private
// key that was used to calculate the signature in the message
func (b *GethStatusBackend) Recover(rpcParams personal.RecoverParams) (types.Address, error) {
return b.personalAPI.Recover(rpcParams)
}
// SignTypedData accepts data and password. Gets verified account and signs typed data.
func (b *GethStatusBackend) SignTypedData(typed typeddata.TypedData, address string, password string) (types.HexBytes, error) {
account, err := b.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
sig, err := typeddata.Sign(typed, account.AccountKey.PrivateKey, chain)
if err != nil {
return types.HexBytes{}, err
}
return types.HexBytes(sig), err
}
2020-10-29 07:46:02 +00:00
// SignTypedDataV4 accepts data and password. Gets verified account and signs typed data.
func (b *GethStatusBackend) SignTypedDataV4(typed signercore.TypedData, address string, password string) (types.HexBytes, error) {
account, err := b.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
sig, err := typeddata.SignTypedDataV4(typed, account.AccountKey.PrivateKey, chain)
if err != nil {
return types.HexBytes{}, err
}
return types.HexBytes(sig), err
}
// HashTypedData generates the hash of TypedData.
func (b *GethStatusBackend) HashTypedData(typed typeddata.TypedData) (types.Hash, error) {
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
hash, err := typeddata.ValidateAndHash(typed, chain)
if err != nil {
return types.Hash{}, err
}
return types.Hash(hash), err
}
2020-10-29 07:46:02 +00:00
// HashTypedDataV4 generates the hash of TypedData.
func (b *GethStatusBackend) HashTypedDataV4(typed signercore.TypedData) (types.Hash, error) {
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
hash, err := typeddata.HashTypedDataV4(typed, chain)
if err != nil {
return types.Hash{}, err
}
return types.Hash(hash), err
}
func (b *GethStatusBackend) getVerifiedWalletAccount(address, password string) (*account.SelectedExtKey, error) {
config := b.StatusNode().Config()
db := accounts.NewDB(b.appDB)
2019-12-11 13:59:37 +00:00
exists, err := db.AddressExists(types.HexToAddress(address))
if err != nil {
b.log.Error("failed to query db for a given address", "address", address, "error", err)
return nil, err
}
if !exists {
b.log.Error("failed to get a selected account", "err", transactions.ErrInvalidTxSender)
return nil, transactions.ErrAccountDoesntExist
}
key, err := b.accountManager.VerifyAccountPassword(config.KeyStoreDir, address, password)
if err != nil {
b.log.Error("failed to verify account", "account", address, "error", err)
return nil, err
}
return &account.SelectedExtKey{
Address: key.Address,
AccountKey: key,
}, nil
}
// registerHandlers attaches Status callback handlers to running node
func (b *GethStatusBackend) registerHandlers() error {
var clients []*rpc.Client
if c := b.StatusNode().RPCClient(); c != nil {
clients = append(clients, c)
} else {
return errors.New("RPC client unavailable")
}
for _, client := range clients {
client.RegisterHandler(
params.AccountsMethodName,
func(context.Context, ...interface{}) (interface{}, error) {
return b.accountManager.Accounts()
},
)
if b.allowAllRPC {
// this should only happen in unit-tests, this variable is not available outside this package
continue
}
client.RegisterHandler(params.SendTransactionMethodName, unsupportedMethodHandler)
client.RegisterHandler(params.PersonalSignMethodName, unsupportedMethodHandler)
client.RegisterHandler(params.PersonalRecoverMethodName, unsupportedMethodHandler)
}
return nil
}
func unsupportedMethodHandler(ctx context.Context, rpcParams ...interface{}) (interface{}, error) {
return nil, ErrUnsupportedRPCMethod
}
// ConnectionChange handles network state changes logic.
func (b *GethStatusBackend) ConnectionChange(typ string, expensive bool) {
b.mu.Lock()
defer b.mu.Unlock()
2021-05-14 10:55:42 +00:00
state := connection.State{
Type: connection.NewConnectionType(typ),
Expensive: expensive,
}
2021-05-14 10:55:42 +00:00
if typ == connection.None {
state.Offline = true
}
b.log.Info("Network state change", "old", b.connectionState, "new", state)
b.connectionState = state
b.statusNode.ConnectionChanged(state)
// logic of handling state changes here
// restart node? force peers reconnect? etc
}
// AppStateChange handles app state changes (background/foreground).
// state values: see https://facebook.github.io/react-native/docs/appstate.html
func (b *GethStatusBackend) AppStateChange(state string) {
s, err := parseAppState(state)
if err != nil {
log.Error("AppStateChange failed, ignoring", "error", err)
return // and do nothing
}
b.log.Info("App State changed", "new-state", s)
b.appState = s
// TODO: put node in low-power mode if the app is in background (or inactive)
// and normal mode if the app is in foreground.
}
func (b *GethStatusBackend) StopLocalNotifications() error {
if b.statusNode == nil {
return nil
}
return b.statusNode.StopLocalNotifications()
}
func (b *GethStatusBackend) StartLocalNotifications() error {
if b.statusNode == nil {
return nil
}
return b.statusNode.StartLocalNotifications()
}
// Logout clears whisper identities.
func (b *GethStatusBackend) Logout() error {
b.mu.Lock()
defer b.mu.Unlock()
err := b.cleanupServices()
if err != nil {
return err
}
err = b.closeAppDB()
if err != nil {
return err
}
b.AccountManager().Logout()
2021-07-07 06:11:09 +00:00
b.appDB = nil
2021-07-07 06:11:09 +00:00
if b.statusNode != nil {
if err := b.statusNode.Stop(); err != nil {
return err
}
b.statusNode = nil
}
// re-initialize the node, at some point we should better manage the lifecycle
b.initialize()
return nil
}
// cleanupServices stops parts of services that doesn't managed by a node and removes injected data from services.
func (b *GethStatusBackend) cleanupServices() error {
b.selectedAccountKeyID = ""
if b.statusNode == nil {
return nil
}
return b.statusNode.Cleanup()
}
func (b *GethStatusBackend) closeAppDB() error {
if b.appDB != nil {
err := b.appDB.Close()
if err != nil {
return err
}
b.appDB = nil
return nil
}
return nil
}
// SelectAccount selects current wallet and chat accounts, by verifying that each address has corresponding account which can be decrypted
// using provided password. Once verification is done, the decrypted chat key is injected into Whisper (as a single identity,
// all previous identities are removed).
func (b *GethStatusBackend) SelectAccount(loginParams account.LoginParams) error {
b.mu.Lock()
defer b.mu.Unlock()
b.AccountManager().RemoveOnboarding()
err := b.accountManager.SelectAccount(loginParams)
if err != nil {
return err
}
if loginParams.MultiAccount != nil {
b.account = loginParams.MultiAccount
}
if err := b.injectAccountsIntoServices(); err != nil {
return err
}
return nil
}
2020-12-09 14:03:43 +00:00
func (b *GethStatusBackend) GetActiveAccount() (*multiaccounts.Account, error) {
2020-12-09 14:45:59 +00:00
if b.account == nil {
2020-12-09 14:36:40 +00:00
return nil, errors.New("master key account is nil in the GethStatusBackend")
2020-12-09 14:45:59 +00:00
}
2020-12-09 14:03:43 +00:00
return b.account, nil
}
func (b *GethStatusBackend) injectAccountsIntoServices() error {
chatAccount, err := b.accountManager.SelectedChatAccount()
if err != nil {
return err
}
identity := chatAccount.AccountKey.PrivateKey
2020-12-09 14:03:43 +00:00
acc, err := b.GetActiveAccount()
if err != nil {
return err
}
wakuService := b.statusNode.WakuService()
if wakuService != nil {
if err := wakuService.DeleteKeyPairs(); err != nil { // err is not possible; method return value is incorrect
return err
}
b.selectedAccountKeyID, err = wakuService.AddKeyPair(identity)
if err != nil {
return ErrWakuIdentityInjectionFailure
}
st := b.statusNode.WakuExtService()
if st != nil {
if err := st.InitProtocol(identity, b.appDB, b.multiaccountsDB, acc, logutils.ZapLogger()); err != nil {
return err
}
// Set initial connection state
st.ConnectionChanged(b.connectionState)
2021-08-05 13:27:47 +00:00
messenger := st.Messenger()
// Init public status api
b.statusNode.StatusPublicService().Init(messenger)
}
2021-05-14 10:55:42 +00:00
}
wakuV2Service := b.statusNode.WakuV2Service()
if wakuV2Service != nil {
if err := wakuV2Service.DeleteKeyPairs(); err != nil { // err is not possible; method return value is incorrect
return err
}
b.selectedAccountKeyID, err = wakuV2Service.AddKeyPair(identity)
if err != nil {
return ErrWakuIdentityInjectionFailure
}
st := b.statusNode.WakuV2ExtService()
if err := st.InitProtocol(identity, b.appDB, b.multiaccountsDB, acc, logutils.ZapLogger()); err != nil {
return err
}
}
return nil
}
// ExtractGroupMembershipSignatures extract signatures from tuples of content/signature
func (b *GethStatusBackend) ExtractGroupMembershipSignatures(signaturePairs [][2]string) ([]string, error) {
return crypto.ExtractSignatures(signaturePairs)
}
// SignGroupMembership signs a piece of data containing membership information
func (b *GethStatusBackend) SignGroupMembership(content string) (string, error) {
selectedChatAccount, err := b.accountManager.SelectedChatAccount()
if err != nil {
return "", err
}
return crypto.SignStringAsHex(content, selectedChatAccount.AccountKey.PrivateKey)
}
// SignHash exposes vanilla ECDSA signing for signing a message for Swarm
func (b *GethStatusBackend) SignHash(hexEncodedHash string) (string, error) {
hash, err := hexutil.Decode(hexEncodedHash)
if err != nil {
return "", fmt.Errorf("SignHash: could not unmarshal the input: %v", err)
}
chatAccount, err := b.accountManager.SelectedChatAccount()
if err != nil {
return "", fmt.Errorf("SignHash: could not select account: %v", err.Error())
}
signature, err := ethcrypto.Sign(hash, chatAccount.AccountKey.PrivateKey)
if err != nil {
return "", fmt.Errorf("SignHash: could not sign the hash: %v", err)
}
hexEncodedSignature := types.EncodeHex(signature)
return hexEncodedSignature, nil
}