diff --git a/config.go b/config.go index ab30eb5061..ae34927651 100644 --- a/config.go +++ b/config.go @@ -48,6 +48,7 @@ const ( defaultChainSubDirname = "chain" defaultGraphSubDirname = "graph" defaultTowerSubDirname = "watchtower" + defaultPoolDirname = "pool" defaultTLSCertFilename = "tls.cert" defaultTLSKeyFilename = "tls.key" defaultAdminMacFilename = "admin.macaroon" @@ -188,6 +189,7 @@ var ( defaultDataDir = filepath.Join(DefaultLndDir, defaultDataDirname) defaultLogDir = filepath.Join(DefaultLndDir, defaultLogDirname) + defaultPoolDir = filepath.Join(DefaultLndDir, defaultPoolDirname) defaultTowerDir = filepath.Join(defaultDataDir, defaultTowerSubDirname) defaultTLSCertPath = filepath.Join(DefaultLndDir, defaultTLSCertFilename) @@ -241,6 +243,7 @@ type Config struct { ReadMacPath string `long:"readonlymacaroonpath" description:"Path to write the read-only macaroon for lnd's RPC and REST services if it doesn't exist"` InvoiceMacPath string `long:"invoicemacaroonpath" description:"Path to the invoice-only macaroon for lnd's RPC and REST services if it doesn't exist"` LogDir string `long:"logdir" description:"Directory to log output."` + PoolDir string `long:"pooldir" description:"DIrectory to store sidecar ticket data."` MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"` MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB"` AcceptorTimeout time.Duration `long:"acceptortimeout" description:"Time after which an RPCAcceptor will time out and return false if it hasn't yet received a response"` @@ -368,6 +371,8 @@ type Config struct { DustThreshold uint64 `long:"dust-threshold" description:"Sets the dust sum threshold in satoshis for a channel after which dust HTLC's will be failed."` + SidecarAcceptor bool `long:"sidecar-acceptor" description:"If true, we run a sidecar acceptor alongside lnd"` + Invoices *lncfg.Invoices `group:"invoices" namespace:"invoices"` Routing *lncfg.Routing `group:"routing" namespace:"routing"` @@ -532,6 +537,7 @@ func DefaultConfig() Config { Watchtower: &lncfg.Watchtower{ TowerDir: defaultTowerDir, }, + PoolDir: defaultPoolDir, HealthChecks: &lncfg.HealthCheckConfig{ ChainCheck: &lncfg.CheckConfig{ Interval: defaultChainInterval, @@ -682,6 +688,7 @@ func ValidateConfig(cfg Config, usageMessage string, cfg.TLSCertPath = filepath.Join(lndDir, defaultTLSCertFilename) cfg.TLSKeyPath = filepath.Join(lndDir, defaultTLSKeyFilename) cfg.LogDir = filepath.Join(lndDir, defaultLogDirname) + cfg.PoolDir = filepath.Join(lndDir, defaultPoolDirname) // If the watchtower's directory is set to the default, i.e. the // user has not requested a different location, we'll move the @@ -690,6 +697,11 @@ func ValidateConfig(cfg Config, usageMessage string, cfg.Watchtower.TowerDir = filepath.Join(cfg.DataDir, defaultTowerSubDirname) } + + if cfg.PoolDir == defaultPoolDir { + cfg.PoolDir = + filepath.Join(cfg.DataDir, defaultPoolDirname) + } } funcName := "loadConfig" @@ -769,6 +781,7 @@ func ValidateConfig(cfg Config, usageMessage string, cfg.ReadMacPath = CleanAndExpandPath(cfg.ReadMacPath) cfg.InvoiceMacPath = CleanAndExpandPath(cfg.InvoiceMacPath) cfg.LogDir = CleanAndExpandPath(cfg.LogDir) + cfg.PoolDir = CleanAndExpandPath(cfg.PoolDir) cfg.BtcdMode.Dir = CleanAndExpandPath(cfg.BtcdMode.Dir) cfg.LtcdMode.Dir = CleanAndExpandPath(cfg.LtcdMode.Dir) cfg.BitcoindMode.Dir = CleanAndExpandPath(cfg.BitcoindMode.Dir) @@ -1301,6 +1314,10 @@ func ValidateConfig(cfg Config, usageMessage string, lncfg.NormalizeNetwork(cfg.ActiveNetParams.Name), ) + cfg.PoolDir = filepath.Join(cfg.PoolDir, + cfg.registeredChains.PrimaryChain().String(), + lncfg.NormalizeNetwork(cfg.ActiveNetParams.Name)) + // We need to make sure the default network directory exists for when we // try to create our default macaroons there. if err := makeDirectory(cfg.networkDir); err != nil { diff --git a/lnd.go b/lnd.go index c101ee3a01..86ab9ab970 100644 --- a/lnd.go +++ b/lnd.go @@ -73,10 +73,22 @@ const ( // // NOTE: This should only be called after the RPCListener has signaled it is // ready. -func AdminAuthOptions(cfg *Config, skipMacaroons bool) ([]grpc.DialOption, error) { - creds, err := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "") - if err != nil { - return nil, fmt.Errorf("unable to read TLS cert: %v", err) +func AdminAuthOptions(cfg *Config, skipMacaroons, insecure bool) ([]grpc.DialOption, error) { + var ( + creds credentials.TransportCredentials + err error + ) + + + if insecure { + creds = credentials.NewTLS(&tls.Config{ + InsecureSkipVerify: true, // nolint:gosec + }) + } else { + creds, err = credentials.NewClientTLSFromFile(cfg.TLSCertPath, "") + if err != nil { + return nil, fmt.Errorf("unable to read TLS cert: %v", err) + } } // Create a dial options array. @@ -658,6 +670,15 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg, ltndLog.Infof("Chain backend is fully synced (end_height=%v)!", bestHeight) + if cfg.SidecarAcceptor { + acceptor, err := StartSidecarAcceptor(cfg) + if err != nil { + ltndLog.Error(err) + return err + } + server.sidecarAcceptor = acceptor + } + // With all the relevant chains initialized, we can finally start the // server itself. if err := server.Start(); err != nil { diff --git a/rpcperms/interceptor.go b/rpcperms/interceptor.go index 97e752b175..c23aa56d0d 100644 --- a/rpcperms/interceptor.go +++ b/rpcperms/interceptor.go @@ -92,6 +92,10 @@ var ( // before we can check macaroons, so we whitelist it. "/lnrpc.State/SubscribeState": {}, "/lnrpc.State/GetState": {}, + + // Let the register sidecar endpoint be unauthenticated + // so we can register tickets for the user. + "/lnrpc.Lightning/RegisterSidecar": {}, } ) diff --git a/rpcserver.go b/rpcserver.go index 9b15348abc..83e451728a 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -29,6 +29,7 @@ import ( "github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/davecgh/go-spew/spew" proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/lightninglabs/pool/sidecar" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/chainreg" @@ -7402,3 +7403,51 @@ func (r *rpcServer) SubscribeCustomMessages(req *lnrpc.SubscribeCustomMessagesRe } } } + + +// RegisterSidecar is step 2/4 of the sidecar negotiation between the provider +// (the trader submitting the bid order) and the recipient (the trader receiving +// the sidecar channel). +// This step must be run by the recipient. The result is a sidecar ticket with +// the recipient's node information and channel funding multisig pubkey filled +// in. The ticket returned by this call will have the state "registered". +func (r *rpcServer) RegisterSidecar(ctx context.Context, + req *lnrpc.RegisterSidecarRequest) (*lnrpc.SidecarTicket, error) { + + if r.server.sidecarAcceptor == nil { + return nil, errors.New("Cannot register sidecar until sidecar"+ + " acceptor is set up. Wait until node is fully synced.") + } + + // Parse the ticket from its string encoded representation. + ticket, err := sidecar.DecodeString(req.Ticket) + if err != nil { + return nil, fmt.Errorf("error decoding ticket: %v", err) + } + + // The sidecar acceptor will add all required information and add the + // ticket to our DB. + registeredTicket, err := r.server.sidecarAcceptor.RegisterSidecar( + ctx, *ticket, + ) + if err != nil { + return nil, err + } + + // At this point, we'll now check if the ticket specifies that + // automated negotiation is to be sued, if so then we'll hand things + // off to the sidecar acceptor to finish the process. + if registeredTicket.Offer.Auto { + err := r.server.sidecarAcceptor.AutoAcceptSidecar(registeredTicket) + if err != nil { + return nil, fmt.Errorf("unable to start ticket auto "+ + "negotiation: %v", err) + } + } + + ticketStr, err := sidecar.EncodeToString(registeredTicket) + if err != nil { + return nil, err + } + return &lnrpc.SidecarTicket{Ticket: ticketStr}, nil +} diff --git a/sample-lnd.conf b/sample-lnd.conf index 5c777c6cb2..ed8f5466ef 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -435,6 +435,8 @@ ; intelligence services. ; color=#3399FF +; Set if we want to spin up a sidecar acceptor. +; sidecar-acceptor=true [Bitcoin] diff --git a/server.go b/server.go index 289d0176a9..5feb214280 100644 --- a/server.go +++ b/server.go @@ -25,6 +25,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/go-errors/errors" + "github.com/lightninglabs/pool/acceptor" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/brontide" @@ -283,6 +284,8 @@ type server struct { readPool *pool.Read + sidecarAcceptor *acceptor.SidecarAcceptor + // featureMgr dispatches feature vectors for various contexts within the // daemon. featureMgr *feature.Manager @@ -1771,6 +1774,21 @@ func (s *server) Start() error { } cleanup = cleanup.add(s.authGossiper.Stop) + if s.cfg.SidecarAcceptor { + if err := s.sidecarAcceptor.FundingManager.Start(); err != nil { + startErr = err + return + } + cleanup = cleanup.add(s.sidecarAcceptor.FundingManager.Stop) + + var testErrChan = make(chan error) + if err := s.sidecarAcceptor.Start(testErrChan); err != nil { + startErr = err + return + } + cleanup = cleanup.add(s.sidecarAcceptor.Stop) + } + if err := s.chanRouter.Start(); err != nil { startErr = err return @@ -2059,6 +2077,15 @@ func (s *server) Stop() error { } s.chanEventStore.Stop() s.missionControl.StopStoreTicker() + if s.cfg.SidecarAcceptor { + if err := s.sidecarAcceptor.FundingManager.Stop(); err != nil { + srvrLog.Warnf("Unable to stop funding manager: %v", err) + } + + if err := s.sidecarAcceptor.Stop(); err != nil { + srvrLog.Warnf("Unable to stop sidecarAcceptor: %v", err) + } + } // Disconnect from each active peers to ensure that // peerTerminationWatchers signal completion to each peer. diff --git a/start_sidecar.go b/start_sidecar.go new file mode 100644 index 0000000000..dac21b72cd --- /dev/null +++ b/start_sidecar.go @@ -0,0 +1,122 @@ +package lnd + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/pool/acceptor" + "github.com/lightninglabs/pool/auctioneer" + "github.com/lightninglabs/pool/clientdb" + "github.com/lightninglabs/pool/funding" + "github.com/lightninglabs/pool/order" + "github.com/lightningnetwork/lnd/lnrpc" + + "google.golang.org/grpc" +) + +func StartSidecarAcceptor(cfg *Config) (*acceptor.SidecarAcceptor, error) { + opts, err := AdminAuthOptions(cfg, false, true) + if err != nil { + return nil, err + } + + host := cfg.RPCListeners[0].String() + conn, err := grpc.Dial(host, opts...) + if err != nil { + return nil, fmt.Errorf("unable to connect to RPC server: %v", err) + } + + network := lndclient.Network(cfg.ActiveNetParams.Params.Name) + + ctxc, cancel := context.WithCancel(context.Background()) + defer cancel() + + lndServices, err := lndclient.NewLndServices(&lndclient.LndServicesConfig{ + LndAddress: host, + Network: network, + TLSPath: cfg.TLSCertPath, + CustomMacaroonPath: cfg.AdminMacPath, + BlockUntilChainSynced: false, + BlockUntilUnlocked: true, + CallerCtx: ctxc, + }) + if err != nil { + return nil, err + } + + db, err := clientdb.New(cfg.PoolDir, clientdb.DBFilename) + if err != nil { + return nil, err + } + + // Parse our lnd node's public key. + nodePubKey, err := btcec.ParsePubKey( + lndServices.NodePubkey[:], btcec.S256(), + ) + if err != nil { + return nil, fmt.Errorf("unable to parse node pubkey: %v", err) + } + + lnClient := lnrpc.NewLightningClient(conn) + + channelAcceptor := acceptor.NewChannelAcceptor(lndServices.Client) + fundingManager := funding.NewManager(&funding.ManagerConfig{ + DB: db, + WalletKit: lndServices.WalletKit, + LightningClient: lndServices.Client, + SignerClient: lndServices.Signer, + BaseClient: lnClient, + NodePubKey: nodePubKey, + BatchStepTimeout: order.DefaultBatchStepTimeout, + NewNodesOnly: false, + NotifyShimCreated: channelAcceptor.ShimRegistered, + }) + + var auctionServer string + // Use the default addresses for mainnet and testnet auction servers. + switch { + case cfg.Bitcoin.MainNet: + auctionServer = "pool.lightning.finance:12010" + case cfg.Bitcoin.TestNet3: + auctionServer = "test.pool.lightning.finance:12010" + default: + return nil, errors.New("no auction server address specified") + } + + clientCfg := &auctioneer.Config{ + ServerAddress: auctionServer, + ProxyAddress: "", + Insecure: false, + TLSPathServer: "", + DialOpts: make([]grpc.DialOption, 0), + Signer: lndServices.Signer, + MinBackoff: time.Millisecond * 100, + MaxBackoff: time.Minute, + BatchSource: db, + BatchCleaner: fundingManager, + GenUserAgent: func(ctx context.Context) string { + return acceptor.UserAgent(acceptor.InitiatorFromContext(ctx)) + }, + } + + acceptor := acceptor.NewSidecarAcceptor(&acceptor.SidecarAcceptorConfig{ + SidecarDB: db, + AcctDB: &acceptor.AccountStore{DB: db}, + BaseClient: lnClient, + Acceptor: channelAcceptor, + Signer: lndServices.Signer, + Wallet: lndServices.WalletKit, + NodePubKey: nodePubKey, + ClientCfg: *clientCfg, + FundingManager: fundingManager, + FetchSidecarBid: db.SidecarBidTemplate, + }) + + acceptor.FundingManager = fundingManager + + return acceptor, nil +}