diff --git a/cmd/statusd/main.go b/cmd/statusd/main.go index 2373f5d7d..00ddbe89a 100644 --- a/cmd/statusd/main.go +++ b/cmd/statusd/main.go @@ -164,7 +164,7 @@ func main() { exitCode := syncAndStopNode(interruptCh, backend.StatusNode(), *syncAndExit) // Call was interrupted. Wait for graceful shutdown. if exitCode == -1 { - if node, err := backend.StatusNode().GethNode(); err == nil && node != nil { + if node := backend.StatusNode().GethNode(); node != nil { node.Wait() } return @@ -173,14 +173,11 @@ func main() { os.Exit(exitCode) } - node, err := backend.StatusNode().GethNode() - if err != nil { - logger.Error("Getting node failed", "error", err) - return + node := backend.StatusNode().GethNode() + if node != nil { + // wait till node has been stopped + node.Wait() } - - // wait till node has been stopped - node.Wait() } // startDebug starts the debugging API server. @@ -195,9 +192,9 @@ func startCollectingStats(interruptCh <-chan struct{}, statusNode *node.StatusNo logger.Info("Starting stats", "stats", *statsAddr) - node, err := statusNode.GethNode() - if err != nil { - logger.Error("Failed to run metrics because could not get node", "error", err) + node := statusNode.GethNode() + if node == nil { + logger.Error("Failed to run metrics because it could not get the node") return } diff --git a/geth/api/backend.go b/geth/api/backend.go index cf7434395..48a0880f4 100644 --- a/geth/api/backend.go +++ b/geth/api/backend.go @@ -112,12 +112,6 @@ func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) { err = b.statusNode.Start(config) if err != nil { - switch err.(type) { - case node.RPCClientError: - err = fmt.Errorf("%v: %v", node.ErrRPCClient, err) - case node.EthNodeError: - err = fmt.Errorf("%v: %v", node.ErrNodeStartFailure, err) - } signal.Send(signal.Envelope{ Type: signal.EventNodeCrashed, Event: signal.NodeCrashEvent{ @@ -162,11 +156,7 @@ func (b *StatusBackend) RestartNode() error { if !b.IsNodeRunning() { return node.ErrNoRunningNode } - config, err := b.statusNode.Config() - if err != nil { - return err - } - newcfg := *config + newcfg := *(b.statusNode.Config()) if err := b.stopNode(); err != nil { return err } @@ -178,11 +168,7 @@ func (b *StatusBackend) RestartNode() error { func (b *StatusBackend) ResetChainData() error { b.mu.Lock() defer b.mu.Unlock() - config, err := b.statusNode.Config() - if err != nil { - return err - } - newcfg := *config + newcfg := *(b.statusNode.Config()) if err := b.stopNode(); err != nil { return err } @@ -217,10 +203,7 @@ func (b *StatusBackend) getVerifiedAccount(password string) (*account.SelectedEx b.log.Error("failed to get a selected account", "err", err) return nil, err } - config, err := b.StatusNode().Config() - if err != nil { - return nil, err - } + config := b.StatusNode().Config() _, err = b.accountManager.VerifyAccountPassword(config.KeyStoreDir, selectedAccount.Address.String(), password) if err != nil { b.log.Error("failed to verify account", "account", selectedAccount.Address.String(), "error", err) @@ -269,7 +252,7 @@ func (b *StatusBackend) DiscardTransactions(ids []string) map[string]error { func (b *StatusBackend) registerHandlers() error { rpcClient := b.StatusNode().RPCClient() if rpcClient == nil { - return node.ErrRPCClient + return errors.New("RPC client unavailable") } rpcClient.RegisterHandler(params.AccountsMethodName, func(context.Context, ...interface{}) (interface{}, error) { diff --git a/geth/node/node.go b/geth/node/node.go index a043764d9..d1f6cd890 100644 --- a/geth/node/node.go +++ b/geth/node/node.go @@ -27,14 +27,11 @@ import ( "github.com/status-im/status-go/shhext" ) -// node-related errors +// Errors related to node and services creation. var ( - ErrEthServiceRegistrationFailure = errors.New("failed to register the Ethereum service") + ErrNodeMakeFailure = errors.New("error creating p2p node") ErrWhisperServiceRegistrationFailure = errors.New("failed to register the Whisper service") ErrLightEthRegistrationFailure = errors.New("failed to register the LES service") - ErrNodeMakeFailure = errors.New("error creating p2p node") - ErrNodeRunFailure = errors.New("error running p2p node") - ErrNodeStartFailure = errors.New("error starting p2p node") ) // All general log messages in this package should be routed through this logger. @@ -42,17 +39,20 @@ var logger = log.New("package", "status-go/geth/node") // MakeNode create a geth node entity func MakeNode(config *params.NodeConfig) (*node.Node, error) { - // make sure data directory exists - if err := os.MkdirAll(filepath.Join(config.DataDir), os.ModePerm); err != nil { - return nil, err + // If DataDir is empty, it means we want to create an ephemeral node + // keeping data only in memory. + if config.DataDir != "" { + // make sure data directory exists + if err := os.MkdirAll(filepath.Clean(config.DataDir), os.ModePerm); err != nil { + return nil, fmt.Errorf("make node: make data directory: %v", err) + } + + // make sure keys directory exists + if err := os.MkdirAll(filepath.Clean(config.KeyStoreDir), os.ModePerm); err != nil { + return nil, fmt.Errorf("make node: make keys directory: %v", err) + } } - // make sure keys directory exists - if err := os.MkdirAll(filepath.Join(config.KeyStoreDir), os.ModePerm); err != nil { - return nil, err - } - - // configure required node (should you need to update node's config, e.g. add bootstrap nodes, see node.Config) stackConfig := defaultEmbeddedNodeConfig(config) if len(config.NodeKeyFile) > 0 { @@ -71,14 +71,14 @@ func MakeNode(config *params.NodeConfig) (*node.Node, error) { return nil, ErrNodeMakeFailure } - // Start Ethereum service if we are not expected to use an upstream server. + // start Ethereum service if we are not expected to use an upstream server if !config.UpstreamConfig.Enabled { - if err := activateEthService(stack, config); err != nil { - return nil, fmt.Errorf("%v: %v", ErrEthServiceRegistrationFailure, err) + if err := activateLightEthService(stack, config); err != nil { + return nil, fmt.Errorf("%v: %v", ErrLightEthRegistrationFailure, err) } } - // start Whisper service + // start Whisper service. if err := activateShhService(stack, config); err != nil { return nil, fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err) } @@ -104,13 +104,9 @@ func defaultEmbeddedNodeConfig(config *params.NodeConfig) *node.Config { MaxPendingPeers: config.MaxPendingPeers, }, IPCPath: makeIPCPath(config), - HTTPCors: []string{"*"}, + HTTPCors: nil, HTTPModules: config.FormatAPIModules(), HTTPVirtualHosts: []string{"localhost"}, - WSHost: makeWSHost(config), - WSPort: config.WSPort, - WSOrigins: []string{"*"}, - WSModules: config.FormatAPIModules(), } if config.RPCEnabled { @@ -118,7 +114,7 @@ func defaultEmbeddedNodeConfig(config *params.NodeConfig) *node.Config { nc.HTTPPort = config.HTTPPort } - if config.ClusterConfig.Enabled { + if config.ClusterConfig != nil && config.ClusterConfig.Enabled { nc.P2P.StaticNodes = parseNodes(config.ClusterConfig.StaticNodes) nc.P2P.BootstrapNodesV5 = parseNodesV5(config.ClusterConfig.BootNodes) } @@ -126,9 +122,9 @@ func defaultEmbeddedNodeConfig(config *params.NodeConfig) *node.Config { return nc } -// activateEthService configures and registers the eth.Ethereum service with a given node. -func activateEthService(stack *node.Node, config *params.NodeConfig) error { - if !config.LightEthConfig.Enabled { +// activateLightEthService configures and registers the eth.Ethereum service with a given node. +func activateLightEthService(stack *node.Node, config *params.NodeConfig) error { + if config.LightEthConfig == nil || !config.LightEthConfig.Enabled { logger.Info("LES protocol is disabled") return nil } @@ -147,18 +143,14 @@ func activateEthService(stack *node.Node, config *params.NodeConfig) error { ethConf.NetworkId = config.NetworkID ethConf.DatabaseCache = config.LightEthConfig.DatabaseCache - if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { + return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { return les.New(ctx, ðConf) - }); err != nil { - return fmt.Errorf("%v: %v", ErrLightEthRegistrationFailure, err) - } - - return nil + }) } // activateShhService configures Whisper and adds it to the given node. func activateShhService(stack *node.Node, config *params.NodeConfig) (err error) { - if !config.WhisperConfig.Enabled { + if config.WhisperConfig == nil || !config.WhisperConfig.Enabled { logger.Info("SHH protocol is disabled") return nil } @@ -239,15 +231,6 @@ func makeIPCPath(config *params.NodeConfig) string { return path.Join(config.DataDir, config.IPCFile) } -// makeWSHost returns WS-RPC Server host, given enabled/disabled flag -func makeWSHost(config *params.NodeConfig) string { - if !config.WSEnabled { - return "" - } - - return config.WSHost -} - // parseNodes creates list of discover.Node out of enode strings. func parseNodes(enodes []string) []*discover.Node { nodes := make([]*discover.Node, len(enodes)) diff --git a/geth/node/node_api_test.go b/geth/node/node_api_test.go index 946b31b3c..51e489add 100644 --- a/geth/node/node_api_test.go +++ b/geth/node/node_api_test.go @@ -5,26 +5,25 @@ import ( whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" - . "github.com/status-im/status-go/t/utils" + "github.com/status-im/status-go/geth/params" "github.com/stretchr/testify/require" ) func TestWhisperLightModeEnabledSetsEmptyBloomFilter(t *testing.T) { - config, err := MakeTestNodeConfig(GetNetworkID()) - require.NoError(t, err) - config.WhisperConfig.LightClient = true - - node, nodeErr := MakeNode(config) - require.NoError(t, nodeErr) - require.NoError(t, node.Start()) + config := params.NodeConfig{ + WhisperConfig: ¶ms.WhisperConfig{ + Enabled: true, + LightClient: true, + }, + } + node := New() + require.NoError(t, node.Start(&config)) defer func() { - err := node.Stop() - require.NoError(t, err) + require.NoError(t, node.Stop()) }() var whisper *whisper.Whisper - err = node.Service(&whisper) - require.NoError(t, err) + require.NoError(t, node.gethService(&whisper)) bloomFilter := whisper.BloomFilter() expectedEmptyBloomFilter := make([]byte, 64) @@ -33,20 +32,19 @@ func TestWhisperLightModeEnabledSetsEmptyBloomFilter(t *testing.T) { } func TestWhisperLightModeEnabledSetsNilBloomFilter(t *testing.T) { - config, err := MakeTestNodeConfig(GetNetworkID()) - require.NoError(t, err) - config.WhisperConfig.LightClient = false - - node, nodeErr := MakeNode(config) - require.NoError(t, nodeErr) - require.NoError(t, node.Start()) + config := params.NodeConfig{ + WhisperConfig: ¶ms.WhisperConfig{ + Enabled: true, + LightClient: false, + }, + } + node := New() + require.NoError(t, node.Start(&config)) defer func() { - err := node.Stop() - require.NoError(t, err) + require.NoError(t, node.Stop()) }() var whisper *whisper.Whisper - err = node.Service(&whisper) - require.NoError(t, err) + require.NoError(t, node.gethService(&whisper)) require.Nil(t, whisper.BloomFilter()) } diff --git a/geth/node/status_node.go b/geth/node/status_node.go index 2cb9d8b5b..3720207bd 100644 --- a/geth/node/status_node.go +++ b/geth/node/status_node.go @@ -24,23 +24,17 @@ import ( "github.com/status-im/status-go/geth/rpc" ) +// tickerResolution is the delta to check blockchain sync progress. +const tickerResolution = time.Second + // errors var ( - ErrNodeExists = errors.New("node is already running") + ErrNodeRunning = errors.New("node is already running") + ErrNoGethNode = errors.New("geth node is not available") ErrNoRunningNode = errors.New("there is no running node") - ErrInvalidStatusNode = errors.New("status node is not properly initialized") - ErrInvalidService = errors.New("service is unavailable") - ErrInvalidAccountManager = errors.New("could not retrieve account manager") ErrAccountKeyStoreMissing = errors.New("account key store is not set") - ErrRPCClient = errors.New("failed to init RPC client") ) -// RPCClientError reported when rpc client is initialized. -type RPCClientError error - -// EthNodeError is reported when node crashed on start up. -type EthNodeError error - // StatusNode abstracts contained geth node and provides helper methods to // interact with it. type StatusNode struct { @@ -65,52 +59,67 @@ func New() *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 +} + // 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 err := n.isAvailable(); err == nil { - return ErrNodeExists + if n.isRunning() { + return ErrNodeRunning } - return n.start(config, services) -} - -// start starts current StatusNode, will fail if it's already started. -func (n *StatusNode) start(config *params.NodeConfig, services []node.ServiceConstructor) error { - ethNode, err := MakeNode(config) - if err != nil { + if err := n.createNode(config); err != nil { return err } - n.gethNode = ethNode n.config = config - for _, service := range services { - if err := ethNode.Register(service); err != nil { - return err - } + if err := n.start(services); err != nil { + return err } - // start underlying node - if err := ethNode.Start(); err != nil { - return EthNodeError(err) - } - - // init RPC client for this node if err := n.setupRPCClient(); err != nil { - n.log.Error("Failed to create an RPC client", "error", err) - return RPCClientError(err) + return err } - // start peer pool only if Discovery V5 is enabled - if ethNode.Server().DiscV5 != nil { + if n.gethNode.Server().DiscV5 != nil { return n.startPeerPool() } return nil } +func (n *StatusNode) createNode(config *params.NodeConfig) (err error) { + n.gethNode, err = MakeNode(config) + return +} + +// 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() @@ -157,45 +166,58 @@ func (n *StatusNode) startPeerPool() error { 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.isAvailable(); err != nil { - return err - } - if n.gethNode.Server().DiscV5 != nil { - n.stopPeerPool() + if err := n.stopPeerPool(); err != nil { + n.log.Error("Error stopping the PeerPool", "error", err) } + n.register = nil + n.peerPool = nil + n.db = nil + if err := n.gethNode.Stop(); err != nil { return err } - n.gethNode = nil - n.config = nil + 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 + return nil } -func (n *StatusNode) stopPeerPool() { +func (n *StatusNode) stopPeerPool() error { + if n.gethNode.Server().DiscV5 == nil { + return nil + } + n.register.Stop() n.peerPool.Stop() - if err := n.db.Close(); err != nil { - n.log.Error("error closing status db", "error", err) - } + return n.db.Close() } // ResetChainData removes chain data if node is not running. func (n *StatusNode) ResetChainData(config *params.NodeConfig) error { - if n.IsRunning() { - return ErrNodeExists - } 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) { - // is it really an error, if we want to remove it as next step? return err } err := os.RemoveAll(chainDataDir) @@ -210,38 +232,24 @@ func (n *StatusNode) IsRunning() bool { n.mu.RLock() defer n.mu.RUnlock() - if err := n.isAvailable(); err != nil { - return false - } - return true + return n.isRunning() } -// GethNode returns underlying geth node. -func (n *StatusNode) GethNode() (*node.Node, error) { - n.mu.RLock() - defer n.mu.RUnlock() - - if err := n.isAvailable(); err != nil { - return nil, err - } - return n.gethNode, nil +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 err := n.isAvailable(); err != nil { - return err - } - if !n.config.ClusterConfig.Enabled { + 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 { - err := n.addPeer(enode) - if err != nil { - n.log.Warn("Static peer addition failed", "error", err) - continue + 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) } @@ -250,18 +258,14 @@ func (n *StatusNode) populateStaticPeers() error { } func (n *StatusNode) removeStaticPeers() error { - if !n.config.ClusterConfig.Enabled { + if n.config.ClusterConfig == nil || !n.config.ClusterConfig.Enabled { n.log.Info("Static peers are disabled") return nil } - server := n.gethNode.Server() - if server == nil { - return ErrNoRunningNode - } + for _, enode := range n.config.ClusterConfig.StaticNodes { - err := n.removePeer(enode) - if err != nil { - n.log.Warn("Static peer deletion failed", "error", err) + 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) @@ -273,9 +277,15 @@ func (n *StatusNode) removeStaticPeers() error { 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() } @@ -283,20 +293,23 @@ func (n *StatusNode) ReconnectStaticPeers() error { func (n *StatusNode) AddPeer(url string) error { n.mu.RLock() defer n.mu.RUnlock() - if err := n.isAvailable(); err != nil { - return err - } + return n.addPeer(url) } // addPeer adds new static peer node func (n *StatusNode) addPeer(url string) error { - // Try to add the url as a static peer and return parsedNode, err := discover.ParseNode(url) if err != nil { return err } + + if !n.isRunning() { + return ErrNoRunningNode + } + n.gethNode.Server().AddPeer(parsedNode) + return nil } @@ -305,38 +318,37 @@ func (n *StatusNode) removePeer(url string) error { 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 { - if !n.IsRunning() { - return 0 - } - return n.gethNode.Server().PeerCount() -} - -// Config exposes reference to running node's configuration -func (n *StatusNode) Config() (*params.NodeConfig, error) { n.mu.RLock() defer n.mu.RUnlock() - if err := n.isAvailable(); err != nil { - return nil, err + if !n.isRunning() { + return 0 } - return n.config, nil + + 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{}, serviceName string) error { - if err := n.isAvailable(); err != nil { - return err +func (n *StatusNode) gethService(serviceInstance interface{}) error { + if !n.isRunning() { + return ErrNoRunningNode } - if err := n.gethNode.Service(serviceInstance); err != nil || serviceInstance == nil { - n.log.Warn("Cannot obtain ", serviceName, " service", "error", err) - return ErrInvalidService + + if err := n.gethNode.Service(serviceInstance); err != nil { + return fmt.Errorf("service unavailable: %v", err) } return nil @@ -344,12 +356,12 @@ func (n *StatusNode) gethService(serviceInstance interface{}, serviceName string // LightEthereumService exposes reference to LES service running on top of the node func (n *StatusNode) LightEthereumService() (l *les.LightEthereum, err error) { - return l, n.gethService(&l, "LES") + return l, n.gethService(&l) } // WhisperService exposes reference to Whisper service running on top of the node func (n *StatusNode) WhisperService() (w *whisper.Whisper, err error) { - return w, n.gethService(&w, "whisper") + return w, n.gethService(&w) } // AccountManager exposes reference to node's accounts manager @@ -357,14 +369,11 @@ func (n *StatusNode) AccountManager() (*accounts.Manager, error) { n.mu.RLock() defer n.mu.RUnlock() - if err := n.isAvailable(); err != nil { - return nil, err + if n.gethNode == nil { + return nil, ErrNoGethNode } - accountManager := n.gethNode.AccountManager() - if accountManager == nil { - return nil, ErrInvalidAccountManager - } - return accountManager, nil + + return n.gethNode.AccountManager(), nil } // AccountKeyStore exposes reference to accounts key store @@ -372,14 +381,11 @@ func (n *StatusNode) AccountKeyStore() (*keystore.KeyStore, error) { n.mu.RLock() defer n.mu.RUnlock() - if err := n.isAvailable(); err != nil { - return nil, err - } - accountManager := n.gethNode.AccountManager() - if accountManager == nil { - return nil, ErrInvalidAccountManager + if n.gethNode == nil { + return nil, ErrNoGethNode } + accountManager := n.gethNode.AccountManager() backends := accountManager.Backends(keystore.KeyStoreType) if len(backends) == 0 { return nil, ErrAccountKeyStoreMissing @@ -408,23 +414,12 @@ func (n *StatusNode) RPCPrivateClient() *rpc.Client { return n.rpcPrivateClient } -// isAvailable check if we have a node running and make sure is fully started -func (n *StatusNode) isAvailable() error { - if n.gethNode == nil || n.gethNode.Server() == nil { - return ErrNoRunningNode - } - return nil -} - -// tickerResolution is the delta to check blockchain sync progress. -const tickerResolution = time.Second - // 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 == params.StatusChainNetworkID { + if n.config.NetworkID == 0 || n.config.NetworkID == params.StatusChainNetworkID { return nil } diff --git a/geth/node/status_node_rpc_client_test.go b/geth/node/status_node_rpc_client_test.go index dfa3cd915..17d207f03 100644 --- a/geth/node/status_node_rpc_client_test.go +++ b/geth/node/status_node_rpc_client_test.go @@ -12,7 +12,6 @@ import ( "github.com/status-im/status-go/geth/node" "github.com/status-im/status-go/geth/params" - . "github.com/status-im/status-go/t/utils" ) type TestServiceAPI struct{} @@ -52,7 +51,7 @@ func (s *testService) Stop() error { return nil } -func createStatusNode(config *params.NodeConfig) (*node.StatusNode, error) { +func createAndStartStatusNode(config *params.NodeConfig) (*node.StatusNode, error) { services := []gethnode.ServiceConstructor{ func(_ *gethnode.ServiceContext) (gethnode.Service, error) { return &testService{}, nil @@ -65,11 +64,9 @@ func createStatusNode(config *params.NodeConfig) (*node.StatusNode, error) { func TestNodeRPCClientCallOnlyPublicAPIs(t *testing.T) { var err error - config, err := MakeTestNodeConfig(GetNetworkID()) - require.NoError(t, err) - config.APIModules = "" // no whitelisted API modules; use only public APIs - - statusNode, err := createStatusNode(config) + statusNode, err := createAndStartStatusNode(¶ms.NodeConfig{ + APIModules: "", // no whitelisted API modules; use only public APIs + }) require.NoError(t, err) defer func() { err := statusNode.Stop() @@ -94,11 +91,9 @@ func TestNodeRPCClientCallOnlyPublicAPIs(t *testing.T) { func TestNodeRPCClientCallWhitelistedPrivateService(t *testing.T) { var err error - config, err := MakeTestNodeConfig(GetNetworkID()) - require.NoError(t, err) - config.APIModules = "pri" - - statusNode, err := createStatusNode(config) + statusNode, err := createAndStartStatusNode(¶ms.NodeConfig{ + APIModules: "pri", + }) require.NoError(t, err) defer func() { err := statusNode.Stop() @@ -118,10 +113,7 @@ func TestNodeRPCClientCallWhitelistedPrivateService(t *testing.T) { func TestNodeRPCPrivateClientCallPrivateService(t *testing.T) { var err error - config, err := MakeTestNodeConfig(GetNetworkID()) - require.NoError(t, err) - - statusNode, err := createStatusNode(config) + statusNode, err := createAndStartStatusNode(¶ms.NodeConfig{}) require.NoError(t, err) defer func() { err := statusNode.Stop() diff --git a/geth/node/status_node_test.go b/geth/node/status_node_test.go new file mode 100644 index 000000000..fefc28d23 --- /dev/null +++ b/geth/node/status_node_test.go @@ -0,0 +1,292 @@ +package node + +import ( + "errors" + "io/ioutil" + "math" + "os" + "path" + "reflect" + "testing" + "time" + + "github.com/ethereum/go-ethereum/les" + gethnode "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/discover" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv6" + + "github.com/status-im/status-go/geth/params" + "github.com/stretchr/testify/require" +) + +func TestStatusNodeStart(t *testing.T) { + var err error + + config := params.NodeConfig{} + n := New() + + // checks before node is started + require.Nil(t, n.GethNode()) + require.Nil(t, n.Config()) + require.Nil(t, n.RPCClient()) + require.Equal(t, 0, n.PeerCount()) + _, err = n.AccountManager() + require.EqualError(t, err, ErrNoGethNode.Error()) + _, err = n.AccountKeyStore() + require.EqualError(t, err, ErrNoGethNode.Error()) + + // start node + require.NoError(t, n.Start(&config)) + + // checks after node is started + require.True(t, n.IsRunning()) + require.NotNil(t, n.GethNode()) + require.NotNil(t, n.Config()) + require.NotNil(t, n.RPCClient()) + require.Equal(t, 0, n.PeerCount()) + accountManager, err := n.AccountManager() + require.Nil(t, err) + require.NotNil(t, accountManager) + keyStore, err := n.AccountKeyStore() + require.Nil(t, err) + require.NotNil(t, keyStore) + // try to start already started node + require.EqualError(t, n.Start(&config), ErrNodeRunning.Error()) + + // stop node + require.NoError(t, n.Stop()) + // try to stop already stopped node + require.EqualError(t, n.Stop(), ErrNoRunningNode.Error()) + + // checks after node is stopped + require.Nil(t, n.GethNode()) + require.Nil(t, n.RPCClient()) + require.Equal(t, 0, n.PeerCount()) + _, err = n.AccountManager() + require.EqualError(t, err, ErrNoGethNode.Error()) + _, err = n.AccountKeyStore() + require.EqualError(t, err, ErrNoGethNode.Error()) +} + +func TestStatusNodeWithDataDir(t *testing.T) { + var err error + + dir, err := ioutil.TempDir("", "status-node-test") + require.NoError(t, err) + defer func() { + require.NoError(t, os.RemoveAll(dir)) + }() + + // keystore directory + keyStoreDir := path.Join(dir, "keystore") + err = os.MkdirAll(keyStoreDir, os.ModePerm) + require.NoError(t, err) + + config := params.NodeConfig{ + DataDir: dir, + KeyStoreDir: keyStoreDir, + } + n := New() + + require.NoError(t, n.Start(&config)) + require.NoError(t, n.Stop()) +} + +func TestStatusNodeServiceGetters(t *testing.T) { + config := params.NodeConfig{ + WhisperConfig: ¶ms.WhisperConfig{ + Enabled: true, + }, + LightEthConfig: ¶ms.LightEthConfig{ + Enabled: true, + }, + } + n := New() + + var ( + instance interface{} + err error + ) + + services := []struct { + getter func() (interface{}, error) + typ reflect.Type + }{ + { + getter: func() (interface{}, error) { + return n.WhisperService() + }, + typ: reflect.TypeOf(&whisper.Whisper{}), + }, + { + getter: func() (interface{}, error) { + return n.LightEthereumService() + }, + typ: reflect.TypeOf(&les.LightEthereum{}), + }, + } + + for _, service := range services { + t.Run(service.typ.String(), func(t *testing.T) { + // checks before node is started + instance, err = service.getter() + require.EqualError(t, err, ErrNoRunningNode.Error()) + require.Nil(t, instance) + + // start node + require.NoError(t, n.Start(&config)) + + // checks after node is started + instance, err = service.getter() + require.NoError(t, err) + require.NotNil(t, instance) + require.Equal(t, service.typ, reflect.TypeOf(instance)) + + // stop node + require.NoError(t, n.Stop()) + + // checks after node is stopped + instance, err = service.getter() + require.EqualError(t, err, ErrNoRunningNode.Error()) + require.Nil(t, instance) + }) + } +} + +func TestStatusNodeAddPeer(t *testing.T) { + var err error + + peer, err := gethnode.New(&gethnode.Config{ + P2P: p2p.Config{ + MaxPeers: math.MaxInt32, + NoDiscovery: true, + ListenAddr: ":0", + }, + }) + require.NoError(t, err) + require.NoError(t, peer.Start()) + defer func() { + require.NoError(t, peer.Stop()) + }() + peerURL := peer.Server().Self().String() + + n := New() + + // checks before node is started + require.EqualError(t, n.AddPeer(peerURL), ErrNoRunningNode.Error()) + + // start status node + config := params.NodeConfig{ + MaxPeers: math.MaxInt32, + } + require.NoError(t, n.Start(&config)) + defer func() { + require.NoError(t, n.Stop()) + }() + + errCh := waitForPeerAsync(n, peerURL, time.Second*5) + + // checks after node is started + require.NoError(t, n.AddPeer(peerURL)) + require.NoError(t, <-errCh) + require.Equal(t, 1, n.PeerCount()) +} + +func TestStatusNodeReconnectStaticPeers(t *testing.T) { + var err error + + peer, err := gethnode.New(&gethnode.Config{ + P2P: p2p.Config{ + MaxPeers: math.MaxInt32, + NoDiscovery: true, + ListenAddr: ":0", + }, + }) + require.NoError(t, err) + require.NoError(t, peer.Start()) + defer func() { + require.NoError(t, peer.Stop()) + }() + peerURL := peer.Server().Self().String() + + n := New() + + var errCh <-chan error + + // checks before node is started + require.EqualError(t, n.ReconnectStaticPeers(), ErrNoRunningNode.Error()) + + // start status node + config := params.NodeConfig{ + MaxPeers: math.MaxInt32, + ClusterConfig: ¶ms.ClusterConfig{ + Enabled: true, + StaticNodes: []string{peerURL}, + }, + } + errCh = waitForPeerAsync(n, peerURL, time.Second*30) + require.NoError(t, n.Start(&config)) + defer func() { + require.NoError(t, n.Stop()) + }() + + // checks after node is started + require.NoError(t, <-errCh) + require.Equal(t, 1, n.PeerCount()) + + // reconnect static peers + // it takes at least 30 seconds to bring back previously connected peer + errCh = waitForPeerAsync(n, peerURL, time.Second*60) + require.NoError(t, n.ReconnectStaticPeers(), ErrNoRunningNode.Error()) + require.NoError(t, <-errCh) + require.Equal(t, 1, n.PeerCount()) +} + +func waitForPeer(node *StatusNode, peerURL string, timeout time.Duration) error { + if !node.IsRunning() { + return ErrNoRunningNode + } + + parsedPeer, err := discover.ParseNode(peerURL) + if err != nil { + return err + } + + server := node.GethNode().Server() + ch := make(chan *p2p.PeerEvent) + subscription := server.SubscribeEvents(ch) + defer subscription.Unsubscribe() + + for { + select { + case ev := <-ch: + if ev.Type == p2p.PeerEventTypeAdd && ev.Peer == parsedPeer.ID { + return nil + } + case err := <-subscription.Err(): + if err != nil { + return err + } + case <-time.After(timeout): + // it may happen that the peer is already connected + // but even was not received + for _, p := range node.GethNode().Server().Peers() { + if p.ID() == parsedPeer.ID { + return nil + } + } + + return errors.New("wait for peer: timeout") + } + } +} + +func waitForPeerAsync(node *StatusNode, peerURL string, timeout time.Duration) <-chan error { + errCh := make(chan error) + go func() { + errCh <- waitForPeer(node, peerURL, timeout) + }() + + return errCh +} diff --git a/geth/params/config.go b/geth/params/config.go index 4aa8f99be..25c869ffa 100644 --- a/geth/params/config.go +++ b/geth/params/config.go @@ -248,15 +248,6 @@ type NodeConfig struct { // HTTPPort is the TCP port number on which to start the Geth's HTTP RPC server. HTTPPort int - // WSHost is a host interface for the WebSocket RPC server - WSHost string - - // WSPort is the TCP port number on which to start the Geth's WebSocket RPC server. - WSPort int - - // WSEnabled specifies whether WS-RPC Server is enabled or not - WSEnabled bool - // IPCFile is filename of exposed IPC RPC Server IPCFile string @@ -322,8 +313,6 @@ func NewNodeConfig(dataDir string, clstrCfgFile string, networkID uint64, devMod HTTPPort: HTTPPort, ListenAddr: ListenAddr, APIModules: APIModules, - WSHost: WSHost, - WSPort: WSPort, MaxPeers: MaxPeers, MaxPendingPeers: MaxPendingPeers, IPCFile: IPCFile, diff --git a/geth/params/config_test.go b/geth/params/config_test.go index b59bcaf65..7f94ae329 100644 --- a/geth/params/config_test.go +++ b/geth/params/config_test.go @@ -144,8 +144,6 @@ var loadConfigTestCases = []struct { require.Equal(t, params.HTTPPort, nodeConfig.HTTPPort) require.Equal(t, params.HTTPHost, nodeConfig.HTTPHost) require.True(t, nodeConfig.RPCEnabled) - require.False(t, nodeConfig.WSEnabled) - require.Equal(t, 4242, nodeConfig.WSPort) require.True(t, nodeConfig.IPCEnabled) require.Equal(t, 64, nodeConfig.LightEthConfig.DatabaseCache) }, diff --git a/geth/params/defaults.go b/geth/params/defaults.go index b6303d9ae..1f2125f15 100644 --- a/geth/params/defaults.go +++ b/geth/params/defaults.go @@ -32,9 +32,6 @@ const ( // APIModules is a list of modules to expose via any type of RPC (HTTP, IPC, in-proc) APIModules = "eth,net,web3,shh,shhext" - // WSHost is a host interface for the websocket RPC server - WSHost = "localhost" - // SendTransactionMethodName defines the name for a giving transaction. SendTransactionMethodName = "eth_sendTransaction" @@ -44,9 +41,6 @@ const ( // PersonalSignMethodName defines the name for `personal.sign` API. PersonalSignMethodName = "personal_sign" - // WSPort is a WS-RPC port (replaced in unit tests) - WSPort = 8546 - // MaxPeers is the maximum number of global peers MaxPeers = 25 diff --git a/t/destructive/peers_test.go b/t/destructive/peers_test.go index b3f50b60e..95d57183f 100644 --- a/t/destructive/peers_test.go +++ b/t/destructive/peers_test.go @@ -65,8 +65,8 @@ func (s *PeersTestSuite) TestStaticPeersReconnect() { // both on rinkeby and ropsten we can expect atleast 2 peers connected expectedPeersCount := 2 events := make(chan *p2p.PeerEvent, 10) - node, err := s.backend.StatusNode().GethNode() - s.Require().NoError(err) + node := s.backend.StatusNode().GethNode() + s.Require().NotNil(node) subscription := node.Server().SubscribeEvents(events) defer subscription.Unsubscribe() diff --git a/t/e2e/accounts/accounts_test.go b/t/e2e/accounts/accounts_test.go index b6cb316a7..6d7b2d9f8 100644 --- a/t/e2e/accounts/accounts_test.go +++ b/t/e2e/accounts/accounts_test.go @@ -219,8 +219,8 @@ func (s *AccountsTestSuite) TestSelectedAccountOnRestart() { s.NoError(s.Backend.SelectAccount(address2, TestConfig.Account1.Password)) // stop node (and all of its sub-protocols) - nodeConfig, err := s.Backend.StatusNode().Config() - s.NoError(err) + nodeConfig := s.Backend.StatusNode().Config() + s.NotNil(nodeConfig) preservedNodeConfig := *nodeConfig s.NoError(s.Backend.StopNode()) diff --git a/t/e2e/node/manager_test.go b/t/e2e/node/manager_test.go index 81a049c87..2270d714b 100644 --- a/t/e2e/node/manager_test.go +++ b/t/e2e/node/manager_test.go @@ -46,16 +46,16 @@ func (s *ManagerTestSuite) TestReferencesWithoutStartedNode() { { "non-null manager, no running node, get NodeConfig", func() (interface{}, error) { - return s.StatusNode.Config() + return s.StatusNode.Config(), nil }, - node.ErrNoRunningNode, + nil, }, { "non-null manager, no running node, get Node", func() (interface{}, error) { - return s.StatusNode.GethNode() + return s.StatusNode.GethNode(), nil }, - node.ErrNoRunningNode, + nil, }, { "non-null manager, no running node, get LES", @@ -76,14 +76,14 @@ func (s *ManagerTestSuite) TestReferencesWithoutStartedNode() { func() (interface{}, error) { return s.StatusNode.AccountManager() }, - node.ErrNoRunningNode, + node.ErrNoGethNode, }, { "non-null manager, no running node, get AccountKeyStore", func() (interface{}, error) { return s.StatusNode.AccountKeyStore() }, - node.ErrNoRunningNode, + node.ErrNoGethNode, }, { "non-null manager, no running node, get RPC Client", @@ -116,14 +116,14 @@ func (s *ManagerTestSuite) TestReferencesWithStartedNode() { { "node is running, get NodeConfig", func() (interface{}, error) { - return s.StatusNode.Config() + return s.StatusNode.Config(), nil }, ¶ms.NodeConfig{}, }, { "node is running, get Node", func() (interface{}, error) { - return s.StatusNode.GethNode() + return s.StatusNode.GethNode(), nil }, &gethnode.Node{}, }, @@ -188,7 +188,7 @@ func (s *ManagerTestSuite) TestNodeStartStop() { s.True(s.StatusNode.IsRunning()) // try starting another node (w/o stopping the previously started node) - s.Equal(node.ErrNodeExists, s.StatusNode.Start(nodeConfig)) + s.Equal(node.ErrNodeRunning, s.StatusNode.Start(nodeConfig)) // now stop node time.Sleep(100 * time.Millisecond) //https://github.com/status-im/status-go/issues/429#issuecomment-339663163 diff --git a/t/e2e/rpc/rpc_test.go b/t/e2e/rpc/rpc_test.go index 14c041288..db7c0ce9f 100644 --- a/t/e2e/rpc/rpc_test.go +++ b/t/e2e/rpc/rpc_test.go @@ -40,7 +40,6 @@ func (s *RPCTestSuite) TestCallRPC() { s.NoError(err) nodeConfig.IPCEnabled = false - nodeConfig.WSEnabled = false nodeConfig.HTTPHost = "" // to make sure that no HTTP interface is started if upstreamEnabled { diff --git a/t/e2e/testing.go b/t/e2e/testing.go index 3181bd8cb..61d2c75e2 100644 --- a/t/e2e/testing.go +++ b/t/e2e/testing.go @@ -29,9 +29,9 @@ func WithDataDir(path string) TestNodeOption { // FirstBlockHash validates Attach operation for the StatusNode. func FirstBlockHash(statusNode *node.StatusNode) (string, error) { // obtain RPC client for running node - runningNode, err := statusNode.GethNode() - if err != nil { - return "", err + runningNode := statusNode.GethNode() + if runningNode == nil { + return "", node.ErrNoGethNode } rpcClient, err := runningNode.Attach() diff --git a/t/e2e/whisper/whisper_ext_test.go b/t/e2e/whisper/whisper_ext_test.go index 0c0e19c6e..3884df0b5 100644 --- a/t/e2e/whisper/whisper_ext_test.go +++ b/t/e2e/whisper/whisper_ext_test.go @@ -43,10 +43,10 @@ func (s *WhisperExtensionSuite) SetupTest() { } func (s *WhisperExtensionSuite) TestSentSignal() { - node1, err := s.nodes[0].GethNode() - s.NoError(err) - node2, err := s.nodes[1].GethNode() - s.NoError(err) + node1 := s.nodes[0].GethNode() + s.NotNil(node1) + node2 := s.nodes[1].GethNode() + s.NotNil(node2) node1.Server().AddPeer(node2.Server().Self()) confirmed := make(chan common.Hash, 1) signal.SetDefaultNodeNotificationHandler(func(rawSignal string) { @@ -125,8 +125,8 @@ func (s *WhisperExtensionSuite) TestExpiredSignal() { func (s *WhisperExtensionSuite) TearDown() { for _, n := range s.nodes { - cfg, err := n.Config() - s.NoError(err) + cfg := n.Config() + s.NotNil(cfg) s.NoError(n.Stop()) s.NoError(os.Remove(cfg.DataDir)) } diff --git a/t/e2e/whisper/whisper_mailbox_test.go b/t/e2e/whisper/whisper_mailbox_test.go index b1d413a29..995647860 100644 --- a/t/e2e/whisper/whisper_mailbox_test.go +++ b/t/e2e/whisper/whisper_mailbox_test.go @@ -28,17 +28,18 @@ func TestWhisperMailboxTestSuite(t *testing.T) { } func (s *WhisperMailboxSuite) TestRequestMessageFromMailboxAsync() { + var err error // Start mailbox and status node. mailboxBackend, stop := s.startMailboxBackend() defer stop() - mailboxNode, err := mailboxBackend.StatusNode().GethNode() - s.Require().NoError(err) + s.Require().True(mailboxBackend.IsNodeRunning()) + mailboxNode := mailboxBackend.StatusNode().GethNode() mailboxEnode := mailboxNode.Server().NodeInfo().Enode sender, stop := s.startBackend("sender") defer stop() - node, err := sender.StatusNode().GethNode() - s.Require().NoError(err) + s.Require().True(sender.IsNodeRunning()) + node := sender.StatusNode().GethNode() s.Require().NotEqual(mailboxEnode, node.Server().NodeInfo().Enode) @@ -124,6 +125,8 @@ func (s *WhisperMailboxSuite) TestRequestMessageFromMailboxAsync() { } func (s *WhisperMailboxSuite) TestRequestMessagesInGroupChat() { + var err error + // Start mailbox, alice, bob, charlie node. mailboxBackend, stop := s.startMailboxBackend() defer stop() @@ -138,8 +141,8 @@ func (s *WhisperMailboxSuite) TestRequestMessagesInGroupChat() { defer stop() // Add mailbox to static peers. - mailboxNode, err := mailboxBackend.StatusNode().GethNode() - s.Require().NoError(err) + s.Require().True(mailboxBackend.IsNodeRunning()) + mailboxNode := mailboxBackend.StatusNode().GethNode() mailboxEnode := mailboxNode.Server().NodeInfo().Enode err = aliceBackend.StatusNode().AddPeer(mailboxEnode) diff --git a/t/e2e/whisper/whisper_test.go b/t/e2e/whisper/whisper_test.go index f5fc92e51..d4d9c8085 100644 --- a/t/e2e/whisper/whisper_test.go +++ b/t/e2e/whisper/whisper_test.go @@ -167,8 +167,8 @@ func (s *WhisperTestSuite) TestSelectedAccountOnRestart() { s.False(whisperService.HasKeyPair(pubKey1), "identity should be removed, but it is still present in whisper") // stop node (and all of its sub-protocols) - nodeConfig, err := s.Backend.StatusNode().Config() - s.NoError(err) + nodeConfig := s.Backend.StatusNode().Config() + s.NotNil(nodeConfig) preservedNodeConfig := *nodeConfig s.NoError(s.Backend.StopNode())