Bump for the GetTransaction crash fix, and make the version one value instead of two literals that could drift. It was duplicated: cmd/server/main.go had `var version = "0.1.1"` for --version, and frontend/service.go had "0.1.1-dragonxlightd" inline in the LightdInfo reply. The gRPC one is the load-bearing copy -- it is walletrpc/service.proto:48, so every client reads it, and it is the only way to tell from off-box which build a node is running. That property is the point of bumping now rather than later. With it, a rollout can be verified by probing each endpoint over TLS and reading the advertised version, instead of shelling in to compare binary checksums, and instead of the only alternative positive test -- calling GetTransaction on a mempool txid, which proves the fix by crashing any node that does not have it. Verified: --version prints 0.1.2, and a GetLightdInfo probe against a test instance returns 0.1.2-dragonxlightd where production still returns 0.1.1-dragonxlightd. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
274 lines
8.4 KiB
Go
274 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/peer"
|
|
"google.golang.org/grpc/reflection"
|
|
|
|
"git.hush.is/hush/lightwalletd/common"
|
|
"git.hush.is/hush/lightwalletd/frontend"
|
|
"git.hush.is/hush/lightwalletd/walletrpc"
|
|
)
|
|
|
|
var log *logrus.Entry
|
|
var logger = logrus.New()
|
|
|
|
func init() {
|
|
logger.SetFormatter(&logrus.TextFormatter{
|
|
//DisableColors: true,
|
|
FullTimestamp: true,
|
|
DisableLevelTruncation: true,
|
|
})
|
|
|
|
log = logger.WithFields(logrus.Fields{
|
|
"app": "frontend-grpc",
|
|
})
|
|
}
|
|
|
|
// TODO stream logging
|
|
|
|
func LoggingInterceptor() grpc.ServerOption {
|
|
return grpc.UnaryInterceptor(logInterceptor)
|
|
}
|
|
|
|
func logInterceptor(
|
|
ctx context.Context,
|
|
req interface{},
|
|
info *grpc.UnaryServerInfo,
|
|
handler grpc.UnaryHandler,
|
|
) (interface{}, error) {
|
|
reqLog := loggerFromContext(ctx)
|
|
start := time.Now()
|
|
|
|
resp, err := handler(ctx, req)
|
|
|
|
entry := reqLog.WithFields(logrus.Fields{
|
|
"method": info.FullMethod,
|
|
"duration": time.Since(start),
|
|
"error": err,
|
|
})
|
|
|
|
if err != nil {
|
|
entry.Error("call failed")
|
|
} else {
|
|
entry.Info("method called")
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
func loggerFromContext(ctx context.Context) *logrus.Entry {
|
|
// TODO: anonymize the addresses. cryptopan?
|
|
if peerInfo, ok := peer.FromContext(ctx); ok {
|
|
return log.WithFields(logrus.Fields{"peer_addr": peerInfo.Addr})
|
|
}
|
|
return log.WithFields(logrus.Fields{"peer_addr": "unknown"})
|
|
}
|
|
|
|
type Options struct {
|
|
bindAddr string `json:"bind_address,omitempty"`
|
|
tlsCertPath string `json:"tls_cert_path,omitempty"`
|
|
tlsKeyPath string `json:"tls_cert_key,omitempty"`
|
|
noTLS bool `json:no_tls,omitempty`
|
|
logLevel uint64 `json:"log_level,omitempty"`
|
|
logPath string `json:"log_file,omitempty"`
|
|
confPath string `json:"conf_file,omitempty"`
|
|
cacheSize int `json:"cache_size,omitempty"`
|
|
|
|
adaptiveLag bool `json:"adaptive_lag,omitempty"`
|
|
lagMin int `json:"lag_min,omitempty"`
|
|
lagMax int `json:"lag_max,omitempty"`
|
|
lagWindowMin int `json:"lag_window_min,omitempty"`
|
|
}
|
|
|
|
func main() {
|
|
var version = common.Version
|
|
|
|
opts := &Options{}
|
|
flag.StringVar(&opts.bindAddr, "bind-addr", "127.0.0.1:9069", "the address to listen on")
|
|
flag.StringVar(&opts.tlsCertPath, "tls-cert", "", "the path to a TLS certificate (optional)")
|
|
flag.StringVar(&opts.tlsKeyPath, "tls-key", "", "the path to a TLS key file (optional)")
|
|
flag.BoolVar(&opts.noTLS, "no-tls", false, "Disable TLS, serve un-encrypted traffic.")
|
|
flag.Uint64Var(&opts.logLevel, "log-level", uint64(logrus.InfoLevel), "log level (logrus 1-7)")
|
|
flag.StringVar(&opts.logPath, "log-file", "", "log file to write to")
|
|
flag.StringVar(&opts.confPath, "conf-file", "", "conf file to pull RPC creds from")
|
|
flag.IntVar(&opts.cacheSize, "cache-size", 400000, "number of blocks to hold in the cache")
|
|
flag.BoolVar(&opts.adaptiveLag, "adaptive-lag", true, "advertise a tip that trails the real tip by a reorg-rate-driven lag, so wallets anchor shielded spends at a settled height")
|
|
flag.IntVar(&opts.lagMin, "lag-min", 1, "minimum confirmation lag in blocks (applied when the chain is stable)")
|
|
flag.IntVar(&opts.lagMax, "lag-max", 12, "maximum confirmation lag in blocks (cap during heavy reorgs)")
|
|
flag.IntVar(&opts.lagWindowMin, "lag-window", 30, "minutes of recent reorg history used to size the adaptive lag")
|
|
|
|
// creating --version as a requirement of help2man
|
|
if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
|
|
fmt.Printf("DragonX lightwalletd version " + version + "\n")
|
|
os.Exit(0)
|
|
}
|
|
|
|
// TODO prod metrics
|
|
// TODO support config from file and env vars
|
|
flag.Parse()
|
|
|
|
if opts.confPath == "" {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
if !opts.noTLS && (opts.tlsCertPath == "" || opts.tlsKeyPath == "") {
|
|
println("Please specify a TLS certificate/key to use. You can use a self-signed certificate.")
|
|
println("See https://git.hush.is/hush/lightwalletd/src/branch/master/README.md")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if opts.logPath != "" {
|
|
// instead write parsable logs for logstash/splunk/etc
|
|
output, err := os.OpenFile(opts.logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"error": err,
|
|
"path": opts.logPath,
|
|
}).Fatal("couldn't open log file")
|
|
}
|
|
defer output.Close()
|
|
logger.SetOutput(output)
|
|
logger.SetFormatter(&logrus.JSONFormatter{})
|
|
}
|
|
|
|
logger.SetLevel(logrus.Level(opts.logLevel))
|
|
|
|
// gRPC initialization
|
|
var server *grpc.Server
|
|
|
|
if !opts.noTLS && (opts.tlsCertPath != "" && opts.tlsKeyPath != "") {
|
|
transportCreds, err := credentials.NewServerTLSFromFile(opts.tlsCertPath, opts.tlsKeyPath)
|
|
if err != nil {
|
|
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), LoggingInterceptor())
|
|
} else {
|
|
server = grpc.NewServer(LoggingInterceptor())
|
|
}
|
|
|
|
// Enable reflection for debugging
|
|
if opts.logLevel >= uint64(logrus.WarnLevel) {
|
|
reflection.Register(server)
|
|
}
|
|
|
|
// Initialize DragonX RPC client. Right now this is only for
|
|
// sending transactions, but in the future it could back a different type
|
|
// of block streamer.
|
|
|
|
rpcClient, err := frontend.NewZRPCFromConf(opts.confPath)
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"error": err,
|
|
}).Warn("DRAGONX.conf failed, will try empty credentials for rpc")
|
|
|
|
rpcClient, err = frontend.NewZRPCFromCreds("127.0.0.1:21769", "", "")
|
|
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"error": err,
|
|
}).Warn("couldn't start rpc conn. won't be able to send transactions")
|
|
}
|
|
}
|
|
|
|
// Get the sapling activation height from the RPC
|
|
saplingHeight, blockHeight, chainName, branchID, difficulty, longestchain, notarized, err := common.GetSaplingInfo(rpcClient)
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"error": err,
|
|
}).Warn("Unable to get sapling activation height")
|
|
}
|
|
|
|
log.Info("Got sapling height ", saplingHeight, " chain ", chainName, " branchID ", branchID, " difficulty ", difficulty, longestchain, " longestchain ", notarized, " notarized ")
|
|
|
|
// Fetch coinsupply for an informational startup log line only (the result is not
|
|
// used elsewhere). On a node whose supply index is cold this RPC can take minutes,
|
|
// so run it in the background rather than blocking the gRPC bind on it — otherwise
|
|
// the lite endpoint stays down for the entire duration on every restart. Clients
|
|
// still get coinsupply on demand via the GetCoinsupply RPC.
|
|
go func() {
|
|
result, coin, height, supply, zfunds, total, err := common.GetCoinsupply(rpcClient)
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"error": err,
|
|
}).Warn("Unable to get coinsupply")
|
|
return
|
|
}
|
|
log.Info(" result ", result, " coin ", coin, " height", height, "supply", supply, "zfunds", zfunds, "total", total)
|
|
}()
|
|
|
|
// Initialize the cache
|
|
cache := common.NewBlockCache(opts.cacheSize)
|
|
cache.ConfigureLag(opts.adaptiveLag, opts.lagWindowMin, opts.lagMin, opts.lagMax)
|
|
|
|
stopChan := make(chan bool, 1)
|
|
|
|
// Start the block cache importer at latestblock - 100k(cache size)
|
|
cacheStart := blockHeight - opts.cacheSize
|
|
if cacheStart < saplingHeight {
|
|
cacheStart = saplingHeight
|
|
}
|
|
|
|
go common.BlockIngestor(rpcClient, cache, log, stopChan, cacheStart)
|
|
|
|
// Compact transaction service initialization
|
|
service, err := frontend.NewSQLiteStreamer(rpcClient, cache, log)
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"error": err,
|
|
}).Fatal("couldn't create SQL backend")
|
|
}
|
|
defer service.(*frontend.SqlStreamer).GracefulStop()
|
|
|
|
// Register service
|
|
walletrpc.RegisterCompactTxStreamerServer(server, service)
|
|
|
|
// Start listening
|
|
listener, err := net.Listen("tcp", opts.bindAddr)
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"bind_addr": opts.bindAddr,
|
|
"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
|
|
log.WithFields(logrus.Fields{
|
|
"signal": s.String(),
|
|
}).Info("caught signal, stopping gRPC server")
|
|
// Stop the server
|
|
server.GracefulStop()
|
|
// Stop the block ingestor
|
|
stopChan <- true
|
|
}()
|
|
|
|
log.Infof("Starting gRPC server on %s", opts.bindAddr)
|
|
|
|
err = server.Serve(listener)
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"error": err,
|
|
}).Fatal("gRPC server exited")
|
|
}
|
|
}
|