Review follow-ups to 73a93f1. Bound in-flight calls. rpcclient's single sendPostHandler goroutine imposed an accidental ceiling of one concurrent RPC; removing it without putting anything in its place left no bound at all. grpc-go supplies none either -- this server sets no MaxConcurrentStreams, so the default is math.MaxUint32 -- and dragonxd answers RPC with 8 worker threads behind a 4096-deep queue, shared on the pool node with getblocktemplate. Overload would therefore surface as mining latency rather than as an error we could back off on. MaxConnsPerHost blocks the caller at the limit instead of dialling more, which is the backpressure wanted; MaxIdleConnsPerHost alone would only cap reuse and let us exceed the limit while churning connections. Default 8, matching the node's DEFAULT_HTTP_THREADS, tunable with -rpc-max-concurrent. Even 8 removes all of the head-of-line blocking this work set out to fix. IdleConnTimeout 90s -> 20s. dragonxd closes idle connections at 30s (DEFAULT_HTTP_SERVER_TIMEOUT, applied via evhttp_set_timeout and not overridden in DRAGONX.conf). At 90s we were always the second to close, so a request could be written into a connection the server had already sent a FIN for, and Go will not retry a POST once bytes are on the wire. Closing first removes the race. Reject a negative -rpc-timeout, which silently meant "unbounded", the same as the documented 0. The check has to run after flag.Parse(); it was initially placed before it and never fired. Also correct the coinsupply note: hush_coinsupply walks the block index back to genesis, loading each block from disk and memoising newcoins and zfunds into the CBlockIndex, so the first call pays for the whole chain and later ones are nearly free. It is not a UTXO-set scan, as the earlier comment claimed. The measured 48s/3s figures are unchanged. Verified: five concurrent GetLightdInfo calls all return grpc-status 0 with no errors, 46 blocks ingested, and the daemon holds 2 sockets to the node rather than one per request; a negative timeout exits 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
286 lines
9.2 KiB
Go
286 lines
9.2 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"`
|
|
|
|
rpcTimeout time.Duration `json:"rpc_timeout,omitempty"`
|
|
rpcMaxConcurrent int `json:"rpc_max_concurrent,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")
|
|
flag.DurationVar(&opts.rpcTimeout, "rpc-timeout", frontend.DefaultRPCTimeout, "bound a single dragonxd JSON-RPC call; 0 disables. Without it one stuck call blocks every other caller")
|
|
flag.IntVar(&opts.rpcMaxConcurrent, "rpc-max-concurrent", frontend.DefaultRPCMaxConcurrent, "maximum dragonxd JSON-RPC calls in flight at once; matches the node's RPC worker threads")
|
|
|
|
// 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()
|
|
|
|
// A negative duration silently means "no timeout", the same as the
|
|
// documented 0, so reject it rather than quietly running unbounded.
|
|
if opts.rpcTimeout < 0 {
|
|
fmt.Fprintln(os.Stderr, "-rpc-timeout must not be negative; use 0 to disable the timeout")
|
|
os.Exit(1)
|
|
}
|
|
|
|
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, opts.rpcTimeout, opts.rpcMaxConcurrent)
|
|
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", "", "", opts.rpcTimeout, opts.rpcMaxConcurrent)
|
|
|
|
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")
|
|
}
|
|
}
|