mirror of
https://github.com/status-im/status-go.git
synced 2025-01-21 20:20:29 +00:00
9268f55d7d
When the node registers topics, Discovery V5 should never be stopped as otherwise, no peer will be able to find the node.
530 lines
12 KiB
Go
530 lines
12 KiB
Go
package node
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/accounts"
|
|
"github.com/ethereum/go-ethereum/accounts/keystore"
|
|
"github.com/ethereum/go-ethereum/les"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"github.com/ethereum/go-ethereum/node"
|
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
|
whisper "github.com/ethereum/go-ethereum/whisper/whisperv6"
|
|
"github.com/syndtr/goleveldb/leveldb"
|
|
|
|
"github.com/status-im/status-go/geth/db"
|
|
"github.com/status-im/status-go/geth/params"
|
|
"github.com/status-im/status-go/geth/peers"
|
|
"github.com/status-im/status-go/geth/rpc"
|
|
"github.com/status-im/status-go/services/status"
|
|
)
|
|
|
|
// tickerResolution is the delta to check blockchain sync progress.
|
|
const tickerResolution = time.Second
|
|
|
|
// errors
|
|
var (
|
|
ErrNodeRunning = errors.New("node is already running")
|
|
ErrNoGethNode = errors.New("geth node is not available")
|
|
ErrNoRunningNode = errors.New("there is no running node")
|
|
ErrAccountKeyStoreMissing = errors.New("account key store is not set")
|
|
ErrServiceUnknown = errors.New("service unknown")
|
|
)
|
|
|
|
// StatusNode abstracts contained geth node and provides helper methods to
|
|
// interact with it.
|
|
type StatusNode struct {
|
|
mu sync.RWMutex
|
|
|
|
config *params.NodeConfig // Status node configuration
|
|
gethNode *node.Node // reference to Geth P2P stack/node
|
|
rpcClient *rpc.Client // reference to public RPC client
|
|
rpcPrivateClient *rpc.Client // reference to private RPC client (can call private APIs)
|
|
|
|
register *peers.Register
|
|
peerPool *peers.PeerPool
|
|
db *leveldb.DB // used as a cache for PeerPool
|
|
|
|
log log.Logger
|
|
}
|
|
|
|
// New makes new instance of StatusNode.
|
|
func New() *StatusNode {
|
|
return &StatusNode{
|
|
log: log.New("package", "status-go/geth/node.StatusNode"),
|
|
}
|
|
}
|
|
|
|
// Config exposes reference to running node's configuration
|
|
func (n *StatusNode) Config() *params.NodeConfig {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
return n.config
|
|
}
|
|
|
|
// GethNode returns underlying geth node.
|
|
func (n *StatusNode) GethNode() *node.Node {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
return n.gethNode
|
|
}
|
|
|
|
func (n *StatusNode) startWithDB(config *params.NodeConfig, db *leveldb.DB, services []node.ServiceConstructor) error {
|
|
if err := n.createNode(config, db); err != nil {
|
|
return err
|
|
}
|
|
n.config = config
|
|
|
|
if err := n.start(services); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := n.setupRPCClient(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if n.config.NoDiscovery {
|
|
return nil
|
|
}
|
|
return n.startPeerPool()
|
|
}
|
|
|
|
// Start starts current StatusNode, will fail if it's already started.
|
|
func (n *StatusNode) Start(config *params.NodeConfig, services ...node.ServiceConstructor) error {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
if n.isRunning() {
|
|
return ErrNodeRunning
|
|
}
|
|
|
|
db, err := db.Create(config.DataDir, params.StatusDatabase)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n.db = db
|
|
|
|
err = n.startWithDB(config, db, services)
|
|
|
|
if err != nil {
|
|
if dberr := db.Close(); dberr != nil {
|
|
n.log.Error("error while closing leveldb after node crash", "error", dberr)
|
|
}
|
|
n.db = nil
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *StatusNode) createNode(config *params.NodeConfig, db *leveldb.DB) (err error) {
|
|
n.gethNode, err = MakeNode(config, db)
|
|
return err
|
|
}
|
|
|
|
// start starts current StatusNode, will fail if it's already started.
|
|
func (n *StatusNode) start(services []node.ServiceConstructor) error {
|
|
for _, service := range services {
|
|
if err := n.gethNode.Register(service); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return n.gethNode.Start()
|
|
}
|
|
|
|
func (n *StatusNode) setupRPCClient() (err error) {
|
|
// setup public RPC client
|
|
gethNodeClient, err := n.gethNode.AttachPublic()
|
|
if err != nil {
|
|
return
|
|
}
|
|
n.rpcClient, err = rpc.NewClient(gethNodeClient, n.config.UpstreamConfig)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// setup private RPC client
|
|
gethNodePrivateClient, err := n.gethNode.Attach()
|
|
if err != nil {
|
|
return
|
|
}
|
|
n.rpcPrivateClient, err = rpc.NewClient(gethNodePrivateClient, n.config.UpstreamConfig)
|
|
|
|
return
|
|
}
|
|
|
|
func (n *StatusNode) startPeerPool() error {
|
|
n.register = peers.NewRegister(n.config.RegisterTopics...)
|
|
options := peers.NewDefaultOptions()
|
|
// TODO(dshulyak) consider adding a flag to define this behaviour
|
|
options.AllowStop = len(n.config.RegisterTopics) == 0
|
|
n.peerPool = peers.NewPeerPool(
|
|
n.config.RequireTopics,
|
|
peers.NewCache(n.db),
|
|
options,
|
|
)
|
|
if err := n.register.Start(n.gethNode.Server()); err != nil {
|
|
return err
|
|
}
|
|
return n.peerPool.Start(n.gethNode.Server())
|
|
}
|
|
|
|
// Stop will stop current StatusNode. A stopped node cannot be resumed.
|
|
func (n *StatusNode) Stop() error {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
if !n.isRunning() {
|
|
return ErrNoRunningNode
|
|
}
|
|
|
|
return n.stop()
|
|
}
|
|
|
|
// stop will stop current StatusNode. A stopped node cannot be resumed.
|
|
func (n *StatusNode) stop() error {
|
|
if err := n.stopPeerPool(); err != nil {
|
|
n.log.Error("Error stopping the PeerPool", "error", err)
|
|
}
|
|
n.register = nil
|
|
n.peerPool = nil
|
|
|
|
if err := n.gethNode.Stop(); err != nil {
|
|
return err
|
|
}
|
|
|
|
n.rpcClient = nil
|
|
n.rpcPrivateClient = nil
|
|
// We need to clear `gethNode` because config is passed to `Start()`
|
|
// and may be completely different. Similarly with `config`.
|
|
n.gethNode = nil
|
|
n.config = nil
|
|
|
|
if n.db != nil {
|
|
err := n.db.Close()
|
|
|
|
n.db = nil
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *StatusNode) stopPeerPool() error {
|
|
if n.config == nil || n.config.NoDiscovery {
|
|
return nil
|
|
}
|
|
|
|
n.register.Stop()
|
|
n.peerPool.Stop()
|
|
return nil
|
|
}
|
|
|
|
// ResetChainData removes chain data if node is not running.
|
|
func (n *StatusNode) ResetChainData(config *params.NodeConfig) error {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
if n.isRunning() {
|
|
return ErrNodeRunning
|
|
}
|
|
|
|
chainDataDir := filepath.Join(config.DataDir, config.Name, "lightchaindata")
|
|
if _, err := os.Stat(chainDataDir); os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
err := os.RemoveAll(chainDataDir)
|
|
if err == nil {
|
|
n.log.Info("Chain data has been removed", "dir", chainDataDir)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// IsRunning confirm that node is running.
|
|
func (n *StatusNode) IsRunning() bool {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
return n.isRunning()
|
|
}
|
|
|
|
func (n *StatusNode) isRunning() bool {
|
|
return n.gethNode != nil && n.gethNode.Server() != nil
|
|
}
|
|
|
|
// populateStaticPeers connects current node with our publicly available LES/SHH/Swarm cluster
|
|
func (n *StatusNode) populateStaticPeers() error {
|
|
if n.config.ClusterConfig == nil || !n.config.ClusterConfig.Enabled {
|
|
n.log.Info("Static peers are disabled")
|
|
return nil
|
|
}
|
|
|
|
for _, enode := range n.config.ClusterConfig.StaticNodes {
|
|
if err := n.addPeer(enode); err != nil {
|
|
n.log.Error("Static peer addition failed", "error", err)
|
|
return err
|
|
}
|
|
n.log.Info("Static peer added", "enode", enode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *StatusNode) removeStaticPeers() error {
|
|
if n.config.ClusterConfig == nil || !n.config.ClusterConfig.Enabled {
|
|
n.log.Info("Static peers are disabled")
|
|
return nil
|
|
}
|
|
|
|
for _, enode := range n.config.ClusterConfig.StaticNodes {
|
|
if err := n.removePeer(enode); err != nil {
|
|
n.log.Error("Static peer deletion failed", "error", err)
|
|
return err
|
|
}
|
|
n.log.Info("Static peer deleted", "enode", enode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReconnectStaticPeers removes and adds static peers to a server.
|
|
func (n *StatusNode) ReconnectStaticPeers() error {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
if !n.isRunning() {
|
|
return ErrNoRunningNode
|
|
}
|
|
|
|
if err := n.removeStaticPeers(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return n.populateStaticPeers()
|
|
}
|
|
|
|
// AddPeer adds new static peer node
|
|
func (n *StatusNode) AddPeer(url string) error {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
return n.addPeer(url)
|
|
}
|
|
|
|
// addPeer adds new static peer node
|
|
func (n *StatusNode) addPeer(url string) error {
|
|
parsedNode, err := discover.ParseNode(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !n.isRunning() {
|
|
return ErrNoRunningNode
|
|
}
|
|
|
|
n.gethNode.Server().AddPeer(parsedNode)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *StatusNode) removePeer(url string) error {
|
|
parsedNode, err := discover.ParseNode(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !n.isRunning() {
|
|
return ErrNoRunningNode
|
|
}
|
|
|
|
n.gethNode.Server().RemovePeer(parsedNode)
|
|
|
|
return nil
|
|
}
|
|
|
|
// PeerCount returns the number of connected peers.
|
|
func (n *StatusNode) PeerCount() int {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
if !n.isRunning() {
|
|
return 0
|
|
}
|
|
|
|
return n.gethNode.Server().PeerCount()
|
|
}
|
|
|
|
// gethService is a wrapper for gethNode.Service which retrieves a currently
|
|
// running service registered of a specific type.
|
|
func (n *StatusNode) gethService(serviceInstance interface{}) error {
|
|
if !n.isRunning() {
|
|
return ErrNoRunningNode
|
|
}
|
|
|
|
if err := n.gethNode.Service(serviceInstance); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LightEthereumService exposes reference to LES service running on top of the node
|
|
func (n *StatusNode) LightEthereumService() (l *les.LightEthereum, err error) {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
err = n.gethService(&l)
|
|
if err == node.ErrServiceUnknown {
|
|
err = ErrServiceUnknown
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// StatusService exposes reference to status service running on top of the node
|
|
func (n *StatusNode) StatusService() (st *status.Service, err error) {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
err = n.gethService(&st)
|
|
if err == node.ErrServiceUnknown {
|
|
err = ErrServiceUnknown
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// WhisperService exposes reference to Whisper service running on top of the node
|
|
func (n *StatusNode) WhisperService() (w *whisper.Whisper, err error) {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
err = n.gethService(&w)
|
|
if err == node.ErrServiceUnknown {
|
|
err = ErrServiceUnknown
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// AccountManager exposes reference to node's accounts manager
|
|
func (n *StatusNode) AccountManager() (*accounts.Manager, error) {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
if n.gethNode == nil {
|
|
return nil, ErrNoGethNode
|
|
}
|
|
|
|
return n.gethNode.AccountManager(), nil
|
|
}
|
|
|
|
// AccountKeyStore exposes reference to accounts key store
|
|
func (n *StatusNode) AccountKeyStore() (*keystore.KeyStore, error) {
|
|
n.mu.RLock()
|
|
defer n.mu.RUnlock()
|
|
|
|
if n.gethNode == nil {
|
|
return nil, ErrNoGethNode
|
|
}
|
|
|
|
accountManager := n.gethNode.AccountManager()
|
|
backends := accountManager.Backends(keystore.KeyStoreType)
|
|
if len(backends) == 0 {
|
|
return nil, ErrAccountKeyStoreMissing
|
|
}
|
|
|
|
keyStore, ok := backends[0].(*keystore.KeyStore)
|
|
if !ok {
|
|
return nil, ErrAccountKeyStoreMissing
|
|
}
|
|
|
|
return keyStore, nil
|
|
}
|
|
|
|
// RPCClient exposes reference to RPC client connected to the running node.
|
|
func (n *StatusNode) RPCClient() *rpc.Client {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
return n.rpcClient
|
|
}
|
|
|
|
// RPCPrivateClient exposes reference to RPC client connected to the running node
|
|
// that can call both public and private APIs.
|
|
func (n *StatusNode) RPCPrivateClient() *rpc.Client {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
return n.rpcPrivateClient
|
|
}
|
|
|
|
// EnsureSync waits until blockchain synchronization
|
|
// is complete and returns.
|
|
func (n *StatusNode) EnsureSync(ctx context.Context) error {
|
|
// Don't wait for any blockchain sync for the
|
|
// local private chain as blocks are never mined.
|
|
if n.config.NetworkID == 0 || n.config.NetworkID == params.StatusChainNetworkID {
|
|
return nil
|
|
}
|
|
|
|
return n.ensureSync(ctx)
|
|
}
|
|
|
|
func (n *StatusNode) ensureSync(ctx context.Context) error {
|
|
les, err := n.LightEthereumService()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get LES service: %v", err)
|
|
}
|
|
|
|
downloader := les.Downloader()
|
|
if downloader == nil {
|
|
return errors.New("LightEthereumService downloader is nil")
|
|
}
|
|
|
|
progress := downloader.Progress()
|
|
if n.PeerCount() > 0 && progress.CurrentBlock >= progress.HighestBlock {
|
|
n.log.Debug("Synchronization completed", "current block", progress.CurrentBlock, "highest block", progress.HighestBlock)
|
|
return nil
|
|
}
|
|
|
|
ticker := time.NewTicker(tickerResolution)
|
|
defer ticker.Stop()
|
|
|
|
progressTicker := time.NewTicker(time.Minute)
|
|
defer progressTicker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return errors.New("timeout during node synchronization")
|
|
case <-ticker.C:
|
|
if n.PeerCount() == 0 {
|
|
n.log.Debug("No established connections with any peers, continue waiting for a sync")
|
|
continue
|
|
}
|
|
if downloader.Synchronising() {
|
|
n.log.Debug("Synchronization is in progress")
|
|
continue
|
|
}
|
|
progress = downloader.Progress()
|
|
if progress.CurrentBlock >= progress.HighestBlock {
|
|
n.log.Info("Synchronization completed", "current block", progress.CurrentBlock, "highest block", progress.HighestBlock)
|
|
return nil
|
|
}
|
|
n.log.Debug("Synchronization is not finished", "current", progress.CurrentBlock, "highest", progress.HighestBlock)
|
|
case <-progressTicker.C:
|
|
progress = downloader.Progress()
|
|
n.log.Warn("Synchronization is not finished", "current", progress.CurrentBlock, "highest", progress.HighestBlock)
|
|
}
|
|
}
|
|
}
|