github.com/status-im/status-go@v1.1.0/cmd/statusd/main.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	stdlog "log"
    10  	"os"
    11  	"os/signal"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/google/uuid"
    18  	"github.com/okzk/sdnotify"
    19  	"golang.org/x/crypto/ssh/terminal"
    20  
    21  	"github.com/ethereum/go-ethereum/log"
    22  	gethmetrics "github.com/ethereum/go-ethereum/metrics"
    23  
    24  	"github.com/status-im/status-go/api"
    25  	"github.com/status-im/status-go/appdatabase"
    26  	"github.com/status-im/status-go/cmd/statusd/server"
    27  	"github.com/status-im/status-go/common/dbsetup"
    28  	gethbridge "github.com/status-im/status-go/eth-node/bridge/geth"
    29  	"github.com/status-im/status-go/eth-node/crypto"
    30  	"github.com/status-im/status-go/logutils"
    31  	"github.com/status-im/status-go/metrics"
    32  	nodemetrics "github.com/status-im/status-go/metrics/node"
    33  	"github.com/status-im/status-go/node"
    34  	"github.com/status-im/status-go/params"
    35  	"github.com/status-im/status-go/profiling"
    36  	"github.com/status-im/status-go/protocol"
    37  	"github.com/status-im/status-go/protocol/pushnotificationserver"
    38  	"github.com/status-im/status-go/protocol/requests"
    39  	"github.com/status-im/status-go/walletdatabase"
    40  )
    41  
    42  const (
    43  	serverClientName = "Statusd"
    44  )
    45  
    46  var (
    47  	configFiles                    configFlags
    48  	logLevel                       = flag.String("log", "", `Log level, one of: "ERROR", "WARN", "INFO", "DEBUG", and "TRACE"`)
    49  	logWithoutColors               = flag.Bool("log-without-color", false, "Disables log colors")
    50  	ipcEnabled                     = flag.Bool("ipc", false, "Enable IPC RPC endpoint")
    51  	ipcFile                        = flag.String("ipcfile", "", "Set IPC file path")
    52  	pprofEnabled                   = flag.Bool("pprof", false, "Enable runtime profiling via pprof")
    53  	pprofPort                      = flag.Int("pprof-port", 52525, "Port for runtime profiling via pprof")
    54  	communityArchiveSupportEnabled = flag.Bool("community-archives", false, "Enable community history archive support")
    55  	torrentClientPort              = flag.Int("torrent-client-port", 9025, "Port for BitTorrent protocol connections")
    56  	version                        = flag.Bool("version", false, "Print version and dump configuration")
    57  
    58  	dataDir    = flag.String("dir", getDefaultDataDir(), "Directory used by node to store data")
    59  	register   = flag.Bool("register", false, "Register and make the node discoverable by other nodes")
    60  	mailserver = flag.Bool("mailserver", false, "Enable Mail Server with default configuration")
    61  	networkID  = flag.Int(
    62  		"network-id",
    63  		params.GoerliNetworkID,
    64  		fmt.Sprintf(
    65  			"A network ID: %d (Mainnet), %d (Goerli)",
    66  			params.MainNetworkID, params.GoerliNetworkID,
    67  		),
    68  	)
    69  	fleet = flag.String(
    70  		"fleet",
    71  		params.FleetProd,
    72  		fmt.Sprintf(
    73  			"Select fleet: %s (default %s)",
    74  			[]string{
    75  				params.FleetProd,
    76  				params.FleetStatusStaging,
    77  				params.FleetStatusProd,
    78  				params.FleetWakuSandbox,
    79  				params.FleetWakuTest,
    80  			},
    81  			params.FleetProd,
    82  		),
    83  	)
    84  	listenAddr = flag.String("addr", "", "address to bind listener to")
    85  	serverAddr = flag.String("server", "", "Address `host:port` for HTTP API server of statusd")
    86  
    87  	// don't change the name of this flag, https://github.com/ethereum/go-ethereum/blob/master/metrics/metrics.go#L41
    88  	metricsEnabled = flag.Bool("metrics", false, "Expose ethereum metrics with debug_metrics jsonrpc call")
    89  	metricsPort    = flag.Int("metrics-port", 9305, "Port for the Prometheus /metrics endpoint")
    90  	seedPhrase     = flag.String("seed-phrase", "", "Seed phrase for account creation")
    91  	password       = flag.String("password", "", "Password for account")
    92  )
    93  
    94  // All general log messages in this package should be routed through this logger.
    95  var logger = log.New("package", "status-go/cmd/statusd")
    96  
    97  func init() {
    98  	flag.Var(&configFiles, "c", "JSON configuration file(s). Multiple configuration files can be specified, and will be merged in occurrence order")
    99  }
   100  
   101  // nolint:gocyclo
   102  func main() {
   103  	colors := terminal.IsTerminal(int(os.Stdin.Fd()))
   104  	if err := logutils.OverrideRootLog(true, "ERROR", logutils.FileOptions{}, colors); err != nil {
   105  		stdlog.Fatalf("Error initializing logger: %v", err)
   106  	}
   107  
   108  	flag.Usage = printUsage
   109  	flag.Parse()
   110  	if flag.NArg() > 0 {
   111  		printUsage()
   112  		logger.Error("Extra args in command line: %v", flag.Args())
   113  		os.Exit(1)
   114  	}
   115  
   116  	if *seedPhrase != "" && *password == "" {
   117  		printUsage()
   118  		logger.Error("password is required when seed phrase is provided")
   119  		os.Exit(1)
   120  	}
   121  
   122  	opts := []params.Option{params.WithFleet(*fleet)}
   123  	if *mailserver {
   124  		opts = append(opts, params.WithMailserver())
   125  	}
   126  
   127  	config, err := params.NewNodeConfigWithDefaultsAndFiles(
   128  		*dataDir,
   129  		uint64(*networkID),
   130  		opts,
   131  		configFiles,
   132  	)
   133  	if err != nil {
   134  		printUsage()
   135  		logger.Error(err.Error())
   136  		os.Exit(1)
   137  	}
   138  
   139  	// Use listenAddr if and only if explicitly provided in the arguments.
   140  	// The default value is set in params.NewNodeConfigWithDefaultsAndFiles().
   141  	if *listenAddr != "" {
   142  		config.ListenAddr = *listenAddr
   143  	}
   144  
   145  	if *register && *mailserver {
   146  		config.RegisterTopics = append(config.RegisterTopics, params.MailServerDiscv5Topic)
   147  	} else if *register {
   148  		config.RegisterTopics = append(config.RegisterTopics, params.WhisperDiscv5Topic)
   149  	}
   150  
   151  	// enable IPC RPC
   152  	if *ipcEnabled {
   153  		config.IPCEnabled = true
   154  		config.IPCFile = *ipcFile
   155  	}
   156  
   157  	if *communityArchiveSupportEnabled {
   158  		config.TorrentConfig.Enabled = true
   159  		config.TorrentConfig.Port = *torrentClientPort
   160  	}
   161  
   162  	// set up logging options
   163  	setupLogging(config)
   164  
   165  	// We want statusd to be distinct from StatusIM client.
   166  	config.Name = serverClientName
   167  
   168  	if *version {
   169  		printVersion(config)
   170  		return
   171  	}
   172  
   173  	if serverAddr != nil && *serverAddr != "" {
   174  		srv := server.NewServer()
   175  		srv.Setup()
   176  		err = srv.Listen(*serverAddr)
   177  		if err != nil {
   178  			logger.Error("failed to start server", "error", err)
   179  			return
   180  		}
   181  		log.Info("server started", "address", srv.Address())
   182  		defer func() {
   183  			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   184  			defer cancel()
   185  			srv.Stop(ctx)
   186  		}()
   187  	}
   188  
   189  	backend := api.NewGethStatusBackend()
   190  	if config.NodeKey == "" {
   191  		logger.Error("node key needs to be set if running a push notification server")
   192  		return
   193  	}
   194  
   195  	identity, err := crypto.HexToECDSA(config.NodeKey)
   196  	if err != nil {
   197  		logger.Error("node key is invalid", "error", err)
   198  		return
   199  	}
   200  
   201  	// Generate installationID from public key, so it's always the same
   202  	installationID, err := uuid.FromBytes(crypto.CompressPubkey(&identity.PublicKey)[:16])
   203  	if err != nil {
   204  		logger.Error("cannot create installation id", "error", err)
   205  		return
   206  	}
   207  
   208  	if *seedPhrase != "" {
   209  		// Remove data inside dir to avoid conflicts with existing data or account restoration fails
   210  		if err := os.RemoveAll(config.DataDir); err != nil {
   211  			logger.Error("failed to remove data dir", "error", err)
   212  			return
   213  		}
   214  
   215  		if err := createDirsFromConfig(config); err != nil {
   216  			logger.Error("failed to create directories", "error", err)
   217  			return
   218  		}
   219  
   220  		request := requests.RestoreAccount{
   221  			Mnemonic:    *seedPhrase,
   222  			FetchBackup: false,
   223  			CreateAccount: requests.CreateAccount{
   224  				DisplayName:        "Account1",
   225  				DeviceName:         "StatusIM",
   226  				Password:           *password,
   227  				CustomizationColor: "0x000000",
   228  				RootDataDir:        config.DataDir,
   229  				APIConfig: &requests.APIConfig{
   230  					ConnectorEnabled: config.ClusterConfig.Enabled,
   231  					HTTPEnabled:      config.HTTPEnabled,
   232  					HTTPHost:         config.HTTPHost,
   233  					HTTPPort:         config.HTTPPort,
   234  					HTTPVirtualHosts: config.HTTPVirtualHosts,
   235  					WSEnabled:        config.WSEnabled,
   236  					WSHost:           config.WSHost,
   237  					WSPort:           config.WSPort,
   238  					APIModules:       config.APIModules,
   239  				},
   240  				NetworkID:            &config.NetworkID,
   241  				TestOverrideNetworks: config.Networks,
   242  			},
   243  		}
   244  
   245  		api.OverrideApiConfigTest()
   246  
   247  		_, err := backend.RestoreAccountAndLogin(&request)
   248  		if err != nil {
   249  			logger.Error("failed to import account", "error", err)
   250  			return
   251  		}
   252  
   253  		appDB, walletDB, err := openDatabases(config.DataDir + "/" + installationID.String())
   254  		if err != nil {
   255  			log.Error("failed to open databases")
   256  			return
   257  		}
   258  
   259  		options := []protocol.Option{
   260  			protocol.WithDatabase(appDB),
   261  			protocol.WithWalletDatabase(walletDB),
   262  			protocol.WithTorrentConfig(&config.TorrentConfig),
   263  			protocol.WithWalletConfig(&config.WalletConfig),
   264  			protocol.WithAccountManager(backend.AccountManager()),
   265  		}
   266  
   267  		messenger, err := protocol.NewMessenger(
   268  			config.Name,
   269  			identity,
   270  			gethbridge.NewNodeBridge(backend.StatusNode().GethNode(), backend.StatusNode().WakuService(), backend.StatusNode().WakuV2Service()),
   271  			installationID.String(),
   272  			nil,
   273  			config.Version,
   274  			options...,
   275  		)
   276  
   277  		if err != nil {
   278  			logger.Error("failed to create messenger", "error", err)
   279  			return
   280  		}
   281  
   282  		_, err = messenger.Start()
   283  		if err != nil {
   284  			logger.Error("failed to start messenger", "error", err)
   285  			return
   286  		}
   287  
   288  		interruptCh := haltOnInterruptSignal(backend.StatusNode())
   289  		go retrieveMessagesLoop(messenger, 300*time.Millisecond, interruptCh)
   290  
   291  	} else {
   292  		appDB, walletDB, err := startNode(config, backend, installationID)
   293  		if err != nil {
   294  			logger.Error("failed to start node", "error", err)
   295  			return
   296  		}
   297  
   298  		err = sdnotify.Ready()
   299  		if err == sdnotify.ErrSdNotifyNoSocket {
   300  			logger.Debug("sd_notify socket not available")
   301  		} else if err != nil {
   302  			logger.Warn("sd_notify READY call failed", "error", err)
   303  		} else {
   304  			// systemd aliveness notifications, affects only Linux
   305  			go startSystemDWatchdog()
   306  		}
   307  
   308  		// handle interrupt signals
   309  		interruptCh := haltOnInterruptSignal(backend.StatusNode())
   310  
   311  		// Start collecting metrics. Metrics can be enabled by providing `-metrics` flag
   312  		// or setting `gethmetrics.Enabled` to true during compilation time:
   313  		// https://github.com/status-im/go-ethereum/pull/76.
   314  		if *metricsEnabled || gethmetrics.Enabled {
   315  			go startCollectingNodeMetrics(interruptCh, backend.StatusNode())
   316  			go gethmetrics.CollectProcessMetrics(3 * time.Second)
   317  			go metrics.NewMetricsServer(*metricsPort, gethmetrics.DefaultRegistry).Listen()
   318  		}
   319  
   320  		// Check if profiling shall be enabled.
   321  		if *pprofEnabled {
   322  			profiling.NewProfiler(*pprofPort).Go()
   323  		}
   324  
   325  		if config.PushNotificationServerConfig.Enabled {
   326  			options := []protocol.Option{
   327  				protocol.WithPushNotifications(),
   328  				protocol.WithPushNotificationServerConfig(&pushnotificationserver.Config{
   329  					Enabled:   config.PushNotificationServerConfig.Enabled,
   330  					Identity:  config.PushNotificationServerConfig.Identity,
   331  					GorushURL: config.PushNotificationServerConfig.GorushURL,
   332  				}),
   333  				protocol.WithDatabase(appDB),
   334  				protocol.WithWalletDatabase(walletDB),
   335  				protocol.WithTorrentConfig(&config.TorrentConfig),
   336  				protocol.WithWalletConfig(&config.WalletConfig),
   337  				protocol.WithAccountManager(backend.AccountManager()),
   338  			}
   339  
   340  			messenger, err := protocol.NewMessenger(
   341  				config.Name,
   342  				identity,
   343  				gethbridge.NewNodeBridge(backend.StatusNode().GethNode(), backend.StatusNode().WakuService(), backend.StatusNode().WakuV2Service()),
   344  				installationID.String(),
   345  				nil,
   346  				config.Version,
   347  				options...,
   348  			)
   349  			if err != nil {
   350  				logger.Error("failed to create messenger", "error", err)
   351  				return
   352  			}
   353  
   354  			err = messenger.InitInstallations()
   355  			if err != nil {
   356  				logger.Error("failed to init messenger installations", "error", err)
   357  				return
   358  			}
   359  
   360  			err = messenger.InitFilters()
   361  			if err != nil {
   362  				logger.Error("failed to init messenger filters", "error", err)
   363  				return
   364  			}
   365  
   366  			// This will start the push notification server as well as
   367  			// the config is set to Enabled
   368  			_, err = messenger.Start()
   369  			if err != nil {
   370  				logger.Error("failed to start messenger", "error", err)
   371  				return
   372  			}
   373  			go retrieveMessagesLoop(messenger, 300*time.Millisecond, interruptCh)
   374  		}
   375  	}
   376  
   377  	gethNode := backend.StatusNode().GethNode()
   378  	if gethNode != nil {
   379  		// wait till node has been stopped
   380  		gethNode.Wait()
   381  		if err := sdnotify.Stopping(); err != nil {
   382  			logger.Warn("sd_notify STOPPING call failed", "error", err)
   383  		}
   384  	}
   385  }
   386  
   387  func getDefaultDataDir() string {
   388  	if home := os.Getenv("HOME"); home != "" {
   389  		return filepath.Join(home, ".statusd")
   390  	}
   391  	return "./statusd-data"
   392  }
   393  
   394  func setupLogging(config *params.NodeConfig) {
   395  	if *logLevel != "" {
   396  		config.LogLevel = *logLevel
   397  	}
   398  
   399  	logSettings := logutils.LogSettings{
   400  		Enabled:         config.LogEnabled,
   401  		MobileSystem:    config.LogMobileSystem,
   402  		Level:           config.LogLevel,
   403  		File:            config.LogFile,
   404  		MaxSize:         config.LogMaxSize,
   405  		MaxBackups:      config.LogMaxBackups,
   406  		CompressRotated: config.LogCompressRotated,
   407  	}
   408  	colors := !(*logWithoutColors) && terminal.IsTerminal(int(os.Stdin.Fd()))
   409  	if err := logutils.OverrideRootLogWithConfig(logSettings, colors); err != nil {
   410  		stdlog.Fatalf("Error initializing logger: %v", err)
   411  	}
   412  }
   413  
   414  // loop for notifying systemd about process being alive
   415  func startSystemDWatchdog() {
   416  	for range time.Tick(30 * time.Second) {
   417  		if err := sdnotify.Watchdog(); err != nil {
   418  			logger.Warn("sd_notify WATCHDOG call failed", "error", err)
   419  		}
   420  	}
   421  }
   422  
   423  // startCollectingStats collects various stats about the node and other protocols like Whisper.
   424  func startCollectingNodeMetrics(interruptCh <-chan struct{}, statusNode *node.StatusNode) {
   425  	logger.Info("Starting collecting node metrics")
   426  
   427  	gethNode := statusNode.GethNode()
   428  	if gethNode == nil {
   429  		logger.Error("Failed to run metrics because it could not get the node")
   430  		return
   431  	}
   432  
   433  	ctx, cancel := context.WithCancel(context.Background())
   434  	defer cancel()
   435  	go func() {
   436  		// Try to subscribe and collect metrics. In case of an error, retry.
   437  		for {
   438  			if err := nodemetrics.SubscribeServerEvents(ctx, gethNode); err != nil {
   439  				logger.Error("Failed to subscribe server events", "error", err)
   440  			} else {
   441  				// no error means that the subscription was terminated by purpose
   442  				return
   443  			}
   444  
   445  			time.Sleep(time.Second)
   446  		}
   447  	}()
   448  
   449  	<-interruptCh
   450  }
   451  
   452  var (
   453  	errStatusServiceRequiresIPC  = errors.New("to enable the StatusService on IPC, -ipc flag must be set")
   454  	errStatusServiceRequiresHTTP = errors.New("to enable the StatusService on HTTP, -http flag must be set")
   455  	errStatusServiceInvalidFlag  = errors.New("-status flag valid values are: ipc, http")
   456  )
   457  
   458  func configureStatusService(flagValue string, nodeConfig *params.NodeConfig) (*params.NodeConfig, error) {
   459  	switch flagValue {
   460  	case "ipc":
   461  		if !nodeConfig.IPCEnabled {
   462  			return nil, errStatusServiceRequiresIPC
   463  		}
   464  		nodeConfig.EnableStatusService = true
   465  	case "http":
   466  		if !nodeConfig.HTTPEnabled {
   467  			return nil, errStatusServiceRequiresHTTP
   468  		}
   469  		nodeConfig.EnableStatusService = true
   470  		nodeConfig.AddAPIModule("status")
   471  	case "":
   472  		nodeConfig.EnableStatusService = false
   473  	default:
   474  		return nil, errStatusServiceInvalidFlag
   475  	}
   476  
   477  	return nodeConfig, nil
   478  }
   479  
   480  // printVersion prints verbose output about version and config.
   481  func printVersion(config *params.NodeConfig) {
   482  	fmt.Println(strings.Title(config.Name))
   483  	fmt.Println("Version:", config.Version)
   484  	fmt.Println("Network ID:", config.NetworkID)
   485  	fmt.Println("Go Version:", runtime.Version())
   486  	fmt.Println("OS:", runtime.GOOS)
   487  	fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH"))
   488  	fmt.Printf("GOROOT=%s\n", runtime.GOROOT())
   489  
   490  	fmt.Println("Loaded Config: ", config)
   491  }
   492  
   493  func printUsage() {
   494  	usage := `
   495  Usage: statusd [options]
   496  Examples:
   497  	statusd                                        # run regular Whisper node that joins Status network
   498  	statusd -c ./default.json                      # run node with configuration specified in ./default.json file
   499  	statusd -c ./default.json -c ./standalone.json # run node with configuration specified in ./default.json file, after merging ./standalone.json file
   500  	statusd -c ./default.json -metrics             # run node with configuration specified in ./default.json file, and expose ethereum metrics with debug_metrics jsonrpc call
   501  	statusd -c ./default.json -log DEBUG --seed-phrase="test test test test test test test test test test test junk" --password=password # run node with configuration specified in ./default.json file, and import account with seed phrase and password
   502  
   503  Options:
   504  `
   505  	fmt.Fprint(os.Stderr, usage)
   506  	flag.PrintDefaults()
   507  }
   508  
   509  // haltOnInterruptSignal catches interrupt signal (SIGINT) and
   510  // stops the node. It times out after 5 seconds
   511  // if the node can not be stopped.
   512  func haltOnInterruptSignal(statusNode *node.StatusNode) <-chan struct{} {
   513  	interruptCh := make(chan struct{})
   514  	go func() {
   515  		signalCh := make(chan os.Signal, 1)
   516  		signal.Notify(signalCh, os.Interrupt)
   517  		defer signal.Stop(signalCh)
   518  		<-signalCh
   519  		close(interruptCh)
   520  		logger.Info("Got interrupt, shutting down...")
   521  		if err := statusNode.Stop(); err != nil {
   522  			logger.Error("Failed to stop node", "error", err)
   523  			os.Exit(1)
   524  		}
   525  	}()
   526  	return interruptCh
   527  }
   528  
   529  // retrieveMessagesLoop fetches messages from a messenger so that they are processed
   530  func retrieveMessagesLoop(messenger *protocol.Messenger, tick time.Duration, cancel <-chan struct{}) {
   531  	ticker := time.NewTicker(tick)
   532  	defer ticker.Stop()
   533  
   534  	for {
   535  		select {
   536  		case <-ticker.C:
   537  			_, err := messenger.RetrieveAll()
   538  			if err != nil {
   539  				logger.Error("failed to retrieve raw messages", "err", err)
   540  				continue
   541  			}
   542  		case <-cancel:
   543  			return
   544  		}
   545  	}
   546  }
   547  
   548  func openDatabases(path string) (*sql.DB, *sql.DB, error) {
   549  	walletDB, err := walletdatabase.InitializeDB(path+"-wallet.db", "", dbsetup.ReducedKDFIterationsNumber)
   550  	if err != nil {
   551  		logger.Error("failed to initialize wallet db", "error", err)
   552  		return nil, nil, err
   553  	}
   554  
   555  	appDB, err := appdatabase.InitializeDB(path+".db", "", dbsetup.ReducedKDFIterationsNumber)
   556  	if err != nil {
   557  		logger.Error("failed to initialize app db", "error", err)
   558  		return nil, nil, err
   559  	}
   560  
   561  	return appDB, walletDB, nil
   562  }
   563  
   564  func createDirsFromConfig(config *params.NodeConfig) error {
   565  	// If DataDir is empty, it means we want to create an ephemeral node
   566  	// keeping data only in memory.
   567  	if config.DataDir != "" {
   568  		// make sure data directory exists
   569  		if err := os.MkdirAll(filepath.Clean(config.DataDir), os.ModePerm); err != nil {
   570  			return fmt.Errorf("make node: make data directory: %v", err)
   571  		}
   572  	}
   573  
   574  	if config.KeyStoreDir != "" {
   575  		// make sure keys directory exists
   576  		if err := os.MkdirAll(filepath.Clean(config.KeyStoreDir), os.ModePerm); err != nil {
   577  			return fmt.Errorf("make node: make keys directory: %v", err)
   578  		}
   579  	}
   580  
   581  	return nil
   582  }
   583  
   584  func startNode(config *params.NodeConfig, backend *api.GethStatusBackend, installationID uuid.UUID) (*sql.DB, *sql.DB, error) {
   585  	err := backend.AccountManager().InitKeystore(config.KeyStoreDir)
   586  	if err != nil {
   587  		logger.Error("Failed to init keystore", "error", err)
   588  		return nil, nil, err
   589  	}
   590  
   591  	err = createDirsFromConfig(config)
   592  	if err != nil {
   593  		logger.Error("failed to create directories", "error", err)
   594  		return nil, nil, err
   595  	}
   596  
   597  	appDB, walletDB, err := openDatabases(config.DataDir + "/" + installationID.String())
   598  	if err != nil {
   599  		log.Error("failed to open databases")
   600  		return nil, nil, err
   601  	}
   602  
   603  	backend.StatusNode().SetAppDB(appDB)
   604  	backend.StatusNode().SetWalletDB(walletDB)
   605  
   606  	err = backend.StartNode(config)
   607  	if err != nil {
   608  		logger.Error("Node start failed", "error", err)
   609  		return nil, nil, err
   610  	}
   611  
   612  	return appDB, walletDB, nil
   613  }