// Copyright (c) 2019-2024 Duke Leto and The Hush developers // Copyright (c) 2019-2020 The Zcash developers // Distributed under the GPLv3 software license package cmd import ( "fmt" "net" "net/http" "os" "os/signal" "path/filepath" "strings" "syscall" "time" "github.com/btcsuite/btcd/rpcclient" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/reflection" "git.hush.is/hush/lightwalletd/common" "git.hush.is/hush/lightwalletd/common/logging" "git.hush.is/hush/lightwalletd/frontend" "git.hush.is/hush/lightwalletd/walletrpc" ) var cfgFile string var logger = logrus.New() // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "lightwalletd", Short: "Lightwalletd is a backend service to the Hush blockchain", Long: `Lightwalletd is a backend service that provides a bandwidth-efficient interface to the Hush blockchain`, Run: func(cmd *cobra.Command, args []string) { opts := &common.Options{ GRPCBindAddr: viper.GetString("grpc-bind-addr"), GRPCLogging: viper.GetBool("grpc-logging-insecure"), HTTPBindAddr: viper.GetString("http-bind-addr"), TLSCertPath: viper.GetString("tls-cert"), TLSKeyPath: viper.GetString("tls-key"), LogLevel: viper.GetUint64("log-level"), LogFile: viper.GetString("log-file"), HushConfPath: viper.GetString("hush-conf-path"), RPCUser: viper.GetString("rpcuser"), RPCPassword: viper.GetString("rpcpassword"), RPCHost: viper.GetString("rpchost"), RPCPort: viper.GetString("rpcport"), NoTLS: viper.GetBool("no-tls"), GenCertVeryInsecure: viper.GetBool("gen-cert-very-insecure"), DataDir: viper.GetString("data-dir"), Redownload: viper.GetBool("redownload"), SyncFromHeight: viper.GetInt("sync-from-height"), PingEnable: viper.GetBool("ping-very-insecure"), // Darkside: viper.GetBool("darkside-very-insecure"), // DarksideTimeout: viper.GetUint64("darkside-timeout"), } common.Log.Debugf("Options: %#v\n", opts) filesThatShouldExist := []string{ opts.LogFile, } if !fileExists(opts.LogFile) { os.OpenFile(opts.LogFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) } if /* !opts.Darkside && */ (opts.RPCUser == "" || opts.RPCPassword == "" || opts.RPCHost == "" || opts.RPCPort == "") { filesThatShouldExist = append(filesThatShouldExist, opts.HushConfPath) } if !opts.NoTLS && !opts.GenCertVeryInsecure { filesThatShouldExist = append(filesThatShouldExist, opts.TLSCertPath, opts.TLSKeyPath) } for _, filename := range filesThatShouldExist { if !fileExists(filename) { os.Stderr.WriteString(fmt.Sprintf("\n ** File does not exist: %s\n\n", filename)) common.Log.Fatal("required file ", filename, " does not exist") } } // Start server and block, or exit if err := startServer(opts); err != nil { common.Log.WithFields(logrus.Fields{ "error": err, }).Fatal("couldn't create server") } }, } func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return !info.IsDir() } func startServer(opts *common.Options) error { if opts.LogFile != "" { // instead write parsable logs for logstash/etc output, err := os.OpenFile(opts.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { common.Log.WithFields(logrus.Fields{ "error": err, "path": opts.LogFile, }).Fatal("couldn't open log file") } defer output.Close() logger.SetOutput(output) logger.SetFormatter(&logrus.JSONFormatter{}) } logger.SetLevel(logrus.Level(opts.LogLevel)) common.Log.WithFields(logrus.Fields{ "gitCommit": common.GitCommit, "buildDate": common.BuildDate, "buildUser": common.BuildUser, }).Infof("Starting gRPC server version %s on %s", common.Version, opts.GRPCBindAddr) logging.LogToStderr = opts.GRPCLogging // gRPC initialization var server *grpc.Server if opts.NoTLS { common.Log.Warningln("Starting no-TLS (plaintext) server") fmt.Println("Starting lightwalletd server") server = grpc.NewServer( grpc.StreamInterceptor( grpc_middleware.ChainStreamServer( grpc_prometheus.StreamServerInterceptor), ), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( logging.LogInterceptor, grpc_prometheus.UnaryServerInterceptor), )) } else { var transportCreds credentials.TransportCredentials if opts.GenCertVeryInsecure { common.Log.Warning("Certificate and key not provided, generating self signed values") fmt.Println("Starting insecure self-certificate server") tlsCert := common.GenerateCerts() transportCreds = credentials.NewServerTLSFromCert(tlsCert) } else { var err error transportCreds, err = credentials.NewServerTLSFromFile(opts.TLSCertPath, opts.TLSKeyPath) if err != nil { common.Log.WithFields(logrus.Fields{ "cert_file": opts.TLSCertPath, "key_path": opts.TLSKeyPath, "error": err, }).Fatal("couldn't load TLS credentials") } } server = grpc.NewServer( grpc.Creds(transportCreds), grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( grpc_prometheus.StreamServerInterceptor), ), grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( logging.LogInterceptor, grpc_prometheus.UnaryServerInterceptor), )) } grpc_prometheus.EnableHandlingTimeHistogram() grpc_prometheus.Register(server) go startHTTPServer(opts) // Enable reflection for debugging if opts.LogLevel >= uint64(logrus.WarnLevel) { reflection.Register(server) } // Initialize Hush RPC client. Right now (Jan 2018) this is only for // sending transactions, but in the future it could back a different type // of block streamer. var saplingHeight int var chainName string var rpcClient *rpcclient.Client var err error if false /* opts.Darkside */ { chainName = "darkside" } else { if opts.RPCUser != "" && opts.RPCPassword != "" && opts.RPCHost != "" && opts.RPCPort != "" { rpcClient, err = frontend.NewZRPCFromFlags(opts) } else { rpcClient, err = frontend.NewZRPCFromConf(opts.HushConfPath) } if err != nil { common.Log.WithFields(logrus.Fields{ "error": err, }).Fatal("setting up RPC connection to hushd") } // Indirect function for test mocking (so unit tests can talk to stub functions). common.RawRequest = rpcClient.RawRequest // Ensure that we can communicate with hushd common.FirstRPC() getLightdInfo, err := common.GetLightdInfo() if err != nil { common.Log.WithFields(logrus.Fields{ "error": err, }).Fatal("getting initial information from hushd") } common.Log.Info("Got sapling height ", getLightdInfo.SaplingActivationHeight, " block height ", getLightdInfo.BlockHeight, " chain ", getLightdInfo.ChainName, " branchID ", getLightdInfo.ConsensusBranchId) saplingHeight = int(getLightdInfo.SaplingActivationHeight) chainName = getLightdInfo.ChainName } dbPath := filepath.Join(opts.DataDir, "db") //if opts.Darkside { // os.RemoveAll(filepath.Join(dbPath, chainName)) //} // Temporary, because PR 320 put the db files in the wrong place // (one level too high, directly in "db/" instead of "db/chainname"), // so delete them if they're present. This can be removed sometime. os.Remove(filepath.Join(dbPath, "blocks")) os.Remove(filepath.Join(dbPath, "blocks-corrupted")) os.Remove(filepath.Join(dbPath, "lengths")) os.Remove(filepath.Join(dbPath, "lengths-corrupted")) if err := os.MkdirAll(opts.DataDir, 0755); err != nil { os.Stderr.WriteString(fmt.Sprintf("\n ** Can't create data directory: %s\n\n", opts.DataDir)) os.Exit(1) } if err := os.MkdirAll(dbPath, 0755); err != nil { os.Stderr.WriteString(fmt.Sprintf("\n ** Can't create db directory: %s\n\n", dbPath)) os.Exit(1) } syncFromHeight := opts.SyncFromHeight if opts.Redownload { syncFromHeight = 0 } cache := common.NewBlockCache(dbPath, chainName, saplingHeight, syncFromHeight) if !opts.Darkside { go common.BlockIngestor(cache, 0 /*loop forever*/) } else { // Darkside wants to control starting the block ingestor. //common.DarksideInit(cache, int(opts.DarksideTimeout)) } // Compact transaction service initialization { service, err := frontend.NewLwdStreamer(cache, chainName, opts.PingEnable) if err != nil { common.Log.WithFields(logrus.Fields{ "error": err, }).Fatal("couldn't create backend") } walletrpc.RegisterCompactTxStreamerServer(server, service) } //if opts.Darkside { // service, err := frontend.NewDarksideStreamer(cache) // if err != nil { // common.Log.WithFields(logrus.Fields{ // "error": err, // }).Fatal("couldn't create backend") // } // walletrpc.RegisterDarksideStreamerServer(server, service) //} // Start listening listener, err := net.Listen("tcp", opts.GRPCBindAddr) if err != nil { common.Log.WithFields(logrus.Fields{ "bind_addr": opts.GRPCBindAddr, "error": err, }).Fatal("couldn't create listener") } // Signal handler for graceful stops signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) go func() { s := <-signals cache.Sync() common.Log.WithFields(logrus.Fields{ "signal": s.String(), }).Info("caught signal, stopping gRPC server") os.Exit(1) }() err = server.Serve(listener) if err != nil { common.Log.WithFields(logrus.Fields{ "error": err, }).Fatal("gRPC server exited") } return nil } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } } func init() { rootCmd.AddCommand(versionCmd) cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is current directory, lightwalletd.yaml)") rootCmd.Flags().String("http-bind-addr", "127.0.0.1:9068", "the address to listen for http on") rootCmd.Flags().String("grpc-bind-addr", "127.0.0.1:9067", "the address to listen for grpc on") rootCmd.Flags().Bool("grpc-logging-insecure", false, "enable grpc logging to stderr") rootCmd.Flags().String("tls-cert", "./cert.pem", "the path to a TLS certificate") rootCmd.Flags().String("tls-key", "./cert.key", "the path to a TLS key file") rootCmd.Flags().Int("log-level", int(logrus.InfoLevel), "log level (logrus 1-7)") rootCmd.Flags().String("log-file", "./server.log", "log file to write to") rootCmd.Flags().String("hush-conf-path", "./HUSH3.conf", "conf file to pull RPC creds from") rootCmd.Flags().String("rpcuser", "", "RPC user name") rootCmd.Flags().String("rpcpassword", "", "RPC password") rootCmd.Flags().String("rpchost", "", "RPC host") rootCmd.Flags().String("rpcport", "", "RPC host port") rootCmd.Flags().Bool("no-tls", false, "run without TLS, only safe if a reverse proxy like nginx does TLS on localhost") rootCmd.Flags().Bool("gen-cert-very-insecure", false, "run with self-signed TLS certificate, only for debugging, DO NOT use in production") rootCmd.Flags().Bool("redownload", false, "re-fetch all blocks from hushd; reinitialize local cache files") rootCmd.Flags().Int("sync-from-height", -1, "re-fetch blocks from hushd start at this height") rootCmd.Flags().String("data-dir", "/var/lib/lightwalletd", "data directory (such as db)") rootCmd.Flags().Bool("ping-very-insecure", false, "allow Ping GRPC for testing") // rootCmd.Flags().Bool("darkside-very-insecure", false, "run with GRPC-controllable mock hushd for integration testing (shuts down after 30 minutes)") // rootCmd.Flags().Int("darkside-timeout", 30, "override 30 minute default darkside timeout") viper.BindPFlag("grpc-bind-addr", rootCmd.Flags().Lookup("grpc-bind-addr")) viper.SetDefault("grpc-bind-addr", "127.0.0.1:9067") viper.BindPFlag("grpc-logging-insecure", rootCmd.Flags().Lookup("grpc-logging-insecure")) viper.SetDefault("grpc-logging-insecure", false) viper.BindPFlag("http-bind-addr", rootCmd.Flags().Lookup("http-bind-addr")) viper.SetDefault("http-bind-addr", "127.0.0.1:9068") viper.BindPFlag("tls-cert", rootCmd.Flags().Lookup("tls-cert")) viper.SetDefault("tls-cert", "./cert.pem") viper.BindPFlag("tls-key", rootCmd.Flags().Lookup("tls-key")) viper.SetDefault("tls-key", "./cert.key") viper.BindPFlag("log-level", rootCmd.Flags().Lookup("log-level")) viper.SetDefault("log-level", int(logrus.InfoLevel)) viper.BindPFlag("log-file", rootCmd.Flags().Lookup("log-file")) viper.SetDefault("log-file", "./server.log") viper.BindPFlag("hush-conf-path", rootCmd.Flags().Lookup("hush-conf-path")) viper.SetDefault("hush-conf-path", "./HUSH3.conf") viper.BindPFlag("rpcuser", rootCmd.Flags().Lookup("rpcuser")) viper.BindPFlag("rpcpassword", rootCmd.Flags().Lookup("rpcpassword")) viper.BindPFlag("rpchost", rootCmd.Flags().Lookup("rpchost")) viper.BindPFlag("rpcport", rootCmd.Flags().Lookup("rpcport")) viper.BindPFlag("no-tls", rootCmd.Flags().Lookup("no-tls")) viper.SetDefault("no-tls", false) viper.BindPFlag("gen-cert-very-insecure", rootCmd.Flags().Lookup("gen-cert-very-insecure")) viper.SetDefault("gen-cert-very-insecure", false) viper.BindPFlag("redownload", rootCmd.Flags().Lookup("redownload")) viper.SetDefault("redownload", false) viper.BindPFlag("sync-from-height", rootCmd.Flags().Lookup("sync-from-height")) viper.SetDefault("sync-from-height", -1) viper.BindPFlag("data-dir", rootCmd.Flags().Lookup("data-dir")) viper.SetDefault("data-dir", "/var/lib/lightwalletd") viper.BindPFlag("ping-very-insecure", rootCmd.Flags().Lookup("ping-very-insecure")) viper.SetDefault("ping-very-insecure", false) // viper.BindPFlag("darkside-very-insecure", rootCmd.Flags().Lookup("darkside-very-insecure")) // viper.SetDefault("darkside-very-insecure", false) // viper.BindPFlag("darkside-timeout", rootCmd.Flags().Lookup("darkside-timeout")) // viper.SetDefault("darkside-timeout", 30) logger.SetFormatter(&logrus.TextFormatter{ //DisableColors: true, FullTimestamp: true, DisableLevelTruncation: true, }) onexit := func() { fmt.Printf("Lightwalletd died with a Fatal error. Check logfile for details.\n") } common.Log = logger.WithFields(logrus.Fields{ "app": "lightwalletd", }) logrus.RegisterExitHandler(onexit) // Indirect functions for test mocking (so unit tests can talk to stub functions) common.Time.Sleep = time.Sleep common.Time.Now = time.Now } // initConfig reads in config file and ENV variables if set. func initConfig() { if cfgFile != "" { // Use config file from the flag. viper.SetConfigFile(cfgFile) } else { // Look in the current directory for a configuration file viper.AddConfigPath(".") // Viper auto appends extension to this config name // For example, lightwalletd.yml viper.SetConfigName("lightwalletd") } // Replace `-` in config options with `_` for ENV keys replacer := strings.NewReplacer("-", "_") viper.SetEnvKeyReplacer(replacer) viper.AutomaticEnv() // read in environment variables that match // If a config file is found, read it in. var err error if err = viper.ReadInConfig(); err == nil { fmt.Println("Using config file:", viper.ConfigFileUsed()) } } func startHTTPServer(opts *common.Options) { http.Handle("/metrics", promhttp.Handler()) http.ListenAndServe(opts.HTTPBindAddr, nil) }