Lite wallet server https://hush.is
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

427 lines
15 KiB

// 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)
}