// +build nimbus package node import ( "context" "crypto/ecdsa" "errors" "fmt" "os" "path/filepath" "reflect" "strings" "sync" "github.com/syndtr/goleveldb/leveldb" "github.com/ethereum/go-ethereum/log" gethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/status-im/status-go/db" nimbusbridge "github.com/status-im/status-go/eth-node/bridge/nimbus" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/params" "github.com/status-im/status-go/rpc" nimbussvc "github.com/status-im/status-go/services/nimbus" "github.com/status-im/status-go/services/nodebridge" "github.com/status-im/status-go/services/shhext" "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") ErrNodeStopped = errors.New("node not started") ErrNoRunningNode = errors.New("there is no running node") ErrServiceUnknown = errors.New("service unknown") ) // NimbusStatusNode abstracts contained geth node and provides helper methods to // interact with it. type NimbusStatusNode struct { mu sync.RWMutex //eventmux *event.TypeMux // Event multiplexer used between the services of a stack config *params.NodeConfig // Status node configuration privateKey *ecdsa.PrivateKey node nimbusbridge.Node nodeRunning bool rpcClient *rpc.Client // reference to public RPC client rpcPrivateClient *rpc.Client // reference to private RPC client (can call private APIs) rpcAPIs []gethrpc.API // List of APIs currently provided by the node inprocHandler *gethrpc.Server // In-process RPC request handler to process the API requests inprocPublicHandler *gethrpc.Server // In-process RPC request handler to process the public API requests serviceFuncs []nimbussvc.ServiceConstructor // Service constructors (in dependency order) services map[reflect.Type]nimbussvc.Service // Currently running services // discovery discovery.Discovery // register *peers.Register // peerPool *peers.PeerPool db *leveldb.DB // used as a cache for PeerPool //stop chan struct{} // Channel to wait for termination notifications lock sync.RWMutex log log.Logger } // NewNimbus makes new instance of NimbusStatusNode. func NewNimbus() *NimbusStatusNode { return &NimbusStatusNode{ //eventmux: new(event.TypeMux), log: log.New("package", "status-go/node.NimbusStatusNode"), } } // Config exposes reference to running node's configuration func (n *NimbusStatusNode) Config() *params.NodeConfig { n.mu.RLock() defer n.mu.RUnlock() return n.config } // GethNode returns underlying geth node. // func (n *NimbusStatusNode) GethNode() *node.Node { // n.mu.RLock() // defer n.mu.RUnlock() // return n.gethNode // } // Server retrieves the currently running P2P network layer. // func (n *NimbusStatusNode) Server() *p2p.Server { // n.mu.RLock() // defer n.mu.RUnlock() // if n.gethNode == nil { // return nil // } // return n.gethNode.Server() // } // Start starts current NimbusStatusNode, failing if it's already started. // It accepts a list of services that should be added to the node. func (n *NimbusStatusNode) Start(config *params.NodeConfig, services ...nimbussvc.ServiceConstructor) error { panic("Start") return n.StartWithOptions(config, NimbusStartOptions{ Services: services, StartDiscovery: true, // AccountsManager: accs, }) } // NimbusStartOptions allows to control some parameters of Start() method. type NimbusStartOptions struct { Node types.Node Services []nimbussvc.ServiceConstructor StartDiscovery bool // AccountsManager *accounts.Manager } // StartWithOptions starts current NimbusStatusNode, failing if it's already started. // It takes some options that allows to further configure starting process. func (n *NimbusStatusNode) StartWithOptions(config *params.NodeConfig, options NimbusStartOptions) error { n.mu.Lock() defer n.mu.Unlock() if n.isRunning() { n.log.Debug("cannot start, node already running") return ErrNodeRunning } n.log.Debug("starting with NodeConfig", "ClusterConfig", config.ClusterConfig) db, err := db.Create(config.DataDir, params.StatusDatabase) if err != nil { return fmt.Errorf("failed to create database at %s: %v", config.DataDir, err) } n.db = db err = n.startWithDB(config, db, options.Services) // continue only if there was no error when starting node with a db if err == nil && options.StartDiscovery && n.discoveryEnabled() { // err = n.startDiscovery() } 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 *NimbusStatusNode) startWithDB(config *params.NodeConfig, db *leveldb.DB, services []nimbussvc.ServiceConstructor) error { if err := n.createNode(config, services, db); err != nil { return err } if err := n.setupRPCClient(); err != nil { return err } return nil } func (n *NimbusStatusNode) createNode(config *params.NodeConfig, services []nimbussvc.ServiceConstructor, db *leveldb.DB) error { var privateKey *ecdsa.PrivateKey if config.NodeKey != "" { var err error privateKey, err = crypto.HexToECDSA(config.NodeKey) if err != nil { return err } } n.privateKey = privateKey n.node = nimbusbridge.NewNodeBridge() err := n.activateServices(config, db) if err != nil { return err } if err = n.start(config, services); err != nil { return err } return nil } // start starts current NimbusStatusNode, will fail if it's already started. func (n *NimbusStatusNode) start(config *params.NodeConfig, services []nimbussvc.ServiceConstructor) error { for _, service := range services { if err := n.Register(service); err != nil { return err } } n.config = config n.startServices() err := n.node.StartNimbus(n.privateKey, config.ListenAddr, true) n.nodeRunning = err == nil return err } func (n *NimbusStatusNode) setupRPCClient() (err error) { // setup public RPC client gethNodeClient := gethrpc.DialInProc(n.inprocPublicHandler) n.rpcClient, err = rpc.NewClient(gethNodeClient, n.config.UpstreamConfig) if err != nil { return } // setup private RPC client gethNodePrivateClient := gethrpc.DialInProc(n.inprocHandler) n.rpcPrivateClient, err = rpc.NewClient(gethNodePrivateClient, n.config.UpstreamConfig) return } func (n *NimbusStatusNode) discoveryEnabled() bool { return n.config != nil && (!n.config.NoDiscovery || n.config.Rendezvous) && n.config.ClusterConfig.Enabled } // func (n *NimbusStatusNode) discoverNode() (*enode.Node, error) { // if !n.isRunning() { // return nil, nil // } // server := n.gethNode.Server() // discNode := server.Self() // if n.config.AdvertiseAddr == "" { // return discNode, nil // } // n.log.Info("Using AdvertiseAddr for rendezvous", "addr", n.config.AdvertiseAddr) // r := discNode.Record() // r.Set(enr.IP(net.ParseIP(n.config.AdvertiseAddr))) // if err := enode.SignV4(r, server.PrivateKey); err != nil { // return nil, err // } // return enode.New(enode.ValidSchemes[r.IdentityScheme()], r) // } // func (n *NimbusStatusNode) startRendezvous() (discovery.Discovery, error) { // if !n.config.Rendezvous { // return nil, errors.New("rendezvous is not enabled") // } // if len(n.config.ClusterConfig.RendezvousNodes) == 0 { // return nil, errors.New("rendezvous node must be provided if rendezvous discovery is enabled") // } // maddrs := make([]ma.Multiaddr, len(n.config.ClusterConfig.RendezvousNodes)) // for i, addr := range n.config.ClusterConfig.RendezvousNodes { // var err error // maddrs[i], err = ma.NewMultiaddr(addr) // if err != nil { // return nil, fmt.Errorf("failed to parse rendezvous node %s: %v", n.config.ClusterConfig.RendezvousNodes[0], err) // } // } // node, err := n.discoverNode() // if err != nil { // return nil, fmt.Errorf("failed to get a discover node: %v", err) // } // return discovery.NewRendezvous(maddrs, n.gethNode.Server().PrivateKey, node) // } // StartDiscovery starts the peers discovery protocols depending on the node config. func (n *NimbusStatusNode) StartDiscovery() error { n.mu.Lock() defer n.mu.Unlock() if n.discoveryEnabled() { // return n.startDiscovery() } return nil } // func (n *NimbusStatusNode) startDiscovery() error { // if n.isDiscoveryRunning() { // return ErrDiscoveryRunning // } // discoveries := []discovery.Discovery{} // if !n.config.NoDiscovery { // discoveries = append(discoveries, discovery.NewDiscV5( // n.gethNode.Server().PrivateKey, // n.config.ListenAddr, // parseNodesV5(n.config.ClusterConfig.BootNodes))) // } // if n.config.Rendezvous { // d, err := n.startRendezvous() // if err != nil { // return err // } // discoveries = append(discoveries, d) // } // if len(discoveries) == 0 { // return errors.New("wasn't able to register any discovery") // } else if len(discoveries) > 1 { // n.discovery = discovery.NewMultiplexer(discoveries) // } else { // n.discovery = discoveries[0] // } // log.Debug( // "using discovery", // "instance", reflect.TypeOf(n.discovery), // "registerTopics", n.config.RegisterTopics, // "requireTopics", n.config.RequireTopics, // ) // n.register = peers.NewRegister(n.discovery, n.config.RegisterTopics...) // options := peers.NewDefaultOptions() // // TODO(dshulyak) consider adding a flag to define this behaviour // options.AllowStop = len(n.config.RegisterTopics) == 0 // options.TrustedMailServers = parseNodesToNodeID(n.config.ClusterConfig.TrustedMailServers) // options.MailServerRegistryAddress = n.config.MailServerRegistryAddress // n.peerPool = peers.NewPeerPool( // n.discovery, // n.config.RequireTopics, // peers.NewCache(n.db), // options, // ) // if err := n.discovery.Start(); err != nil { // return err // } // if err := n.register.Start(); err != nil { // return err // } // return n.peerPool.Start(n.gethNode.Server(), n.rpcClient) // } // Stop will stop current NimbusStatusNode. A stopped node cannot be resumed. func (n *NimbusStatusNode) Stop() error { n.mu.Lock() defer n.mu.Unlock() if !n.isRunning() { return ErrNoRunningNode } var errs []error // Terminate all subsystems and collect any errors if err := n.stop(); err != nil && err != ErrNodeStopped { errs = append(errs, err) } // Report any errors that might have occurred switch len(errs) { case 0: return nil case 1: return errs[0] default: return fmt.Errorf("%v", errs) } } // StopError is returned if a Node fails to stop either any of its registered // services or itself. type StopError struct { Server error Services map[reflect.Type]error } // Error generates a textual representation of the stop error. func (e *StopError) Error() string { return fmt.Sprintf("server: %v, services: %v", e.Server, e.Services) } // stop will stop current NimbusStatusNode. A stopped node cannot be resumed. func (n *NimbusStatusNode) stop() error { // if n.isDiscoveryRunning() { // if err := n.stopDiscovery(); err != nil { // n.log.Error("Error stopping the discovery components", "error", err) // } // n.register = nil // n.peerPool = nil // n.discovery = nil // } // if err := n.gethNode.Stop(); err != nil { // return err // } // Terminate the API, services and the p2p server. n.stopPublicInProc() n.stopInProc() n.rpcClient = nil n.rpcPrivateClient = nil n.rpcAPIs = nil failure := &StopError{ Services: make(map[reflect.Type]error), } for kind, service := range n.services { if err := service.Stop(); err != nil { failure.Services[kind] = err } } n.services = nil // We need to clear `node` because config is passed to `Start()` // and may be completely different. Similarly with `config`. if n.node != nil { n.node.Stop() n.node = nil } n.nodeRunning = false n.config = nil if n.db != nil { err := n.db.Close() n.db = nil return err } if len(failure.Services) > 0 { return failure } return nil } func (n *NimbusStatusNode) isDiscoveryRunning() bool { return false //n.register != nil || n.peerPool != nil || n.discovery != nil } // func (n *NimbusStatusNode) stopDiscovery() error { // n.register.Stop() // n.peerPool.Stop() // return n.discovery.Stop() // } // ResetChainData removes chain data if node is not running. func (n *NimbusStatusNode) 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 *NimbusStatusNode) IsRunning() bool { n.mu.RLock() defer n.mu.RUnlock() return n.isRunning() } func (n *NimbusStatusNode) isRunning() bool { return n.node != nil && n.nodeRunning // && n.gethNode.Server() != nil } // populateStaticPeers connects current node with our publicly available LES/SHH/Swarm cluster func (n *NimbusStatusNode) populateStaticPeers() error { if !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 *NimbusStatusNode) removeStaticPeers() error { if !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 *NimbusStatusNode) 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 *NimbusStatusNode) AddPeer(url string) error { n.mu.RLock() defer n.mu.RUnlock() return n.addPeer(url) } // addPeer adds new static peer node func (n *NimbusStatusNode) addPeer(url string) error { if !n.isRunning() { return ErrNoRunningNode } n.node.AddPeer(url) return nil } func (n *NimbusStatusNode) removePeer(url string) error { if !n.isRunning() { return ErrNoRunningNode } n.node.RemovePeer(url) return nil } // PeerCount returns the number of connected peers. func (n *NimbusStatusNode) PeerCount() int { n.mu.RLock() defer n.mu.RUnlock() if !n.isRunning() { return 0 } return 1 //return n.gethNode.Server().PeerCount() } // Service retrieves a currently running service registered of a specific type. func (n *NimbusStatusNode) Service(service interface{}) error { n.lock.RLock() defer n.lock.RUnlock() // Short circuit if the node's not running if !n.isRunning() { return ErrNodeStopped } // Otherwise try to find the service to return element := reflect.ValueOf(service).Elem() if running, ok := n.services[element.Type()]; ok { element.Set(reflect.ValueOf(running)) return nil } return ErrServiceUnknown } // // LightEthereumService exposes reference to LES service running on top of the node // func (n *NimbusStatusNode) LightEthereumService() (l *les.LightEthereum, err error) { // n.mu.RLock() // defer n.mu.RUnlock() // err = n.Service(&l) // return // } // StatusService exposes reference to status service running on top of the node func (n *NimbusStatusNode) StatusService() (st *status.Service, err error) { n.mu.RLock() defer n.mu.RUnlock() err = n.Service(&st) return } // // PeerService exposes reference to peer service running on top of the node. // func (n *NimbusStatusNode) PeerService() (st *peer.Service, err error) { // n.mu.RLock() // defer n.mu.RUnlock() // err = n.Service(&st) // return // } // WhisperService exposes reference to Whisper service running on top of the node func (n *NimbusStatusNode) WhisperService() (w *nodebridge.WhisperService, err error) { n.mu.RLock() defer n.mu.RUnlock() err = n.Service(&w) return } // ShhExtService exposes reference to shh extension service running on top of the node func (n *NimbusStatusNode) ShhExtService() (s *shhext.NimbusService, err error) { n.mu.RLock() defer n.mu.RUnlock() err = n.Service(&s) return } // // WalletService returns wallet.Service instance if it was started. // func (n *NimbusStatusNode) WalletService() (s *wallet.Service, err error) { // n.mu.RLock() // defer n.mu.RUnlock() // err = n.Service(&s) // return // } // // BrowsersService returns browsers.Service instance if it was started. // func (n *NimbusStatusNode) BrowsersService() (s *browsers.Service, err error) { // n.mu.RLock() // defer n.mu.RUnlock() // err = n.Service(&s) // return // } // // PermissionsService returns browsers.Service instance if it was started. // func (n *NimbusStatusNode) PermissionsService() (s *permissions.Service, err error) { // n.mu.RLock() // defer n.mu.RUnlock() // err = n.Service(&s) // return // } // // AccountManager exposes reference to node's accounts manager // func (n *NimbusStatusNode) AccountManager() (*accounts.Manager, error) { // n.mu.RLock() // defer n.mu.RUnlock() // if n.gethNode == nil { // return nil, ErrNoGethNode // } // return n.gethNode.AccountManager(), nil // } // RPCClient exposes reference to RPC client connected to the running node. func (n *NimbusStatusNode) RPCClient() *rpc.Client { n.mu.RLock() defer n.mu.RUnlock() return n.rpcClient } // RPCPrivateClient exposes reference to RPC client connected to the running node // that can call both public and private APIs. func (n *NimbusStatusNode) RPCPrivateClient() *rpc.Client { n.mu.Lock() defer n.mu.Unlock() return n.rpcPrivateClient } // ChaosModeCheckRPCClientsUpstreamURL updates RPCClient and RPCPrivateClient upstream URLs, // if defined, without restarting the node. This is required for the Chaos Unicorn Day. // Additionally, if the passed URL is Infura, it changes it to httpstat.us/500. func (n *NimbusStatusNode) ChaosModeCheckRPCClientsUpstreamURL(on bool) error { url := n.config.UpstreamConfig.URL if on { if strings.Contains(url, "infura.io") { url = "https://httpbin.org/status/500" } } publicClient := n.RPCClient() if publicClient != nil { if err := publicClient.UpdateUpstreamURL(url); err != nil { return err } } privateClient := n.RPCPrivateClient() if privateClient != nil { if err := privateClient.UpdateUpstreamURL(url); err != nil { return err } } return nil } // EnsureSync waits until blockchain synchronization // is complete and returns. func (n *NimbusStatusNode) 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 *NimbusStatusNode) ensureSync(ctx context.Context) error { return errors.New("Sync not implemented") // 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) // } // } } // // Discover sets up the discovery for a specific topic. // func (n *NimbusStatusNode) Discover(topic string, max, min int) (err error) { // if n.peerPool == nil { // return errors.New("peerPool not running") // } // return n.peerPool.UpdateTopic(topic, params.Limits{ // Max: max, // Min: min, // }) // }