From e0a9e48fb6cb1c5d7afad7f63402bd13e3f7e99a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 26 Jan 2024 18:48:19 -0800 Subject: [PATCH 01/35] peer: add new abstract message router In this commit, we add a new abstract message router. Over time, the goal is that this message router replaces the logic we currently have in the readHandler (the giant switch for each message). With this new abstraction, can reduce the responsibilities of the readHandler to *just* reading messages off the wire and handing them off to the msg router. The readHandler no longer needs to know *where* the messages should go, or how they should be dispatched. This will be used in tandem with the new `protofsm` module in an upcoming PR implementing the new rbf-coop close. --- peer/msg_router.go | 301 ++++++++++++++++++++++++++++++++++++++++ peer/msg_router_test.go | 146 +++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100644 peer/msg_router.go create mode 100644 peer/msg_router_test.go diff --git a/peer/msg_router.go b/peer/msg_router.go new file mode 100644 index 00000000000..8bf9b4bafdb --- /dev/null +++ b/peer/msg_router.go @@ -0,0 +1,301 @@ +package peer + +import ( + "fmt" + "maps" + "sync" + + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnwire" +) + +var ( + // ErrDuplicateEndpoint is returned when an endpoint is registered with + // a name that already exists. + ErrDuplicateEndpoint = fmt.Errorf("endpoint already registered") + + // ErrUnableToRouteMsg is returned when a message is unable to be + // routed to any endpoints. + ErrUnableToRouteMsg = fmt.Errorf("unable to route message") +) + +// EndPointName is the name of a given endpoint. This MUST be unique across all +// registered endpoints. +type EndPointName = string + +// MsgEndpoint is an interface that represents a message endpoint, or the +// sub-system that will handle processing an incoming wire message. +type MsgEndpoint interface { + // Name returns the name of this endpoint. This MUST be unique across + // all registered endpoints. + Name() EndPointName + + // CanHandle returns true if the target message can be routed to this + // endpoint. + CanHandle(msg lnwire.Message) bool + + // SendMessage handles the target message, and returns true if the + // message was able to be processed. + SendMessage(msg lnwire.Message) bool +} + +// MsgRouter is an interface that represents a message router, which is generic +// sub-system capable of routing any incoming wire message to a set of +// registered endpoints. +// +// TODO(roasbeef): move to diff sub-system? +type MsgRouter interface { + // RegisterEndpoint registers a new endpoint with the router. If a + // duplicate endpoint exists, an error is returned. + RegisterEndpoint(MsgEndpoint) error + + // UnregisterEndpoint unregisters the target endpoint from the router. + UnregisterEndpoint(EndPointName) error + + // RouteMsg attempts to route the target message to a registered + // endpoint. If ANY endpoint could handle the message, then nil is + // returned. Otherwise, ErrUnableToRouteMsg is returned. + RouteMsg(lnwire.Message) error + + // Start starts the peer message router. + Start() + + // Stop stops the peer message router. + Stop() +} + +// queryMsg is a message sent into the main event loop to query or modify the +// internal state. +type queryMsg[Q any, R any] struct { + query Q + + respChan chan fn.Either[R, error] +} + +// sendQuery sends a query to the main event loop, and returns the response. +func sendQuery[Q any, R any](sendChan chan queryMsg[Q, R], queryArg Q, + quit chan struct{}) fn.Either[R, error] { + + query := queryMsg[Q, R]{ + query: queryArg, + respChan: make(chan fn.Either[R, error], 1), + } + + if !fn.SendOrQuit(sendChan, query, quit) { + return fn.NewRight[R](fmt.Errorf("router shutting down")) + } + + resp, err := fn.RecvResp(query.respChan, nil, quit) + if err != nil { + return fn.NewRight[R](err) + } + + return resp +} + +// sendQueryErr is a helper function based on sendQuery that can be used when +// the query only needs an error response. +func sendQueryErr[Q any](sendChan chan queryMsg[Q, error], queryArg Q, + quitChan chan struct{}) error { + + var err error + resp := sendQuery(sendChan, queryArg, quitChan) + resp.WhenRight(func(e error) { + err = e + }) + resp.WhenLeft(func(e error) { + err = e + }) + + return err +} + +// EndpointsMap is a map of all registered endpoints. +type EndpointsMap map[EndPointName]MsgEndpoint + +// MultiMsgRouter is a type of message router that is capable of routing new +// incoming messages, permitting a message to be routed to multiple registered +// endpoints. +type MultiMsgRouter struct { + startOnce sync.Once + stopOnce sync.Once + + // registerChan is the channel that all new endpoints will be sent to. + registerChan chan queryMsg[MsgEndpoint, error] + + // unregisterChan is the channel that all endpoints that are to be + // removed are sent to. + unregisterChan chan queryMsg[EndPointName, error] + + // msgChan is the channel that all messages will be sent to for + // processing. + msgChan chan queryMsg[lnwire.Message, error] + + // endpointsQueries is a channel that all queries to the endpoints map + // will be sent to. + endpointQueries chan queryMsg[MsgEndpoint, EndpointsMap] + + wg sync.WaitGroup + quit chan struct{} +} + +// NewMultiMsgRouter creates a new instance of a peer message router. +func NewMultiMsgRouter() *MultiMsgRouter { + return &MultiMsgRouter{ + registerChan: make(chan queryMsg[MsgEndpoint, error]), + unregisterChan: make(chan queryMsg[EndPointName, error]), + msgChan: make(chan queryMsg[lnwire.Message, error]), + endpointQueries: make(chan queryMsg[MsgEndpoint, EndpointsMap]), + quit: make(chan struct{}), + } +} + +// Start starts the peer message router. +func (p *MultiMsgRouter) Start() { + peerLog.Infof("Starting MsgRouter") + + p.startOnce.Do(func() { + p.wg.Add(1) + go p.msgRouter() + }) +} + +// Stop stops the peer message router. +func (p *MultiMsgRouter) Stop() { + peerLog.Infof("Stopping MsgRouter") + + p.stopOnce.Do(func() { + close(p.quit) + p.wg.Wait() + }) +} + +// RegisterEndpoint registers a new endpoint with the router. If a duplicate +// endpoint exists, an error is returned. +func (p *MultiMsgRouter) RegisterEndpoint(endpoint MsgEndpoint) error { + return sendQueryErr(p.registerChan, endpoint, p.quit) +} + +// UnregisterEndpoint unregisters the target endpoint from the router. +func (p *MultiMsgRouter) UnregisterEndpoint(name EndPointName) error { + return sendQueryErr(p.unregisterChan, name, p.quit) +} + +// RouteMsg attempts to route the target message to a registered endpoint. If +// ANY endpoint could handle the message, then true is +// returned. +func (p *MultiMsgRouter) RouteMsg(msg lnwire.Message) error { + return sendQueryErr(p.msgChan, msg, p.quit) +} + +// Endpoints returns a list of all registered endpoints. +func (p *MultiMsgRouter) Endpoints() EndpointsMap { + resp := sendQuery(p.endpointQueries, nil, p.quit) + + var endpoints EndpointsMap + resp.WhenLeft(func(e EndpointsMap) { + endpoints = e + }) + + return endpoints +} + +// msgRouter is the main goroutine that handles all incoming messages. +func (p *MultiMsgRouter) msgRouter() { + defer p.wg.Done() + + // endpoints is a map of all registered endpoints. + endpoints := make(map[EndPointName]MsgEndpoint) + + for { + select { + // A new endpoint was just sent in, so we'll add it to our set + // of registered endpoints. + case newEndpointMsg := <-p.registerChan: + endpoint := newEndpointMsg.query + + peerLog.Infof("MsgRouter: registering new "+ + "MsgEndpoint(%s)", endpoint.Name()) + + // If this endpoint already exists, then we'll return + // an error as we require unique names. + if _, ok := endpoints[endpoint.Name()]; ok { + peerLog.Errorf("MsgRouter: rejecting "+ + "duplicate endpoint: %v", + endpoint.Name()) + + newEndpointMsg.respChan <- fn.NewRight[error]( + ErrDuplicateEndpoint, + ) + + continue + } + + endpoints[endpoint.Name()] = endpoint + + // TODO(roasbeef): put in method? + newEndpointMsg.respChan <- fn.NewRight[error, error]( + nil, + ) + + // A request to unregister an endpoint was just sent in, so + // we'll attempt to remove it. + case endpointName := <-p.unregisterChan: + delete(endpoints, endpointName.query) + + peerLog.Infof("MsgRouter: unregistering "+ + "MsgEndpoint(%s)", endpointName.query) + + endpointName.respChan <- fn.NewRight[error, error]( + nil, + ) + + // A new message was just sent in. We'll attempt to route it to + // all the endpoints that can handle it. + case msgQuery := <-p.msgChan: + msg := msgQuery.query + + // Loop through all the endpoints and send the message + // to those that can handle it the message. + var couldSend bool + for _, endpoint := range endpoints { + if endpoint.CanHandle(msg) { + peerLog.Tracef("MsgRouter: sending "+ + "msg %T to endpoint %s", msg, + endpoint.Name()) + + sent := endpoint.SendMessage(msg) + couldSend = couldSend || sent + } + } + + var err error + if !couldSend { + peerLog.Tracef("MsgRouter: unable to route "+ + "msg %T", msg) + + err = ErrUnableToRouteMsg + } + + msgQuery.respChan <- fn.NewRight[error](err) + + // A query for the endpoint state just came in, we'll send back + // a copy of our current state. + case endpointQuery := <-p.endpointQueries: + endpointsCopy := make(EndpointsMap, len(endpoints)) + maps.Copy(endpointsCopy, endpoints) + + //nolint:lll + endpointQuery.respChan <- fn.NewLeft[EndpointsMap, error]( + endpointsCopy, + ) + + case <-p.quit: + return + } + } +} + +// A compile time check to ensure MultiMsgRouter implements the MsgRouter +// interface. +var _ MsgRouter = (*MultiMsgRouter)(nil) diff --git a/peer/msg_router_test.go b/peer/msg_router_test.go new file mode 100644 index 00000000000..ee52968eb53 --- /dev/null +++ b/peer/msg_router_test.go @@ -0,0 +1,146 @@ +package peer + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type mockEndpoint struct { + mock.Mock +} + +func (m *mockEndpoint) Name() string { + args := m.Called() + + return args.String(0) +} + +func (m *mockEndpoint) CanHandle(msg lnwire.Message) bool { + args := m.Called(msg) + + return args.Bool(0) +} + +func (m *mockEndpoint) SendMessage(msg lnwire.Message) bool { + args := m.Called(msg) + + return args.Bool(0) +} + +// TestMessageRouterOperation tests the basic operation of the message router: +// add new endpoints, route to them, remove, them, etc. +func TestMessageRouterOperation(t *testing.T) { + msgRouter := NewMultiMsgRouter() + msgRouter.Start() + defer msgRouter.Stop() + + openChanMsg := &lnwire.OpenChannel{} + commitSigMsg := &lnwire.CommitSig{} + + errorMsg := &lnwire.Error{} + + // For this test, we'll have two endpoints, each with distinct names. + // One endpoint will only handle OpenChannel, while the other will + // handle the CommitSig message. + fundingEndpoint := &mockEndpoint{} + fundingEndpointName := "funding" + fundingEndpoint.On("Name").Return(fundingEndpointName) + fundingEndpoint.On("CanHandle", openChanMsg).Return(true) + fundingEndpoint.On("CanHandle", errorMsg).Return(false) + fundingEndpoint.On("CanHandle", commitSigMsg).Return(false) + fundingEndpoint.On("SendMessage", openChanMsg).Return(true) + + commitEndpoint := &mockEndpoint{} + commitEndpointName := "commit" + commitEndpoint.On("Name").Return(commitEndpointName) + commitEndpoint.On("CanHandle", commitSigMsg).Return(true) + commitEndpoint.On("CanHandle", openChanMsg).Return(false) + commitEndpoint.On("CanHandle", errorMsg).Return(false) + commitEndpoint.On("SendMessage", commitSigMsg).Return(true) + + t.Run("add endpoints", func(t *testing.T) { + // First, we'll add the funding endpoint to the router. + require.NoError(t, msgRouter.RegisterEndpoint(fundingEndpoint)) + + // There should be a single endpoint registered. + require.Len(t, msgRouter.Endpoints(), 1) + + // The name of the registered endpoint should be "funding". + require.Equal( + t, "funding", + msgRouter.Endpoints()[fundingEndpointName].Name(), + ) + }) + + t.Run("duplicate endpoint reject", func(t *testing.T) { + // Next, we'll attempt to add the funding endpoint again. This + // should return an ErrDuplicateEndpoint error. + require.ErrorIs( + t, msgRouter.RegisterEndpoint(fundingEndpoint), + ErrDuplicateEndpoint, + ) + }) + + t.Run("route to endpoint", func(t *testing.T) { + // Next, we'll add our other endpoint, then attempt to route a + // message. + require.NoError(t, msgRouter.RegisterEndpoint(commitEndpoint)) + + // If we try to route a message none of the endpoints know of, + // we should get an error. + require.ErrorIs( + t, msgRouter.RouteMsg(errorMsg), ErrUnableToRouteMsg, + ) + + fundingEndpoint.AssertCalled(t, "CanHandle", errorMsg) + commitEndpoint.AssertCalled(t, "CanHandle", errorMsg) + + // Next, we'll route the open channel message. Only the + // fundingEndpoint should be used. + require.NoError(t, msgRouter.RouteMsg(openChanMsg)) + + fundingEndpoint.AssertCalled(t, "CanHandle", openChanMsg) + commitEndpoint.AssertCalled(t, "CanHandle", openChanMsg) + + fundingEndpoint.AssertCalled(t, "SendMessage", openChanMsg) + commitEndpoint.AssertNotCalled(t, "SendMessage", openChanMsg) + + // We'll do the same for the commit sig message. + require.NoError(t, msgRouter.RouteMsg(commitSigMsg)) + + fundingEndpoint.AssertCalled(t, "CanHandle", commitSigMsg) + commitEndpoint.AssertCalled(t, "CanHandle", commitSigMsg) + + commitEndpoint.AssertCalled(t, "SendMessage", commitSigMsg) + fundingEndpoint.AssertNotCalled(t, "SendMessage", commitSigMsg) + }) + + t.Run("remove endpoints", func(t *testing.T) { + // Finally, we'll remove both endpoints. + require.NoError( + t, msgRouter.UnregisterEndpoint(fundingEndpointName), + ) + require.NoError( + t, msgRouter.UnregisterEndpoint(commitEndpointName), + ) + + // There should be no endpoints registered. + require.Len(t, msgRouter.Endpoints(), 0) + + // Trying to route a message should fail. + require.ErrorIs( + t, msgRouter.RouteMsg(openChanMsg), + ErrUnableToRouteMsg, + ) + require.ErrorIs( + t, msgRouter.RouteMsg(commitSigMsg), + ErrUnableToRouteMsg, + ) + }) + + commitEndpoint.AssertExpectations(t) + fundingEndpoint.AssertExpectations(t) +} From c7a1324832262c3cb497638f9e7bdc746bd1ecad Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 30 Jan 2024 18:00:11 -0800 Subject: [PATCH 02/35] peer: update readHandler to dispatch to msgRouter if set Over time with this, we should be able to significantly reduce the size of the peer.Brontide struct as we only need all those deps as the peer needs to recognize and handle each incoming wire message itself. --- peer/brontide.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/peer/brontide.go b/peer/brontide.go index b9a9f68ca15..d12c3ce3849 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -493,6 +493,10 @@ type Brontide struct { // potentially holding lots of un-consumed events. channelEventClient *subscribe.Client + // msgRouter is an instance of the MsgRouter which is used to send off + // new wire messages for handing. + msgRouter fn.Option[MsgRouter] + startReady chan struct{} quit chan struct{} wg sync.WaitGroup @@ -530,6 +534,7 @@ func NewBrontide(cfg Config) *Brontide { startReady: make(chan struct{}), quit: make(chan struct{}), log: build.NewPrefixLog(logPrefix, peerLog), + msgRouter: fn.Some[MsgRouter](NewMultiMsgRouter()), } var ( @@ -1708,6 +1713,19 @@ out: } } + // If a message router is active, then we'll try to have it + // handle this message. If it can, then we're able to skip the + // rest of the message handling logic. + ok := fn.MapOptionZ(p.msgRouter, func(r MsgRouter) error { + return r.RouteMsg(nextMsg) + }) + + // No error occurred, and the message was handled by the + // router. + if ok == nil { + continue + } + var ( targetChan lnwire.ChannelID isLinkUpdate bool From b151aa7d0b73665cd52f809050258184ddc431c1 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 5 Mar 2024 00:05:45 -0600 Subject: [PATCH 03/35] peer: update Start/Stop w/ awareness of msg router --- peer/brontide.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/peer/brontide.go b/peer/brontide.go index d12c3ce3849..230f3cb4144 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -709,6 +709,12 @@ func (p *Brontide) Start() error { return err } + // Register the message router now as we may need to register some + // endpoints while loading the channels below. + p.msgRouter.WhenSome(func(router MsgRouter) { + router.Start() + }) + msgs, err := p.loadActiveChannels(activeChans) if err != nil { return fmt.Errorf("unable to load channels: %w", err) @@ -1274,6 +1280,10 @@ func (p *Brontide) Disconnect(reason error) { p.log.Errorf("couldn't stop pingManager during disconnect: %v", err) } + + p.msgRouter.WhenSome(func(router MsgRouter) { + router.Stop() + }) } // String returns the string representation of this peer. From 8272566288f14fd3f543007a6079e3152f842838 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 16:13:30 -0700 Subject: [PATCH 04/35] multi: make MsgRouter available in the ImplementationCfg With this commit, we allow the `MsgRouter` to be available in the `ImplementationCfg`. With this, programs outside of lnd itself are able to now hook into the message processing flow to direct handle custom messages, and even normal wire messages. --- config_builder.go | 5 +++++ peer/brontide.go | 15 ++++++++++++++- server.go | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/config_builder.go b/config_builder.go index 7af32738243..e5839208dad 100644 --- a/config_builder.go +++ b/config_builder.go @@ -44,6 +44,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwallet/rpcwallet" "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/peer" "github.com/lightningnetwork/lnd/rpcperms" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/sqldb" @@ -157,6 +158,10 @@ type AuxComponents struct { // AuxLeafStore is an optional data source that can be used by custom // channels to fetch+store various data. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // MsgRouter is an optional message router that if set will be used in + // place of a new balnk default message router. + MsgRouter fn.Option[peer.MsgRouter] } // DefaultWalletImpl is the default implementation of our normal, btcwallet diff --git a/peer/brontide.go b/peer/brontide.go index 230f3cb4144..b86a2db6204 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -374,6 +374,11 @@ type Config struct { // invalid. DisallowRouteBlinding bool + // MsgRouter is an optional instance of the main message router that + // the peer will use. If None, then a new default version will be used + // in place. + MsgRouter fn.Option[MsgRouter] + // Quit is the server's quit channel. If this is closed, we halt operation. Quit chan struct{} } @@ -512,6 +517,14 @@ var _ lnpeer.Peer = (*Brontide)(nil) func NewBrontide(cfg Config) *Brontide { logPrefix := fmt.Sprintf("Peer(%x):", cfg.PubKeyBytes) + // We'll either use the msg router instance passed in, or create a new + // blank instance. + // + // TODO(roasbeef): extend w/ source peer info? + msgRouter := cfg.MsgRouter.Alt( + fn.Some[MsgRouter](NewMultiMsgRouter()), + ) + p := &Brontide{ cfg: cfg, activeSignal: make(chan struct{}), @@ -534,7 +547,7 @@ func NewBrontide(cfg Config) *Brontide { startReady: make(chan struct{}), quit: make(chan struct{}), log: build.NewPrefixLog(logPrefix, peerLog), - msgRouter: fn.Some[MsgRouter](NewMultiMsgRouter()), + msgRouter: msgRouter, } var ( diff --git a/server.go b/server.go index 650cdbced40..e53a7bb07ee 100644 --- a/server.go +++ b/server.go @@ -3912,6 +3912,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, DisallowRouteBlinding: s.cfg.ProtocolOptions.NoRouteBlinding(), Quit: s.quit, AuxLeafStore: s.implCfg.AuxLeafStore, + MsgRouter: s.implCfg.MsgRouter, } copy(pCfg.PubKeyBytes[:], peerAddr.IdentityKey.SerializeCompressed()) From ffedd5f36b1c73d07b28b7ecd8357d8ef7628c52 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 16:24:23 -0700 Subject: [PATCH 05/35] peer+protofsm: move MsgRouter to new protofsm package Without this, any other sub-system that wants to use the interface may run into an import cycle. --- config_builder.go | 4 +-- peer/brontide.go | 13 ++++---- protofsm/log.go | 45 +++++++++++++++++++++++++++ {peer => protofsm}/msg_router.go | 16 +++++----- {peer => protofsm}/msg_router_test.go | 2 +- 5 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 protofsm/log.go rename {peer => protofsm}/msg_router.go (96%) rename {peer => protofsm}/msg_router_test.go (99%) diff --git a/config_builder.go b/config_builder.go index e5839208dad..a1658fd67d2 100644 --- a/config_builder.go +++ b/config_builder.go @@ -44,7 +44,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwallet/rpcwallet" "github.com/lightningnetwork/lnd/macaroons" - "github.com/lightningnetwork/lnd/peer" + "github.com/lightningnetwork/lnd/protofsm" "github.com/lightningnetwork/lnd/rpcperms" "github.com/lightningnetwork/lnd/signal" "github.com/lightningnetwork/lnd/sqldb" @@ -161,7 +161,7 @@ type AuxComponents struct { // MsgRouter is an optional message router that if set will be used in // place of a new balnk default message router. - MsgRouter fn.Option[peer.MsgRouter] + MsgRouter fn.Option[protofsm.MsgRouter] } // DefaultWalletImpl is the default implementation of our normal, btcwallet diff --git a/peer/brontide.go b/peer/brontide.go index b86a2db6204..779206038e2 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -42,6 +42,7 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/netann" "github.com/lightningnetwork/lnd/pool" + "github.com/lightningnetwork/lnd/protofsm" "github.com/lightningnetwork/lnd/queue" "github.com/lightningnetwork/lnd/subscribe" "github.com/lightningnetwork/lnd/ticker" @@ -377,7 +378,7 @@ type Config struct { // MsgRouter is an optional instance of the main message router that // the peer will use. If None, then a new default version will be used // in place. - MsgRouter fn.Option[MsgRouter] + MsgRouter fn.Option[protofsm.MsgRouter] // Quit is the server's quit channel. If this is closed, we halt operation. Quit chan struct{} @@ -500,7 +501,7 @@ type Brontide struct { // msgRouter is an instance of the MsgRouter which is used to send off // new wire messages for handing. - msgRouter fn.Option[MsgRouter] + msgRouter fn.Option[protofsm.MsgRouter] startReady chan struct{} quit chan struct{} @@ -522,7 +523,7 @@ func NewBrontide(cfg Config) *Brontide { // // TODO(roasbeef): extend w/ source peer info? msgRouter := cfg.MsgRouter.Alt( - fn.Some[MsgRouter](NewMultiMsgRouter()), + fn.Some[protofsm.MsgRouter](protofsm.NewMultiMsgRouter()), ) p := &Brontide{ @@ -724,7 +725,7 @@ func (p *Brontide) Start() error { // Register the message router now as we may need to register some // endpoints while loading the channels below. - p.msgRouter.WhenSome(func(router MsgRouter) { + p.msgRouter.WhenSome(func(router protofsm.MsgRouter) { router.Start() }) @@ -1294,7 +1295,7 @@ func (p *Brontide) Disconnect(reason error) { err) } - p.msgRouter.WhenSome(func(router MsgRouter) { + p.msgRouter.WhenSome(func(router protofsm.MsgRouter) { router.Stop() }) } @@ -1739,7 +1740,7 @@ out: // If a message router is active, then we'll try to have it // handle this message. If it can, then we're able to skip the // rest of the message handling logic. - ok := fn.MapOptionZ(p.msgRouter, func(r MsgRouter) error { + ok := fn.MapOptionZ(p.msgRouter, func(r protofsm.MsgRouter) error { return r.RouteMsg(nextMsg) }) diff --git a/protofsm/log.go b/protofsm/log.go new file mode 100644 index 00000000000..368abfab20b --- /dev/null +++ b/protofsm/log.go @@ -0,0 +1,45 @@ +package protofsm + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger("PFSM", nil)) +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} + +// logClosure is used to provide a closure over expensive logging operations +// so they aren't performed when the logging level doesn't warrant it. +type logClosure func() string + +// String invokes the underlying function and returns the result. +func (c logClosure) String() string { + return c() +} + +// newLogClosure returns a new closure over a function that returns a string +// which itself provides a Stringer interface so that it can be used with the +// logging system. +func newLogClosure(c func() string) logClosure { + return logClosure(c) +} diff --git a/peer/msg_router.go b/protofsm/msg_router.go similarity index 96% rename from peer/msg_router.go rename to protofsm/msg_router.go index 8bf9b4bafdb..176cda5b475 100644 --- a/peer/msg_router.go +++ b/protofsm/msg_router.go @@ -1,4 +1,4 @@ -package peer +package protofsm import ( "fmt" @@ -152,7 +152,7 @@ func NewMultiMsgRouter() *MultiMsgRouter { // Start starts the peer message router. func (p *MultiMsgRouter) Start() { - peerLog.Infof("Starting MsgRouter") + log.Infof("Starting MsgRouter") p.startOnce.Do(func() { p.wg.Add(1) @@ -162,7 +162,7 @@ func (p *MultiMsgRouter) Start() { // Stop stops the peer message router. func (p *MultiMsgRouter) Stop() { - peerLog.Infof("Stopping MsgRouter") + log.Infof("Stopping MsgRouter") p.stopOnce.Do(func() { close(p.quit) @@ -214,13 +214,13 @@ func (p *MultiMsgRouter) msgRouter() { case newEndpointMsg := <-p.registerChan: endpoint := newEndpointMsg.query - peerLog.Infof("MsgRouter: registering new "+ + log.Infof("MsgRouter: registering new "+ "MsgEndpoint(%s)", endpoint.Name()) // If this endpoint already exists, then we'll return // an error as we require unique names. if _, ok := endpoints[endpoint.Name()]; ok { - peerLog.Errorf("MsgRouter: rejecting "+ + log.Errorf("MsgRouter: rejecting "+ "duplicate endpoint: %v", endpoint.Name()) @@ -243,7 +243,7 @@ func (p *MultiMsgRouter) msgRouter() { case endpointName := <-p.unregisterChan: delete(endpoints, endpointName.query) - peerLog.Infof("MsgRouter: unregistering "+ + log.Infof("MsgRouter: unregistering "+ "MsgEndpoint(%s)", endpointName.query) endpointName.respChan <- fn.NewRight[error, error]( @@ -260,7 +260,7 @@ func (p *MultiMsgRouter) msgRouter() { var couldSend bool for _, endpoint := range endpoints { if endpoint.CanHandle(msg) { - peerLog.Tracef("MsgRouter: sending "+ + log.Tracef("MsgRouter: sending "+ "msg %T to endpoint %s", msg, endpoint.Name()) @@ -271,7 +271,7 @@ func (p *MultiMsgRouter) msgRouter() { var err error if !couldSend { - peerLog.Tracef("MsgRouter: unable to route "+ + log.Tracef("MsgRouter: unable to route "+ "msg %T", msg) err = ErrUnableToRouteMsg diff --git a/peer/msg_router_test.go b/protofsm/msg_router_test.go similarity index 99% rename from peer/msg_router_test.go rename to protofsm/msg_router_test.go index ee52968eb53..b5e56e1e371 100644 --- a/peer/msg_router_test.go +++ b/protofsm/msg_router_test.go @@ -1,4 +1,4 @@ -package peer +package protofsm import ( "testing" From ed1c489f7c01afe1fbc97f6b1737b2b90391ea96 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 16:41:16 -0700 Subject: [PATCH 06/35] funding: add new type alias for PendingChanID = [32]byte This'll be useful for new interface definitions that use the contents of the package. --- funding/manager.go | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/funding/manager.go b/funding/manager.go index 4a9c0122d61..b47d0989334 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -288,7 +288,7 @@ type InitFundingMsg struct { // PendingChanID is not all zeroes (the default value), then this will // be the pending channel ID used for the funding flow within the wire // protocol. - PendingChanID [32]byte + PendingChanID PendingChanID // ChannelType allows the caller to use an explicit channel type for the // funding negotiation. This type will only be observed if BOTH sides @@ -318,7 +318,7 @@ type fundingMsg struct { // pendingChannels is a map instantiated per-peer which tracks all active // pending single funded channels indexed by their pending channel identifier, // which is a set of 32-bytes generated via a CSPRNG. -type pendingChannels map[[32]byte]*reservationWithCtx +type pendingChannels map[PendingChanID]*reservationWithCtx // serializedPubKey is used within the FundingManager's activeReservations list // to identify the nodes with which the FundingManager is actively working to @@ -590,7 +590,7 @@ type Manager struct { // required as mid funding flow, we switch to referencing the channel // by its full channel ID once the commitment transactions have been // signed by both parties. - signedReservations map[lnwire.ChannelID][32]byte + signedReservations map[lnwire.ChannelID]PendingChanID // resMtx guards both of the maps above to ensure that all access is // goroutine safe. @@ -797,9 +797,13 @@ func (f *Manager) rebroadcastFundingTx(c *channeldb.OpenChannel) { } } +// PendingChanID is a type that represents a pending channel ID. This might be +// selected by the caller, but if not, will be automatically selected. +type PendingChanID = [32]byte + // nextPendingChanID returns the next free pending channel ID to be used to // identify a particular future channel funding workflow. -func (f *Manager) nextPendingChanID() [32]byte { +func (f *Manager) nextPendingChanID() PendingChanID { // Obtain a fresh nonce. We do this by encoding the current nonce // counter, then incrementing it by one. f.nonceMtx.Lock() @@ -811,7 +815,7 @@ func (f *Manager) nextPendingChanID() [32]byte { // We'll generate the next pending channelID by "encrypting" 32-bytes // of zeroes which'll extract 32 random bytes from our stream cipher. var ( - nextChanID [32]byte + nextChanID PendingChanID zeroes [32]byte ) salsa20.XORKeyStream(nextChanID[:], zeroes[:], nonce[:], &f.chanIDKey) @@ -1044,7 +1048,8 @@ func (f *Manager) reservationCoordinator() { // // NOTE: This MUST be run as a goroutine. func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel, - pendingChanID [32]byte, updateChan chan<- *lnrpc.OpenStatusUpdate) { + pendingChanID PendingChanID, + updateChan chan<- *lnrpc.OpenStatusUpdate) { defer f.wg.Done() @@ -1119,7 +1124,7 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel, // updateChan can be set non-nil to get OpenStatusUpdates. func (f *Manager) stateStep(channel *channeldb.OpenChannel, lnChannel *lnwallet.LightningChannel, - shortChanID *lnwire.ShortChannelID, pendingChanID [32]byte, + shortChanID *lnwire.ShortChannelID, pendingChanID PendingChanID, channelState channelOpeningState, updateChan chan<- *lnrpc.OpenStatusUpdate) error { @@ -1243,7 +1248,7 @@ func (f *Manager) stateStep(channel *channeldb.OpenChannel, // advancePendingChannelState waits for a pending channel's funding tx to // confirm, and marks it open in the database when that happens. func (f *Manager) advancePendingChannelState( - channel *channeldb.OpenChannel, pendingChanID [32]byte) error { + channel *channeldb.OpenChannel, pendingChanID PendingChanID) error { if channel.IsZeroConf() { // Persist the alias to the alias database. @@ -2744,7 +2749,7 @@ type confirmedChannel struct { // channel as closed. The error is only returned for the responder of the // channel flow. func (f *Manager) fundingTimeout(c *channeldb.OpenChannel, - pendingID [32]byte) error { + pendingID PendingChanID) error { // We'll get a timeout if the number of blocks mined since the channel // was initiated reaches MaxWaitNumBlocksFundingConf and we are not the @@ -3607,7 +3612,7 @@ func (f *Manager) annAfterSixConfs(completeChan *channeldb.OpenChannel, // a zero-conf channel. This will wait for the real confirmation, add the // confirmed SCID to the router graph, and then announce after six confs. func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel, - pendingID [32]byte) error { + pendingID PendingChanID) error { // First we'll check whether the channel is confirmed on-chain. If it // is already confirmed, the chainntnfs subsystem will return with the @@ -3975,7 +3980,7 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen // channel is now active, thus we change its state to `addedToRouterGraph` to // let the channel start handling routing. func (f *Manager) handleChannelReadyReceived(channel *channeldb.OpenChannel, - scid *lnwire.ShortChannelID, pendingChanID [32]byte, + scid *lnwire.ShortChannelID, pendingChanID PendingChanID, updateChan chan<- *lnrpc.OpenStatusUpdate) error { chanID := lnwire.NewChanIDFromOutPoint(channel.FundingOutpoint) @@ -4499,7 +4504,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { // If the caller specified their own channel ID, then we'll use that. // Otherwise we'll generate a fresh one as normal. This will be used // to track this reservation throughout its lifetime. - var chanID [32]byte + var chanID PendingChanID if msg.PendingChanID == zeroID { chanID = f.nextPendingChanID() } else { @@ -4904,7 +4909,8 @@ func (f *Manager) pruneZombieReservations() { // cancelReservationCtx does all needed work in order to securely cancel the // reservation. func (f *Manager) cancelReservationCtx(peerKey *btcec.PublicKey, - pendingChanID [32]byte, byRemote bool) (*reservationWithCtx, error) { + pendingChanID PendingChanID, + byRemote bool) (*reservationWithCtx, error) { log.Infof("Cancelling funding reservation for node_key=%x, "+ "chan_id=%x", peerKey.SerializeCompressed(), pendingChanID[:]) @@ -4952,7 +4958,7 @@ func (f *Manager) cancelReservationCtx(peerKey *btcec.PublicKey, // deleteReservationCtx deletes the reservation uniquely identified by the // target public key of the peer, and the specified pending channel ID. func (f *Manager) deleteReservationCtx(peerKey *btcec.PublicKey, - pendingChanID [32]byte) { + pendingChanID PendingChanID) { peerIDKey := newSerializedKey(peerKey) f.resMtx.Lock() @@ -4975,7 +4981,7 @@ func (f *Manager) deleteReservationCtx(peerKey *btcec.PublicKey, // getReservationCtx returns the reservation context for a particular pending // channel ID for a target peer. func (f *Manager) getReservationCtx(peerKey *btcec.PublicKey, - pendingChanID [32]byte) (*reservationWithCtx, error) { + pendingChanID PendingChanID) (*reservationWithCtx, error) { peerIDKey := newSerializedKey(peerKey) f.resMtx.RLock() @@ -4995,7 +5001,7 @@ func (f *Manager) getReservationCtx(peerKey *btcec.PublicKey, // of being funded. After the funding transaction has been confirmed, the // channel will receive a new, permanent channel ID, and will no longer be // considered pending. -func (f *Manager) IsPendingChannel(pendingChanID [32]byte, +func (f *Manager) IsPendingChannel(pendingChanID PendingChanID, peer lnpeer.Peer) bool { peerIDKey := newSerializedKey(peer.IdentityKey()) From b7aae1dc46db8a3cb56b257105b8ced9f899febe Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 16:43:53 -0700 Subject: [PATCH 07/35] fundign: use atomic.Uint64 for chanIDNonce This lets us get rid of the mutex usage there. We also shift the algo slightly to increment by 1, then use that as the next value, which plays nicer with the atomics. --- funding/manager.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/funding/manager.go b/funding/manager.go index b47d0989334..e4b55d6435d 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "sync" + "sync/atomic" "time" "github.com/btcsuite/btcd/blockchain" @@ -565,10 +566,11 @@ type Manager struct { // temporary channel ID's. chanIDKey [32]byte + nonceMtx sync.RWMutex + // chanIDNonce is a nonce that's incremented for each new funding // reservation created. - nonceMtx sync.RWMutex - chanIDNonce uint64 + chanIDNonce atomic.Uint64 // pendingMusigNonces is used to store the musig2 nonce we generate to // send funding locked until we receive a funding locked message from @@ -806,11 +808,10 @@ type PendingChanID = [32]byte func (f *Manager) nextPendingChanID() PendingChanID { // Obtain a fresh nonce. We do this by encoding the current nonce // counter, then incrementing it by one. - f.nonceMtx.Lock() - var nonce [8]byte - binary.LittleEndian.PutUint64(nonce[:], f.chanIDNonce) - f.chanIDNonce++ - f.nonceMtx.Unlock() + nextNonce := f.chanIDNonce.Add(1) + + var nonceBytes [8]byte + binary.LittleEndian.PutUint64(nonceBytes[:], nextNonce) // We'll generate the next pending channelID by "encrypting" 32-bytes // of zeroes which'll extract 32 random bytes from our stream cipher. @@ -818,7 +819,9 @@ func (f *Manager) nextPendingChanID() PendingChanID { nextChanID PendingChanID zeroes [32]byte ) - salsa20.XORKeyStream(nextChanID[:], zeroes[:], nonce[:], &f.chanIDKey) + salsa20.XORKeyStream( + nextChanID[:], zeroes[:], nonceBytes[:], &f.chanIDKey, + ) return nextChanID } From ee9e679540522485dfe8fce06bcc94caf923262e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 17:25:09 -0700 Subject: [PATCH 08/35] lnwallet: add new AuxFundingDesc struct This struct will house all the information we'll need to do a class of custom channels that relies primarily on adding additional items to the tapscript root of the HTLC/commitment/funding outputs. --- lnwallet/wallet.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index c579363542c..d9482437d25 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -31,6 +31,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chanvalidate" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/shachain" + "github.com/lightningnetwork/lnd/tlv" ) const ( @@ -89,6 +90,33 @@ func (p *PsbtFundingRequired) Error() string { return ErrPsbtFundingRequired.Error() } +// AuxFundingDesc stores a series of attributes that may be used to modify the +// way the channel funding occurs +type AuxFundingDesc struct { + // CustomFundingBlob is a custom blob that'll be stored in the database + // within the OpenChannel struct. This should represent information + // static to the channel lifetime. + CustomFundingBlob tlv.Blob + + // TapscriptRoot is the root of the tapscript tree that will be used to + // create the funding output. + TapscriptRoot chainhash.Hash + + // CustomLocalCommitBlob is a custom blob that'll be stored in the + // first commitment entry for the local party. + CustomLocalCommitBlob tlv.Blob + + // CustomRemoteCommitBlob is a custom blob that'll be stored in the + // first commitment entry for the remote party. + // + // TODO(roasbeef): have this just use the leaf fetcher? + CustomRemoteCommitBlob tlv.Blob + + // InitAuxLeaves is the set of aux leaves that'll be used for the very + // first commitment state. + InitAuxLeaves CommitAuxLeaves +} + // InitFundingReserveMsg is the first message sent to initiate the workflow // required to open a payment channel with a remote peer. The initial required // parameters are configurable across channels. These parameters are to be From 486a1db0ca5992612f7937b430e24a894eacd89e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 17:26:03 -0700 Subject: [PATCH 09/35] lnwallet: use AuxFundingDesc to populate all custom chan info With this commit, we'll now populate all the custom channel information within the OpenChannel and ChannelCommitment structs. --- lnwallet/reservation.go | 25 +++++++++++++++++++++++-- lnwallet/wallet.go | 18 ++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 4f0940fe967..f511ffb6146 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -11,10 +11,12 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" ) // CommitmentType is an enum indicating the commitment type we should use for @@ -217,6 +219,11 @@ type ChannelReservation struct { fundingIntent chanfunding.Intent + // initAuxLeaves is an optional set of aux commitment leaves that'll + // modify the way we construct the commitment transaction, in + // particular the tapscript leaves. + initAuxLeaves fn.Option[CommitAuxLeaves] + // nextRevocationKeyLoc stores the key locator information for this // channel. nextRevocationKeyLoc keychain.KeyLocator @@ -412,7 +419,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, chanType |= channeldb.ScidAliasFeatureBit } - if req.TapscriptRoot.IsSome() { + if req.AuxFundingDesc.IsSome() { chanType |= channeldb.TapscriptRootBit } @@ -437,25 +444,39 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, RemoteBalance: theirBalance, FeePerKw: btcutil.Amount(req.CommitFeePerKw), CommitFee: commitFee, + CustomBlob: fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomLocalCommitBlob + })(req.AuxFundingDesc), }, RemoteCommitment: channeldb.ChannelCommitment{ LocalBalance: ourBalance, RemoteBalance: theirBalance, FeePerKw: btcutil.Amount(req.CommitFeePerKw), CommitFee: commitFee, + CustomBlob: fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomRemoteCommitBlob + })(req.AuxFundingDesc), }, ThawHeight: thawHeight, Db: wallet.Cfg.Database, InitialLocalBalance: ourBalance, InitialRemoteBalance: theirBalance, Memo: req.Memo, - TapscriptRoot: req.TapscriptRoot, + CustomBlob: fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomFundingBlob + })(req.AuxFundingDesc), + TapscriptRoot: fn.MapOption(func(desc AuxFundingDesc) chainhash.Hash { + return desc.TapscriptRoot + })(req.AuxFundingDesc), }, pushMSat: req.PushMSat, pendingChanID: req.PendingChanID, reservationID: id, wallet: wallet, chanFunder: req.ChanFunder, + initAuxLeaves: fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.InitAuxLeaves + })(req.AuxFundingDesc), }, nil } diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index d9482437d25..3946cdc59f1 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -229,10 +229,9 @@ type InitFundingReserveMsg struct { // channel that will be useful to our future selves. Memo []byte - // TapscriptRoot is the root of the tapscript tree that will be used to - // create the funding output. This is an optional field that should - // only be set for taproot channels. - TapscriptRoot fn.Option[chainhash.Hash] + // AuxFundingDesc is an optional descriptor that can be used to modify + // the way channel funding occurs. + AuxFundingDesc fn.Option[AuxFundingDesc] // err is a channel in which all errors will be sent across. Will be // nil if this initial set is successful. @@ -1497,6 +1496,14 @@ func defaultCommitOpts() createCommitOpts { return createCommitOpts{} } +// WithAuxLeaves is a functional option that can be used to set the aux leaves +// for a new commitment transaction. +func WithAuxLeaves(leaves fn.Option[CommitAuxLeaves]) CreateCommitOpt { + return func(o *createCommitOpts) { + o.auxLeaves = leaves + } +} + // CreateCommitOpt is a functional option that can be used to modify the way a // new commitment transaction is created. type CreateCommitOpt func(*createCommitOpts) @@ -1885,6 +1892,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { if pendingReservation.partialState.ChanType.HasLeaseExpiration() { leaseExpiry = pendingReservation.partialState.ThawHeight } + ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( localBalance, remoteBalance, ourContribution.ChannelConfig, theirContribution.ChannelConfig, @@ -1892,6 +1900,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { theirContribution.FirstCommitmentPoint, fundingTxIn, pendingReservation.partialState.ChanType, pendingReservation.partialState.IsInitiator, leaseExpiry, + WithAuxLeaves(pendingReservation.initAuxLeaves), ) if err != nil { req.err <- err @@ -2331,6 +2340,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.theirContribution.FirstCommitmentPoint, *fundingTxIn, chanType, pendingReservation.partialState.IsInitiator, leaseExpiry, + WithAuxLeaves(pendingReservation.initAuxLeaves), ) if err != nil { req.err <- err From f70e702f1bc3f691730188e5e785b5bd134d7979 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 17:27:20 -0700 Subject: [PATCH 10/35] funding: create new AuxFundingController interface In this commit, we make a new `AuxFundingController` interface capable of processing messages off the wire. In addition, we can use it to abstract away details w.r.t how we obtain a `AuxFundingDesc` for a given channel. We'll now use this whenever we get a channel funding request, to make sure we pass along the custom state that a channel may require. --- funding/aux_funding.go | 26 ++++++++++++++++++++++++++ funding/manager.go | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 funding/aux_funding.go diff --git a/funding/aux_funding.go b/funding/aux_funding.go new file mode 100644 index 00000000000..c13be80381c --- /dev/null +++ b/funding/aux_funding.go @@ -0,0 +1,26 @@ +package funding + +import ( + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/protofsm" +) + +// AuxFundingController permits the implementation of the funding of custom +// channels types. The controller serves as a MsgEndpoint which allows it to +// intercept custom messages, or even the regular funding messages. The +// controller might also pass along an aux funding desc based on an existing +// pending channel ID. +type AuxFundingController interface { + // The controller is also a message endpoint. This'll allow it to + // handle custom messages specific to the funding type. + protofsm.MsgEndpoint + + // DescPendingChanID takes a pending channel ID, that may already be + // known due to prior custom channel messages, and maybe returns an aux + // funding desc which can be used to modify how a channel is funded. + // + // TODO(roasbeef): erorr on validation if fail due to invalid root + // match? + DescFromPendingChanID(PendingChanID) fn.Option[lnwallet.AuxFundingDesc] +} diff --git a/funding/manager.go b/funding/manager.go index e4b55d6435d..c7cc22f8119 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -543,6 +543,12 @@ type Config struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // AuxFundingController is an optional controller that can be used to + // modify the way we handle certain custom chanenl types. It's also + // able to automatically handle new custom protocol messages related to + // the funding process. + AuxFundingController fn.Option[AuxFundingController] } // Manager acts as an orchestrator/bridge between the wallet's @@ -1613,6 +1619,15 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, return } + // At this point, if we have an AuxFundingController active, we'll + // check to see if we have any aux info that we should carry along for + // this pid. + auxFundingDesc := fn.MapOption( + func(a AuxFundingController) fn.Option[lnwallet.AuxFundingDesc] { + return a.DescFromPendingChanID(msg.PendingChannelID) + }, + )(f.cfg.AuxFundingController) + req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, PendingChanID: msg.PendingChannelID, @@ -1629,6 +1644,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, ZeroConf: zeroConf, OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, + AuxFundingDesc: fn.FlattenOption(auxFundingDesc), } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -4603,6 +4619,15 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { scidFeatureVal = true } + // At this point, if we have an AuxFundingController active, we'll + // check to see if we have any aux info that we should carry along for + // this pid. + auxFundingDesc := fn.MapOption( + func(a AuxFundingController) fn.Option[lnwallet.AuxFundingDesc] { + return a.DescFromPendingChanID(chanID) + }, + )(f.cfg.AuxFundingController) + req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, PendingChanID: chanID, @@ -4626,6 +4651,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, Memo: msg.Memo, + AuxFundingDesc: fn.FlattenOption(auxFundingDesc), } reservation, err := f.cfg.Wallet.InitChannelReservation(req) From 53018ebfaf2d0a981aaa1d0b97f40ed453d17a75 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Thu, 4 Apr 2024 17:48:11 -0700 Subject: [PATCH 11/35] config+serer: add AuxFundingController as top level cfg option --- config_builder.go | 7 +++++++ server.go | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/config_builder.go b/config_builder.go index a1658fd67d2..62a5c05e960 100644 --- a/config_builder.go +++ b/config_builder.go @@ -35,6 +35,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" @@ -162,6 +163,12 @@ type AuxComponents struct { // MsgRouter is an optional message router that if set will be used in // place of a new balnk default message router. MsgRouter fn.Option[protofsm.MsgRouter] + + // AuxFundingController is an optional controller that can be used to + // modify the way we handle certain custom chanenl types. It's also + // able to automatically handle new custom protocol messages related to + // the funding process. + AuxFundingController fn.Option[funding.AuxFundingController] } // DefaultWalletImpl is the default implementation of our normal, btcwallet diff --git a/server.go b/server.go index e53a7bb07ee..68a7feb7dce 100644 --- a/server.go +++ b/server.go @@ -1493,8 +1493,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr, EnableUpfrontShutdown: cfg.EnableUpfrontShutdown, MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte( s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(), - DeleteAliasEdge: deleteAliasEdge, - AliasManager: s.aliasMgr, + DeleteAliasEdge: deleteAliasEdge, + AliasManager: s.aliasMgr, + AuxFundingController: implCfg.AuxFundingController, }) if err != nil { return nil, err From 26c6ae699fedc95ccca719cf562f506b3cc2d797 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Apr 2024 16:24:43 -0700 Subject: [PATCH 12/35] lnwallet: update GenTaprootFundingScript to also return the taproot internal key --- contractcourt/chain_watcher.go | 2 +- funding/manager.go | 2 +- input/script_utils.go | 16 ++++++++++------ itest/lnd_funding_test.go | 2 +- lnwallet/chanfunding/canned_assembler.go | 4 +++- lnwallet/channel.go | 2 +- lnwallet/wallet.go | 6 +++--- routing/router.go | 2 +- 8 files changed, 21 insertions(+), 15 deletions(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 89280d1bd1c..9cc56f9992b 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -308,7 +308,7 @@ func (c *chainWatcher) Start() error { fundingOpts := fn.MapOptionZ( chanState.TapscriptRoot, lnwallet.TapscriptRootToOpt, ) - c.fundingPkScript, _, err = input.GenTaprootFundingScript( + c.fundingPkScript, _, _, err = input.GenTaprootFundingScript( localKey, remoteKey, 0, fundingOpts..., ) if err != nil { diff --git a/funding/manager.go b/funding/manager.go index c7cc22f8119..05f03deedb7 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -2890,7 +2890,7 @@ func makeFundingScript(channel *channeldb.OpenChannel) ([]byte, error) { fundingOpts := fn.MapOptionZ( channel.TapscriptRoot, lnwallet.TapscriptRootToOpt, ) - pkScript, _, err := input.GenTaprootFundingScript( + pkScript, _, _, err := input.GenTaprootFundingScript( localKey, remoteKey, int64(channel.Capacity), fundingOpts..., ) diff --git a/input/script_utils.go b/input/script_utils.go index 5ad0a0e90d9..08110ed2c20 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -224,9 +224,12 @@ func WithTapscriptRoot(root chainhash.Hash) FundingScriptOpt { } // GenTaprootFundingScript constructs the taproot-native funding output that -// uses MuSig2 to create a single aggregated key to anchor the channel. -func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, - amt int64, opts ...FundingScriptOpt) ([]byte, *wire.TxOut, error) { +// uses MuSig2 to create a single aggregated key to anchor the channel. This +// also returns the MuSig2 aggregated key to allow the callers to examine the +// pre tweaked key as well as the final combined key. +func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, amt int64, + opts ...FundingScriptOpt) ([]byte, *wire.TxOut, + *musig2.AggregateKey, error) { options := defaultFundingScriptOpts() for _, optFunc := range opts { @@ -247,14 +250,15 @@ func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, []*btcec.PublicKey{aPub, bPub}, true, muSig2Opt, ) if err != nil { - return nil, nil, fmt.Errorf("unable to combine keys: %w", err) + return nil, nil, nil, fmt.Errorf("unable to combine "+ + "keys: %w", err) } // Now that we have the combined key, we can create a taproot pkScript // from this, and then make the txOut given the amount. pkScript, err := PayToTaprootScript(combinedKey.FinalKey) if err != nil { - return nil, nil, fmt.Errorf("unable to make taproot "+ + return nil, nil, nil, fmt.Errorf("unable to make taproot "+ "pkscript: %w", err) } @@ -262,7 +266,7 @@ func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, // For the "witness program" we just return the raw pkScript since the // output we create can _only_ be spent with a MuSig2 signature. - return pkScript, txOut, nil + return pkScript, txOut, combinedKey, nil } // SpendMultiSig generates the witness stack required to redeem the 2-of-2 p2wsh diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go index 97613429ebe..a376c590667 100644 --- a/itest/lnd_funding_test.go +++ b/itest/lnd_funding_test.go @@ -1164,7 +1164,7 @@ func deriveFundingShim(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, daveKey, err = btcec.ParsePubKey(daveFundingKey.RawKeyBytes) require.NoError(ht, err) - _, fundingOutput, err = input.GenTaprootFundingScript( + _, fundingOutput, _, err = input.GenTaprootFundingScript( carolKey, daveKey, int64(chanSize), ) require.NoError(ht, err) diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go index 6b95c2bd2aa..bc4f83531f6 100644 --- a/lnwallet/chanfunding/canned_assembler.go +++ b/lnwallet/chanfunding/canned_assembler.go @@ -92,10 +92,12 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) { // Similar to the existing p2wsh script, we'll always ensure // the keys are sorted before use. - return input.GenTaprootFundingScript( + pkScript, txOut, _, err := input.GenTaprootFundingScript( s.localKey.PubKey, s.remoteKey, int64(totalAmt), scriptOpts..., ) + + return pkScript, txOut, err } return input.GenFundingPkScript( diff --git a/lnwallet/channel.go b/lnwallet/channel.go index b048a37b43a..bff21868028 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1554,7 +1554,7 @@ func (lc *LightningChannel) createSignDesc() error { chanState.TapscriptRoot, TapscriptRootToOpt, ) - fundingPkScript, _, err = input.GenTaprootFundingScript( + fundingPkScript, _, _, err = input.GenTaprootFundingScript( localKey, remoteKey, int64(lc.channelState.Capacity), fundingOpts..., ) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 3946cdc59f1..b03c10987fe 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -2155,7 +2155,7 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation, TapscriptRootToOpt, ) - _, fundingOutput, err := input.GenTaprootFundingScript( + _, fundingOutput, _, err := input.GenTaprootFundingScript( localKey, remoteKey, channelValue, fundingOpts..., ) @@ -2403,7 +2403,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { TapscriptRootToOpt, ) //nolint:lll - fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript( + fundingWitnessScript, fundingTxOut, _, err = input.GenTaprootFundingScript( ourKey.PubKey, theirKey.PubKey, channelValue, fundingOpts..., ) @@ -2569,7 +2569,7 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, channelState.TapscriptRoot, TapscriptRootToOpt, ) - fundingScript, _, err = input.GenTaprootFundingScript( + fundingScript, _, _, err = input.GenTaprootFundingScript( localKey, remoteKey, int64(channel.Capacity), fundingOpts..., ) diff --git a/routing/router.go b/routing/router.go index aa3201f8853..5bc1bca2c56 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1550,7 +1550,7 @@ func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, return nil, err } - fundingScript, _, err := input.GenTaprootFundingScript( + fundingScript, _, _, err := input.GenTaprootFundingScript( pubKey1, pubKey2, 0, ) if err != nil { From eaada1060c3a1d0752e01a0af00915953abcb36e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Apr 2024 16:25:22 -0700 Subject: [PATCH 13/35] lnwallet: add TaprootInternalKey method to ShimIntent If this is a taproot channel, then we'll return the internal key which'll be useful to callers. --- lnwallet/chanfunding/canned_assembler.go | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go index bc4f83531f6..b4238ff75a9 100644 --- a/lnwallet/chanfunding/canned_assembler.go +++ b/lnwallet/chanfunding/canned_assembler.go @@ -107,6 +107,34 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) { ) } +// TaprootInternalKey may return the internal key for a musig2 funding output, +// but only if this is actually a musig2 channel. +func (s *ShimIntent) TaprootInternalKey() fn.Option[*btcec.PublicKey] { + if !s.musig2 { + return fn.None[*btcec.PublicKey]() + } + + // TODO(roasbeef): refactor + + var scriptOpts []input.FundingScriptOpt + s.tapscriptRoot.WhenSome(func(root chainhash.Hash) { + scriptOpts = append( + scriptOpts, input.WithTapscriptRoot(root), + ) + }) + + // Similar to the existing p2wsh script, we'll always ensure the keys + // are sorted before use. + // + // We ignore the eror here as this is only called afterr FundingOutput + // is called. + _, _, musig2Key, _ := input.GenTaprootFundingScript( + s.localKey.PubKey, s.remoteKey, 0, scriptOpts..., + ) + + return fn.Some(musig2Key.PreTweakedKey) +} + // Cancel allows the caller to cancel a funding Intent at any time. This will // return any resources such as coins back to the eligible pool to be used in // order channel fundings. From 4e422a940d17afb229735f94ca2f44ca33f3c228 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Apr 2024 16:25:57 -0700 Subject: [PATCH 14/35] lnwallet: for PsbtIntent return the internal key in the POutput We also add a new assertion to the itests to ensure the field is being properly set. --- itest/lnd_psbt_test.go | 11 +++++++++++ lnwallet/chanfunding/psbt_assembler.go | 14 +++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go index a3b5f757b90..fd3db1211bf 100644 --- a/itest/lnd_psbt_test.go +++ b/itest/lnd_psbt_test.go @@ -177,6 +177,17 @@ func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode, }, ) + // If this is a taproot channel, then we'll decode the PSBT to assert + // that an internal key is included. + if commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT { + decodedPSBT, err := psbt.NewFromRawBytes( + bytes.NewReader(tempPsbt), false, + ) + require.NoError(ht, err) + + require.Len(ht, decodedPSBT.Outputs[0].TaprootInternalKey, 32) + } + // Let's add a second channel to the batch. This time between Carol and // Alice. We will publish the batch TX once this channel funding is // complete. diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go index 10bcd70159e..b2686a2250f 100644 --- a/lnwallet/chanfunding/psbt_assembler.go +++ b/lnwallet/chanfunding/psbt_assembler.go @@ -6,11 +6,13 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" ) @@ -208,7 +210,17 @@ func (i *PsbtIntent) FundingParams() (btcutil.Address, int64, *psbt.Packet, } } packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, out) - packet.Outputs = append(packet.Outputs, psbt.POutput{}) + + var pOut psbt.POutput + + // If this is a musig2 channel, then we'll also return the internal key + // information as well along side the output. + pOut.TaprootInternalKey = fn.MapOptionZ( + i.TaprootInternalKey(), schnorr.SerializePubKey, + ) + + packet.Outputs = append(packet.Outputs, pOut) + return addr, out.Value, packet, nil } From cd34dbb412085f3534e8f8242dd822a0db74ec72 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Apr 2024 16:26:17 -0700 Subject: [PATCH 15/35] lnwallet+input: update unit tests for compilation --- lnwallet/channel_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 4914fa13946..1d0763ae961 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -10168,9 +10168,8 @@ func TestCreateBreachRetribution(t *testing.T) { } br, our, their, err := createBreachRetribution( - tc.revocationLog, tx, - aliceChannel.channelState, keyRing, - dummyPrivate, leaseExpiry, + tc.revocationLog, tx, aliceChannel.channelState, + keyRing, dummyPrivate, leaseExpiry, fn.None[CommitAuxLeaves](), ) From 078f756e5734b920a006252718cf9063ebcaea1e Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 17 Apr 2024 18:43:01 -0700 Subject: [PATCH 16/35] funding+lnwallet: only blind tapscript root early in funding flow In this commit, we modify the aux funding work flow slightly. We won't be able to generate the full AuxFundingDesc until both sides has sent+received funding params. So we'll now only attempt to bind the tapscript root as soon as we send+recv the open_channel message. We'll now also make sure that we pass the tapscript root all the way down into the musig2 session creation. --- funding/aux_funding.go | 6 +++ funding/manager.go | 24 +++++------ lnwallet/chanfunding/psbt_assembler.go | 8 ++++ lnwallet/reservation.go | 19 +-------- lnwallet/wallet.go | 56 ++++++++++++-------------- 5 files changed, 53 insertions(+), 60 deletions(-) diff --git a/funding/aux_funding.go b/funding/aux_funding.go index c13be80381c..6c1c2b9b78f 100644 --- a/funding/aux_funding.go +++ b/funding/aux_funding.go @@ -1,6 +1,7 @@ package funding import ( + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/protofsm" @@ -23,4 +24,9 @@ type AuxFundingController interface { // TODO(roasbeef): erorr on validation if fail due to invalid root // match? DescFromPendingChanID(PendingChanID) fn.Option[lnwallet.AuxFundingDesc] + + // DeriveTapscriptRoot takes a pending channel ID and maybe returns a + // tapscript root that should be used when creating any musig2 sessions + // for a channel. + DeriveTapscriptRoot(PendingChanID) fn.Option[chainhash.Hash] } diff --git a/funding/manager.go b/funding/manager.go index 05f03deedb7..9859ef8d20a 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -1620,11 +1620,11 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, } // At this point, if we have an AuxFundingController active, we'll - // check to see if we have any aux info that we should carry along for - // this pid. - auxFundingDesc := fn.MapOption( - func(a AuxFundingController) fn.Option[lnwallet.AuxFundingDesc] { - return a.DescFromPendingChanID(msg.PendingChannelID) + // check to see if we have a special tapscript root to use in our + // musig2 funding output. + tapscriptRoot := fn.MapOption( + func(a AuxFundingController) fn.Option[chainhash.Hash] { + return a.DeriveTapscriptRoot(msg.PendingChannelID) }, )(f.cfg.AuxFundingController) @@ -1644,7 +1644,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, ZeroConf: zeroConf, OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, - AuxFundingDesc: fn.FlattenOption(auxFundingDesc), + TapscriptRoot: fn.FlattenOption(tapscriptRoot), } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -4620,11 +4620,11 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { } // At this point, if we have an AuxFundingController active, we'll - // check to see if we have any aux info that we should carry along for - // this pid. - auxFundingDesc := fn.MapOption( - func(a AuxFundingController) fn.Option[lnwallet.AuxFundingDesc] { - return a.DescFromPendingChanID(chanID) + // check to see if we have a special tapscript root to use in our + // musig2 funding output. + tapscriptRoot := fn.MapOption( + func(a AuxFundingController) fn.Option[chainhash.Hash] { + return a.DeriveTapscriptRoot(chanID) }, )(f.cfg.AuxFundingController) @@ -4651,7 +4651,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, Memo: msg.Memo, - AuxFundingDesc: fn.FlattenOption(auxFundingDesc), + TapscriptRoot: fn.FlattenOption(tapscriptRoot), } reservation, err := f.cfg.Wallet.InitChannelReservation(req) diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go index b2686a2250f..8a9f41f3cf7 100644 --- a/lnwallet/chanfunding/psbt_assembler.go +++ b/lnwallet/chanfunding/psbt_assembler.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/fn" @@ -164,6 +165,13 @@ func (i *PsbtIntent) BindKeys(localKey *keychain.KeyDescriptor, i.State = PsbtOutputKnown } +// BindTapscriptRoot takes an optional tapscript root and binds it to the +// underlying funding intent. This only applies to musig2 channels, and will be +// used to make the musig2 funding output. +func (i *PsbtIntent) BindTapscriptRoot(root fn.Option[chainhash.Hash]) { + i.tapscriptRoot = root +} + // FundingParams returns the parameters that are necessary to start funding the // channel output this intent was created for. It returns the P2WSH funding // address, the exact funding amount and a PSBT packet that contains exactly one diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index f511ffb6146..1e65f29b3a3 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -16,7 +16,6 @@ import ( "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/tlv" ) // CommitmentType is an enum indicating the commitment type we should use for @@ -419,7 +418,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, chanType |= channeldb.ScidAliasFeatureBit } - if req.AuxFundingDesc.IsSome() { + if req.TapscriptRoot.IsSome() { chanType |= channeldb.TapscriptRootBit } @@ -444,39 +443,25 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, RemoteBalance: theirBalance, FeePerKw: btcutil.Amount(req.CommitFeePerKw), CommitFee: commitFee, - CustomBlob: fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { - return desc.CustomLocalCommitBlob - })(req.AuxFundingDesc), }, RemoteCommitment: channeldb.ChannelCommitment{ LocalBalance: ourBalance, RemoteBalance: theirBalance, FeePerKw: btcutil.Amount(req.CommitFeePerKw), CommitFee: commitFee, - CustomBlob: fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { - return desc.CustomRemoteCommitBlob - })(req.AuxFundingDesc), }, ThawHeight: thawHeight, Db: wallet.Cfg.Database, InitialLocalBalance: ourBalance, InitialRemoteBalance: theirBalance, Memo: req.Memo, - CustomBlob: fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { - return desc.CustomFundingBlob - })(req.AuxFundingDesc), - TapscriptRoot: fn.MapOption(func(desc AuxFundingDesc) chainhash.Hash { - return desc.TapscriptRoot - })(req.AuxFundingDesc), + TapscriptRoot: req.TapscriptRoot, }, pushMSat: req.PushMSat, pendingChanID: req.PendingChanID, reservationID: id, wallet: wallet, chanFunder: req.ChanFunder, - initAuxLeaves: fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { - return desc.InitAuxLeaves - })(req.AuxFundingDesc), }, nil } diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index b03c10987fe..b11571b0087 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -91,25 +91,21 @@ func (p *PsbtFundingRequired) Error() string { } // AuxFundingDesc stores a series of attributes that may be used to modify the -// way the channel funding occurs +// way the channel funding occurs. This struct contains information that can +// only be derived once both sides have received and sent their contributions +// to the channel (keys, etc). type AuxFundingDesc struct { // CustomFundingBlob is a custom blob that'll be stored in the database // within the OpenChannel struct. This should represent information // static to the channel lifetime. CustomFundingBlob tlv.Blob - // TapscriptRoot is the root of the tapscript tree that will be used to - // create the funding output. - TapscriptRoot chainhash.Hash - // CustomLocalCommitBlob is a custom blob that'll be stored in the // first commitment entry for the local party. CustomLocalCommitBlob tlv.Blob // CustomRemoteCommitBlob is a custom blob that'll be stored in the // first commitment entry for the remote party. - // - // TODO(roasbeef): have this just use the leaf fetcher? CustomRemoteCommitBlob tlv.Blob // InitAuxLeaves is the set of aux leaves that'll be used for the very @@ -229,9 +225,9 @@ type InitFundingReserveMsg struct { // channel that will be useful to our future selves. Memo []byte - // AuxFundingDesc is an optional descriptor that can be used to modify - // the way channel funding occurs. - AuxFundingDesc fn.Option[AuxFundingDesc] + // TapscriptRoot is an optional tapscript root that if provided, will + // be used to create the combined key for musig2 based channels. + TapscriptRoot fn.Option[chainhash.Hash] // err is a channel in which all errors will be sent across. Will be // nil if this initial set is successful. @@ -268,7 +264,6 @@ type fundingReserveCancelMsg struct { type addContributionMsg struct { pendingFundingID uint64 - // TODO(roasbeef): Should also carry SPV proofs in we're in SPV mode contribution *ChannelContribution // NOTE: In order to avoid deadlocks, this channel MUST be buffered. @@ -439,8 +434,6 @@ type LightningWallet struct { quit chan struct{} wg sync.WaitGroup - - // TODO(roasbeef): handle wallet lock/unlock } // NewLightningWallet creates/opens and initializes a LightningWallet instance. @@ -485,7 +478,6 @@ func (l *LightningWallet) Startup() error { } l.wg.Add(1) - // TODO(roasbeef): multiple request handlers? go l.requestHandler() return nil @@ -1439,7 +1431,6 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation, // transaction via coin selection are freed allowing future reservations to // include them. func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMsg) { - // TODO(roasbeef): holding lock too long l.limboMtx.Lock() defer l.limboMtx.Unlock() @@ -1464,11 +1455,6 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs ) } - // TODO(roasbeef): is it even worth it to keep track of unused keys? - - // TODO(roasbeef): Is it possible to mark the unused change also as - // available? - delete(l.fundingLimbo, req.pendingFundingID) pid := pendingReservation.pendingChanID @@ -1642,16 +1628,24 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // and remote key which will be needed to calculate the multisig // funding output in a next step. pendingChanID := pendingReservation.pendingChanID + walletLog.Debugf("Advancing PSBT funding flow for "+ "pending_id(%x), binding keys local_key=%v, "+ "remote_key=%x", pendingChanID, &ourContribution.MultiSigKey, theirContribution.MultiSigKey.PubKey.SerializeCompressed()) + fundingIntent.BindKeys( &ourContribution.MultiSigKey, theirContribution.MultiSigKey.PubKey, ) + // We might have a tapscript root, so we'll bind that now to + // ensure we make the proper funding output. + fundingIntent.BindTapscriptRoot( + pendingReservation.partialState.TapscriptRoot, + ) + // Exit early because we can't continue the funding flow yet. req.err <- &PsbtFundingRequired{ Intent: fundingIntent, @@ -1724,16 +1718,17 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) { // the commitment transaction for the remote party, and verify their incoming // partial signature. func genMusigSession(ourContribution, theirContribution *ChannelContribution, - signer input.MuSig2Signer, - fundingOutput *wire.TxOut) *MusigPairSession { + signer input.MuSig2Signer, fundingOutput *wire.TxOut, + tapscriptRoot fn.Option[chainhash.Hash]) *MusigPairSession { return NewMusigPairSession(&MusigSessionCfg{ - LocalKey: ourContribution.MultiSigKey, - RemoteKey: theirContribution.MultiSigKey, - LocalNonce: *ourContribution.LocalNonce, - RemoteNonce: *theirContribution.LocalNonce, - Signer: signer, - InputTxOut: fundingOutput, + LocalKey: ourContribution.MultiSigKey, + RemoteKey: theirContribution.MultiSigKey, + LocalNonce: *ourContribution.LocalNonce, + RemoteNonce: *theirContribution.LocalNonce, + Signer: signer, + InputTxOut: fundingOutput, + TapscriptTweak: tapscriptRoot, }) } @@ -1783,6 +1778,7 @@ func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation, musigSessions := genMusigSession( ourContribution, theirContribution, l.Cfg.Signer, fundingOutput, + pendingReservation.partialState.TapscriptRoot, ) pendingReservation.musigSessions = musigSessions } @@ -2166,6 +2162,7 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation, res.musigSessions = genMusigSession( res.ourContribution, res.theirContribution, l.Cfg.Signer, fundingOutput, + res.partialState.TapscriptRoot, ) } @@ -2256,9 +2253,6 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs // As we're about to broadcast the funding transaction, we'll take note // of the current height for record keeping purposes. - // - // TODO(roasbeef): this info can also be piped into light client's - // basic fee estimation? _, bestHeight, err := l.Cfg.ChainIO.GetBestBlock() if err != nil { msg.err <- err From 8a7167a7b7508b0840a1c748e871d003f0eaccb5 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Wed, 17 Apr 2024 23:35:44 -0700 Subject: [PATCH 17/35] funding+lnwallet: finish hook up new aux funding flow For the initiator, once we get the signal that the PSBT has been finalized, we'll call into the aux funder to get the funding desc. For the responder, once we receive the funding_created message, we'll do the same. We now also have local+remote aux leaves for the commitment transaction. --- funding/aux_funding.go | 43 ++++++++++++++--- funding/manager.go | 84 ++++++++++++++++++++++++--------- lnwallet/reservation.go | 56 ++++++++++++++++++---- lnwallet/test/test_interface.go | 2 + lnwallet/wallet.go | 83 +++++++++++++++++++++++++++----- 5 files changed, 220 insertions(+), 48 deletions(-) diff --git a/funding/aux_funding.go b/funding/aux_funding.go index 6c1c2b9b78f..9f041c4e01b 100644 --- a/funding/aux_funding.go +++ b/funding/aux_funding.go @@ -2,6 +2,7 @@ package funding import ( "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/protofsm" @@ -17,16 +18,46 @@ type AuxFundingController interface { // handle custom messages specific to the funding type. protofsm.MsgEndpoint - // DescPendingChanID takes a pending channel ID, that may already be + // DescFromPendingChanID takes a pending channel ID, that may already be // known due to prior custom channel messages, and maybe returns an aux // funding desc which can be used to modify how a channel is funded. - // - // TODO(roasbeef): erorr on validation if fail due to invalid root - // match? - DescFromPendingChanID(PendingChanID) fn.Option[lnwallet.AuxFundingDesc] + DescFromPendingChanID(pid PendingChanID, + openChan *channeldb.OpenChannel, + localKeyRing, remoteKeyRing lnwallet.CommitmentKeyRing, + initiator bool) (fn.Option[lnwallet.AuxFundingDesc], error) // DeriveTapscriptRoot takes a pending channel ID and maybe returns a // tapscript root that should be used when creating any musig2 sessions // for a channel. - DeriveTapscriptRoot(PendingChanID) fn.Option[chainhash.Hash] + DeriveTapscriptRoot(PendingChanID) (fn.Option[chainhash.Hash], error) +} + +// descFromPendingChanID takes a pending channel ID, that may already be +// known due to prior custom channel messages, and maybe returns an aux +// funding desc which can be used to modify how a channel is funded. +func descFromPendingChanID(controller fn.Option[AuxFundingController], + chanID PendingChanID, openChan *channeldb.OpenChannel, + localKeyRing, remoteKeyRing lnwallet.CommitmentKeyRing, + initiator bool) (fn.Option[lnwallet.AuxFundingDesc], error) { + + if controller.IsNone() { + return fn.None[lnwallet.AuxFundingDesc](), nil + } + + return controller.UnsafeFromSome().DescFromPendingChanID( + chanID, openChan, localKeyRing, remoteKeyRing, initiator, + ) +} + +// deriveTapscriptRoot takes a pending channel ID and maybe returns a +// tapscript root that should be used when creating any musig2 sessions +// for a channel. +func deriveTapscriptRoot(controller fn.Option[AuxFundingController], + chanID PendingChanID) (fn.Option[chainhash.Hash], error) { + + if controller.IsNone() { + return fn.None[chainhash.Hash](), nil + } + + return controller.UnsafeFromSome().DeriveTapscriptRoot(chanID) } diff --git a/funding/manager.go b/funding/manager.go index 9859ef8d20a..64329c04dcb 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -99,7 +99,6 @@ const ( // you and limitless channel size (apart from 21 million cap). MaxBtcFundingAmountWumbo = btcutil.Amount(1000000000) - // TODO(roasbeef): tune. msgBufferSize = 50 // MaxWaitNumBlocksFundingConf is the maximum number of blocks to wait @@ -1622,11 +1621,14 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, // At this point, if we have an AuxFundingController active, we'll // check to see if we have a special tapscript root to use in our // musig2 funding output. - tapscriptRoot := fn.MapOption( - func(a AuxFundingController) fn.Option[chainhash.Hash] { - return a.DeriveTapscriptRoot(msg.PendingChannelID) - }, - )(f.cfg.AuxFundingController) + tapscriptRoot, err := deriveTapscriptRoot( + f.cfg.AuxFundingController, msg.PendingChannelID, + ) + if err != nil { + err = fmt.Errorf("error deriving tapscript root: %w", err) + log.Error(err) + f.failFundingFlow(peer, cid, err) + } req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, @@ -1644,7 +1646,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer, ZeroConf: zeroConf, OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, - TapscriptRoot: fn.FlattenOption(tapscriptRoot), + TapscriptRoot: tapscriptRoot, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) @@ -2241,10 +2243,27 @@ func (f *Manager) waitForPsbt(intent *chanfunding.PsbtIntent, return } + // At this point, we'll see if there's an AuxFundingDesc we + // need to deliver so the funding process can continue + // properly. + chanState := resCtx.reservation.ChanState() + localKeys, remoteKeys := resCtx.reservation.CommitmentKeyRings() + auxFundingDesc, err := descFromPendingChanID( + f.cfg.AuxFundingController, cid.tempChanID, chanState, + *localKeys, *remoteKeys, true, + ) + if err != nil { + failFlow("error continuing PSBT flow", err) + return + } + // A non-nil error means we can continue the funding flow. // Notify the wallet so it can prepare everything we need to // continue. - err = resCtx.reservation.ProcessPsbt() + // + // We'll also pass along the aux funding controller as well, + // which may be used to help process the finalized PSBT. + err = resCtx.reservation.ProcessPsbt(auxFundingDesc) if err != nil { failFlow("error continuing PSBT flow", err) return @@ -2253,8 +2272,8 @@ func (f *Manager) waitForPsbt(intent *chanfunding.PsbtIntent, // We are now ready to continue the funding flow. f.continueFundingAccept(resCtx, cid) - // Handle a server shutdown as well because the reservation won't - // survive a restart as it's in memory only. + // Handle a server shutdown as well because the reservation won't + // survive a restart as it's in memory only. case <-f.quit: log.Errorf("Unable to handle funding accept message "+ "for peer_key=%x, pending_chan_id=%x: funding manager "+ @@ -2308,6 +2327,10 @@ func (f *Manager) continueFundingAccept(resCtx *reservationWithCtx, // funding flow fails. cid.setChanID(channelID) + // Now that we're ready to resume the funding flow, we'll call into the + // aux controller with the final funding details so we can obtain the + // funding descs we need. + // Send the FundingCreated msg. fundingCreated := &lnwire.FundingCreated{ PendingChannelID: cid.tempChanID, @@ -2370,7 +2393,6 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer, // final funding transaction, as well as a signature for our version of // the commitment transaction. So at this point, we can validate the // initiator's commitment transaction, then send our own if it's valid. - // TODO(roasbeef): make case (p vs P) consistent throughout fundingOut := msg.FundingPoint log.Infof("completing pending_id(%x) with ChannelPoint(%v)", pendingChanID[:], fundingOut) @@ -2402,16 +2424,33 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer, } } + // At this point, we'll see if there's an AuxFundingDesc we need to + // deliver so the funding process can continue properly. + chanState := resCtx.reservation.ChanState() + localKeys, remoteKeys := resCtx.reservation.CommitmentKeyRings() + auxFundingDesc, err := descFromPendingChanID( + f.cfg.AuxFundingController, cid.tempChanID, chanState, + *localKeys, *remoteKeys, true, + ) + if err != nil { + log.Errorf("error continuing PSBT flow: %v", err) + f.failFundingFlow(peer, cid, err) + return + } + // With all the necessary data available, attempt to advance the // funding workflow to the next stage. If this succeeds then the // funding transaction will broadcast after our next message. // CompleteReservationSingle will also mark the channel as 'IsPending' // in the database. + // + // We'll also directly pass in the AuxFundiner controller as well, + // which may be used by the reservation system to finalize funding our + // side. completeChan, err := resCtx.reservation.CompleteReservationSingle( - &fundingOut, commitSig, + &fundingOut, commitSig, auxFundingDesc, ) if err != nil { - // TODO(roasbeef): better error logging: peerID, channelID, etc. log.Errorf("unable to complete single reservation: %v", err) f.failFundingFlow(peer, cid, err) return @@ -2722,9 +2761,6 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer, // Send an update to the upstream client that the negotiation process // is over. - // - // TODO(roasbeef): add abstraction over updates to accommodate - // long-polling, or SSE, etc. upd := &lnrpc.OpenStatusUpdate{ Update: &lnrpc.OpenStatusUpdate_ChanPending{ ChanPending: &lnrpc.PendingUpdate{ @@ -4429,7 +4465,6 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey, // InitFundingWorkflow sends a message to the funding manager instructing it // to initiate a single funder workflow with the source peer. -// TODO(roasbeef): re-visit blocking nature.. func (f *Manager) InitFundingWorkflow(msg *InitFundingMsg) { f.fundingRequests <- msg } @@ -4622,11 +4657,14 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { // At this point, if we have an AuxFundingController active, we'll // check to see if we have a special tapscript root to use in our // musig2 funding output. - tapscriptRoot := fn.MapOption( - func(a AuxFundingController) fn.Option[chainhash.Hash] { - return a.DeriveTapscriptRoot(chanID) - }, - )(f.cfg.AuxFundingController) + tapscriptRoot, err := deriveTapscriptRoot( + f.cfg.AuxFundingController, chanID, + ) + if err != nil { + err = fmt.Errorf("error deriving tapscript root: %w", err) + msg.Err <- err + return + } req := &lnwallet.InitFundingReserveMsg{ ChainHash: &msg.ChainHash, @@ -4651,7 +4689,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { OptionScidAlias: scid, ScidAliasFeature: scidFeatureVal, Memo: msg.Memo, - TapscriptRoot: fn.FlattenOption(tapscriptRoot), + TapscriptRoot: tapscriptRoot, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 1e65f29b3a3..2dbe9382828 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -218,10 +218,14 @@ type ChannelReservation struct { fundingIntent chanfunding.Intent - // initAuxLeaves is an optional set of aux commitment leaves that'll - // modify the way we construct the commitment transaction, in + // localInitAuxLeaves is an optional set of aux commitment leaves + // that'll modify the way we construct the commitment transaction, in // particular the tapscript leaves. - initAuxLeaves fn.Option[CommitAuxLeaves] + localInitAuxLeaves fn.Option[CommitAuxLeaves] + + // remoteInitAuxLeaves is an optional set of aux commitment leaves for + // the remote party. + remoteInitAuxLeaves fn.Option[CommitAuxLeaves] // nextRevocationKeyLoc stores the key locator information for this // channel. @@ -608,12 +612,15 @@ func (r *ChannelReservation) IsCannedShim() bool { } // ProcessPsbt continues a previously paused funding flow that involves PSBT to -// construct the funding transaction. This method can be called once the PSBT is -// finalized and the signed transaction is available. -func (r *ChannelReservation) ProcessPsbt() error { +// construct the funding transaction. This method can be called once the PSBT +// is finalized and the signed transaction is available. +func (r *ChannelReservation) ProcessPsbt( + auxFundingDesc fn.Option[AuxFundingDesc]) error { + errChan := make(chan error, 1) r.wallet.msgChan <- &continueContributionMsg{ + auxFundingDesc: auxFundingDesc, pendingFundingID: r.reservationID, err: errChan, } @@ -715,8 +722,10 @@ func (r *ChannelReservation) CompleteReservation(fundingInputScripts []*input.Sc // available via the .OurSignatures() method. As this method should only be // called as a response to a single funder channel, only a commitment signature // will be populated. -func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoint, - commitSig input.Signature) (*channeldb.OpenChannel, error) { +func (r *ChannelReservation) CompleteReservationSingle( + fundingPoint *wire.OutPoint, commitSig input.Signature, + auxFundingDesc fn.Option[AuxFundingDesc], +) (*channeldb.OpenChannel, error) { errChan := make(chan error, 1) completeChan := make(chan *channeldb.OpenChannel, 1) @@ -726,6 +735,7 @@ func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoi fundingOutpoint: fundingPoint, theirCommitmentSig: commitSig, completeChan: completeChan, + auxFundingDesc: auxFundingDesc, err: errChan, } @@ -811,6 +821,36 @@ func (r *ChannelReservation) Cancel() error { return <-errChan } +// ChanState the current open channel state. +func (r *ChannelReservation) ChanState() *channeldb.OpenChannel { + r.RLock() + defer r.RUnlock() + return r.partialState +} + +// CommitmentKeyRings returns the local+remote key ring used for the very first +// commitment transaction both parties. +func (r *ChannelReservation) CommitmentKeyRings() (*CommitmentKeyRing, *CommitmentKeyRing) { + r.RLock() + defer r.RUnlock() + + chanType := r.partialState.ChanType + ourChanCfg := r.ourContribution.ChannelConfig + theirChanCfg := r.theirContribution.ChannelConfig + + localKeys := DeriveCommitmentKeys( + r.ourContribution.FirstCommitmentPoint, true, chanType, + ourChanCfg, theirChanCfg, + ) + + remoteKeys := DeriveCommitmentKeys( + r.theirContribution.FirstCommitmentPoint, false, chanType, + ourChanCfg, theirChanCfg, + ) + + return localKeys, remoteKeys +} + // VerifyConstraints is a helper function that can be used to check the sanity // of various channel constraints. func VerifyConstraints(c *channeldb.ChannelConstraints, diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index 401c46683a4..0a6f20e5f2e 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -34,6 +34,7 @@ import ( "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs/btcdnotify" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/kvdb" @@ -936,6 +937,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness, fundingPoint := aliceChanReservation.FundingOutpoint() _, err = bobChanReservation.CompleteReservationSingle( fundingPoint, aliceCommitSig, + fn.None[lnwallet.AuxFundingDesc](), ) require.NoError(t, err, "bob unable to consume single reservation") diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index b11571b0087..98d251e32b7 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -108,9 +108,13 @@ type AuxFundingDesc struct { // first commitment entry for the remote party. CustomRemoteCommitBlob tlv.Blob - // InitAuxLeaves is the set of aux leaves that'll be used for the very - // first commitment state. - InitAuxLeaves CommitAuxLeaves + // LocalInitAuxLeaves is the set of aux leaves that'll be used for our + // very first commitment state. + LocalInitAuxLeaves CommitAuxLeaves + + // RemoteInitAuxLeaves is the set of aux leaves that'll be used for the + // very first commitment state for the remote party. + RemoteInitAuxLeaves CommitAuxLeaves } // InitFundingReserveMsg is the first message sent to initiate the workflow @@ -276,6 +280,8 @@ type addContributionMsg struct { type continueContributionMsg struct { pendingFundingID uint64 + auxFundingDesc fn.Option[AuxFundingDesc] + // NOTE: In order to avoid deadlocks, this channel MUST be buffered. err chan error } @@ -331,6 +337,8 @@ type addCounterPartySigsMsg struct { type addSingleFunderSigsMsg struct { pendingFundingID uint64 + auxFundingDesc fn.Option[AuxFundingDesc] + // fundingOutpoint is the outpoint of the completed funding // transaction as assembled by the workflow initiator. fundingOutpoint *wire.OutPoint @@ -1474,7 +1482,8 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs // createCommitOpts is a struct that holds the options for creating a new // commitment transaction. type createCommitOpts struct { - auxLeaves fn.Option[CommitAuxLeaves] + localAuxLeaves fn.Option[CommitAuxLeaves] + remoteAuxLeaves fn.Option[CommitAuxLeaves] } // defaultCommitOpts returns a new createCommitOpts with default values. @@ -1484,9 +1493,12 @@ func defaultCommitOpts() createCommitOpts { // WithAuxLeaves is a functional option that can be used to set the aux leaves // for a new commitment transaction. -func WithAuxLeaves(leaves fn.Option[CommitAuxLeaves]) CreateCommitOpt { +func WithAuxLeaves(localLeaves, + remoteLeaves fn.Option[CommitAuxLeaves]) CreateCommitOpt { + return func(o *createCommitOpts) { - o.auxLeaves = leaves + o.localAuxLeaves = localLeaves + o.remoteAuxLeaves = remoteLeaves } } @@ -1521,7 +1533,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, ourCommitTx, err := CreateCommitTx( chanType, fundingTxIn, localCommitmentKeys, ourChanCfg, theirChanCfg, localBalance, remoteBalance, 0, initiator, - leaseExpiry, options.auxLeaves, + leaseExpiry, options.localAuxLeaves, ) if err != nil { return nil, nil, err @@ -1535,7 +1547,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, theirCommitTx, err := CreateCommitTx( chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg, ourChanCfg, remoteBalance, localBalance, 0, !initiator, - leaseExpiry, options.auxLeaves, + leaseExpiry, options.remoteAuxLeaves, ) if err != nil { return nil, nil, err @@ -1814,6 +1826,24 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { return } + chanState := pendingReservation.partialState + + // If we have an aux funding desc, then we can use it to populate some + // of the optional, but opaque TLV blobs we'll carry for the channel. + chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomFundingBlob + })(req.auxFundingDesc) + chanState.LocalCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomLocalCommitBlob + }, + )(req.auxFundingDesc) + chanState.RemoteCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomRemoteCommitBlob + }, + )(req.auxFundingDesc) + ourContribution := pendingReservation.ourContribution theirContribution := pendingReservation.theirContribution chanPoint := pendingReservation.partialState.FundingOutpoint @@ -1872,7 +1902,6 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { // Store their current commitment point. We'll need this after the // first state transition in order to verify the authenticity of the // revocation. - chanState := pendingReservation.partialState chanState.RemoteCurrentRevocation = theirContribution.FirstCommitmentPoint // Create the txin to our commitment transaction; required to construct @@ -1889,6 +1918,13 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { leaseExpiry = pendingReservation.partialState.ThawHeight } + localAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.LocalInitAuxLeaves + })(req.auxFundingDesc) + remoteAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.RemoteInitAuxLeaves + })(req.auxFundingDesc) + ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( localBalance, remoteBalance, ourContribution.ChannelConfig, theirContribution.ChannelConfig, @@ -1896,7 +1932,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { theirContribution.FirstCommitmentPoint, fundingTxIn, pendingReservation.partialState.ChanType, pendingReservation.partialState.IsInitiator, leaseExpiry, - WithAuxLeaves(pendingReservation.initAuxLeaves), + WithAuxLeaves(localAuxLeaves, remoteAuxLeaves), ) if err != nil { req.err <- err @@ -2313,6 +2349,23 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { defer pendingReservation.Unlock() chanState := pendingReservation.partialState + + // If we have an aux funding desc, then we can use it to populate some + // of the optional, but opaque TLV blobs we'll carry for the channel. + chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomFundingBlob + })(req.auxFundingDesc) + chanState.LocalCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomLocalCommitBlob + }, + )(req.auxFundingDesc) + chanState.RemoteCommitment.CustomBlob = fn.MapOption( + func(desc AuxFundingDesc) tlv.Blob { + return desc.CustomRemoteCommitBlob + }, + )(req.auxFundingDesc) + chanType := pendingReservation.partialState.ChanType chanState.FundingOutpoint = *req.fundingOutpoint fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil) @@ -2326,6 +2379,14 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { if pendingReservation.partialState.ChanType.HasLeaseExpiration() { leaseExpiry = pendingReservation.partialState.ThawHeight } + + localAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.LocalInitAuxLeaves + })(req.auxFundingDesc) + remoteAuxLeaves := fn.MapOption(func(desc AuxFundingDesc) CommitAuxLeaves { + return desc.RemoteInitAuxLeaves + })(req.auxFundingDesc) + ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( localBalance, remoteBalance, pendingReservation.ourContribution.ChannelConfig, @@ -2334,7 +2395,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.theirContribution.FirstCommitmentPoint, *fundingTxIn, chanType, pendingReservation.partialState.IsInitiator, leaseExpiry, - WithAuxLeaves(pendingReservation.initAuxLeaves), + WithAuxLeaves(localAuxLeaves, remoteAuxLeaves), ) if err != nil { req.err <- err From ce70590b9e316ae1211731033350c2f546c7eee3 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:47:05 -0700 Subject: [PATCH 18/35] lnwallet: add new AuxSigner interface to mirror SigPool In this commit, we add a new aux signer interface that's meant to mirror the SigPool. If present, this'll be used to (maybe) obtain signatures for second level HTLCs for certain classes of custom channels. --- lnwallet/aux_signer.go | 145 +++++++++++++++++++++++++++++++++++++++++ lnwallet/commitment.go | 31 +++++++++ 2 files changed, 176 insertions(+) create mode 100644 lnwallet/aux_signer.go diff --git a/lnwallet/aux_signer.go b/lnwallet/aux_signer.go new file mode 100644 index 00000000000..ed2760d12f2 --- /dev/null +++ b/lnwallet/aux_signer.go @@ -0,0 +1,145 @@ +package lnwallet + +import ( + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/tlv" +) + +// BaseAuxJob is a struct that contains the common fields that are shared among +// the aux sign/verify jobs. +type BaseAuxJob struct { + // OutputIndex is the output index of the HTLC on the commitment + // transaction being signed. + OutputIndex int32 + + // KeyRing is the commitment key ring that contains the keys needed to + // generate the second level HTLC signatures. + KeyRing CommitmentKeyRing + + // HTLC is the HTLC that is being signed or verified. + HTLC PaymentDescriptor + + // CommitBlob is the commitment transaction blob that contains the aux + // information for this channel. + CommitBlob fn.Option[tlv.Blob] + + // HtlcLeaf is the aux tap leaf that corresponds to the HTLC being + // signed/verified. + HtlcLeaf input.AuxTapLeaf +} + +// AuxSigJob is a struct that contains all the information needed to sign an +// HTLC for custom channels. +type AuxSigJob struct { + // SignDesc is the sign desc for this HTLC. + SignDesc input.SignDescriptor + + BaseAuxJob + + // Resp is a channel that will be used to send the result of the sign + // job. + Resp chan AuxSigJobResp + + // Cancel is a channel that should be closed if the caller wishes to + // abandon all pending sign jobs part of a single batch. + Cancel chan struct{} +} + +// NewAuxSigJob creates a new AuxSigJob. +func NewAuxSigJob(sigJob SignJob, keyRing CommitmentKeyRing, + htlc PaymentDescriptor, commitBlob fn.Option[tlv.Blob], + htlcLeaf input.AuxTapLeaf, cancelChan chan struct{}) AuxSigJob { + + return AuxSigJob{ + SignDesc: sigJob.SignDesc, + BaseAuxJob: BaseAuxJob{ + OutputIndex: sigJob.OutputIndex, + KeyRing: keyRing, + HTLC: htlc, + CommitBlob: commitBlob, + HtlcLeaf: htlcLeaf, + }, + Resp: make(chan AuxSigJobResp, 1), + Cancel: cancelChan, + } +} + +// AuxSigJobResp is a struct that contains the result of a sign job. +type AuxSigJobResp struct { + // SigBlob is the signature blob that was generated for the HTLC. This + // is an opauqe TLV field that may contains the signature and other + // data. + // + // TODO(roasbeef): just make sig? or new type that can map to/from a + // blob + SigBlob fn.Option[tlv.Blob] + + // Err is the error that occurred when executing the specified + // signature job. In the case that no error occurred, this value will + // be nil. + Err error +} + +// AuxVerifyJob is a struct that contains all the information needed to verify +// an HTLC for custom channels. +type AuxVerifyJob struct { + // SigBlob is the signature blob that was generated for the HTLC. This + // is an opauqe TLV field that may contains the signature and other + // data. + SigBlob fn.Option[tlv.Blob] + + BaseAuxJob + + // Cancel is a channel that should be closed if the caller wishes to + // abandon the job. + Cancel chan struct{} + + // ErrResp is a channel that will be used to send the result of the + // verify job. + ErrResp chan error +} + +// NewAuxVerifyJob creates a new AuxVerifyJob. +func NewAuxVerifyJob(sig fn.Option[tlv.Blob], keyRing CommitmentKeyRing, + htlc PaymentDescriptor, commitBlob fn.Option[tlv.Blob], + htlcLeaf input.AuxTapLeaf) AuxVerifyJob { + + return AuxVerifyJob{ + SigBlob: sig, + BaseAuxJob: BaseAuxJob{ + KeyRing: keyRing, + HTLC: htlc, + CommitBlob: commitBlob, + HtlcLeaf: htlcLeaf, + }, + } +} + +// AuxSigner is an interface that is used to sign and verify HTLCs for custom +// channels. It is similar to the existing SigPool, but uses opaque blobs to +// shuffle around signature information and other metadata. +type AuxSigner interface { + // SubmitSecondLevelSigBatch takes a batch of aux sign jobs and + // processes them asynchronously. + SubmitSecondLevelSigBatch(chanState *channeldb.OpenChannel, + commitTx *wire.MsgTx, sigJob []AuxSigJob) error + + // PackSigs takes a series of aux signatures and packs them into a + // single blob that can be sent alongside the CommitSig messages. + PackSigs(map[input.HtlcIndex]fn.Option[tlv.Blob]) (fn.Option[tlv.Blob], + error) + + // UnpackSigs takes a packed blob of signatures and returns the + // original signatures for each HTLC, keyed by HTLC index. + UnpackSigs( + fn.Option[tlv.Blob]) (map[input.HtlcIndex]fn.Option[tlv.Blob], + error) + + // VerifySecondLevelSigs attempts to synchronously verify a batch of aux + // sig jobs. + VerifySecondLevelSigs(chanState *channeldb.OpenChannel, + commitTx *wire.MsgTx, verifyJob []AuxVerifyJob) error +} diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index bdd10555a10..b0c7fc645b6 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -833,6 +833,37 @@ func updateAuxBlob(chanState *channeldb.OpenChannel, ) } +// packSigs is a helper function that attempts to pack a series of aux +// signatures and packs them into a single blob that can be sent alongside the +// CommitSig messages. +func packSigs(auxSigs map[input.HtlcIndex]fn.Option[tlv.Blob], + signer fn.Option[AuxSigner]) (tlv.Blob, error) { + + if signer.IsNone() { + return nil, nil + } + + blobOption, err := signer.UnsafeFromSome().PackSigs(auxSigs) + if err != nil { + return nil, fmt.Errorf("error packing aux sigs: %w", err) + } + + return blobOption.UnwrapOr(nil), nil +} + +// unpackSigs is a helper function that takes a packed blob of signatures and +// returns the original signatures for each HTLC, keyed by HTLC index. +func unpackSigs(blob fn.Option[tlv.Blob], + signer fn.Option[AuxSigner]) (map[input.HtlcIndex]fn.Option[tlv.Blob], + error) { + + if signer.IsNone() { + return nil, nil + } + + return signer.UnsafeFromSome().UnpackSigs(blob) +} + // createUnsignedCommitmentTx generates the unsigned commitment transaction for // a commitment view and returns it as part of the unsignedCommitmentTx. The // passed in balances should be balances *before* subtracting any commitment From f4b5641f22bb04d0df4ad678e8028c87fa57c102 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:47:26 -0700 Subject: [PATCH 19/35] lnwallet: add WithAuxSigner option to channel --- lnwallet/channel.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index bff21868028..699a8274c62 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1336,6 +1336,10 @@ type LightningChannel struct { // signatures, of which there may be hundreds. sigPool *SigPool + // auxSigner is a special signer used to obtain opaque signatures for + // custom channel variants. + auxSigner fn.Option[AuxSigner] + // Capacity is the total capacity of this channel. Capacity btcutil.Amount @@ -1400,6 +1404,7 @@ type channelOpts struct { remoteNonce *musig2.Nonces leafStore fn.Option[AuxLeafStore] + auxSigner fn.Option[AuxSigner] skipNonceInit bool } @@ -1438,6 +1443,13 @@ func WithLeafStore(store AuxLeafStore) ChannelOpt { } } +// WithAuxSigner is used to specify a custom aux signer for the channel. +func WithAuxSigner(signer AuxSigner) ChannelOpt { + return func(o *channelOpts) { + o.auxSigner = fn.Some[AuxSigner](signer) + } +} + // defaultChannelOpts returns the set of default options for a new channel. func defaultChannelOpts() *channelOpts { return &channelOpts{} @@ -1481,6 +1493,7 @@ func NewLightningChannel(signer input.Signer, lc := &LightningChannel{ Signer: signer, leafStore: opts.leafStore, + auxSigner: opts.auxSigner, sigPool: sigPool, currentHeight: localCommit.CommitHeight, remoteCommitChain: newCommitmentChain(), From 01ef2b1a54af5a69abbf85fbc854d9d0bf9a6a9b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:48:36 -0700 Subject: [PATCH 20/35] lnwallet: obtain+verify aux sigs for all second level HTLCs In this commit, we start to use the new AuxSigner to obtain+verify aux sigs for all second level HTLCs. This is similar to the existing SigPool, but we'll only attempt to do this if the AuxSigner is present (won't be for most channels). --- chainreg/chainregistry.go | 4 + config_builder.go | 12 ++- contractcourt/chain_arbitrator.go | 10 ++ funding/manager.go | 7 ++ htlcswitch/link.go | 1 + lnwallet/aux_signer.go | 3 + lnwallet/channel.go | 159 ++++++++++++++++++++++++------ lnwallet/config.go | 4 + lnwallet/sigpool.go | 2 + lnwallet/wallet.go | 3 + peer/brontide.go | 10 ++ server.go | 2 + 12 files changed, 181 insertions(+), 36 deletions(-) diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index 934a84996b8..8e0142eec7a 100644 --- a/chainreg/chainregistry.go +++ b/chainreg/chainregistry.go @@ -68,6 +68,10 @@ type Config struct { // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] + // BlockCache is the main cache for storing block information. BlockCache *blockcache.BlockCache diff --git a/config_builder.go b/config_builder.go index 62a5c05e960..7c9045d7eb8 100644 --- a/config_builder.go +++ b/config_builder.go @@ -165,10 +165,14 @@ type AuxComponents struct { MsgRouter fn.Option[protofsm.MsgRouter] // AuxFundingController is an optional controller that can be used to - // modify the way we handle certain custom chanenl types. It's also + // modify the way we handle certain custom channel types. It's also // able to automatically handle new custom protocol messages related to // the funding process. AuxFundingController fn.Option[funding.AuxFundingController] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // DefaultWalletImpl is the default implementation of our normal, btcwallet @@ -575,6 +579,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, ChanStateDB: dbs.ChanStateDB.ChannelStateDB(), NeutrinoCS: neutrinoCS, AuxLeafStore: aux.AuxLeafStore, + AuxSigner: aux.AuxSigner, ActiveNetParams: d.cfg.ActiveNetParams, FeeURL: d.cfg.FeeURL, Dialer: func(addr string) (net.Conn, error) { @@ -727,6 +732,7 @@ func (d *DefaultWalletImpl) BuildChainControl( NetParams: *walletConfig.NetParams, CoinSelectionStrategy: walletConfig.CoinSelectionStrategy, AuxLeafStore: partialChainControl.Cfg.AuxLeafStore, + AuxSigner: partialChainControl.Cfg.AuxSigner, } // The broadcast is already always active for neutrino nodes, so we @@ -906,10 +912,6 @@ type DatabaseInstances struct { // for native SQL queries for tables that already support it. This may // be nil if the use-native-sql flag was not set. NativeSQLStore *sqldb.BaseDB - - // AuxLeafStore is an optional data source that can be used by custom - // channels to fetch+store various data. - AuxLeafStore fn.Option[lnwallet.AuxLeafStore] } // DefaultDatabaseBuilder is a type that builds the default database backends diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index 3245162cb59..d7d9e93cfd3 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -221,6 +221,10 @@ type ChainArbitratorConfig struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // ChainArbitrator is a sub-system that oversees the on-chain resolution of all @@ -307,6 +311,9 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions, a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) chanMachine, err := lnwallet.NewLightningChannel( a.c.cfg.Signer, channel, nil, chanOpts..., @@ -357,6 +364,9 @@ func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // Finally, we'll force close the channel completing // the force close workflow. diff --git a/funding/manager.go b/funding/manager.go index 64329c04dcb..8c338c6f3e9 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -548,6 +548,10 @@ type Config struct { // able to automatically handle new custom protocol messages related to // the funding process. AuxFundingController fn.Option[AuxFundingController] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] } // Manager acts as an orchestrator/bridge between the wallet's @@ -1077,6 +1081,9 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel, f.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + f.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // We create the state-machine object which wraps the database state. lnChannel, err := lnwallet.NewLightningChannel( diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 778e78d7008..010ad03bf08 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -2554,6 +2554,7 @@ func (l *channelLink) updateCommitTx() error { CommitSig: newCommit.CommitSig, HtlcSigs: newCommit.HtlcSigs, PartialSig: newCommit.PartialSig, + ExtraData: newCommit.AuxSigBlob, } l.cfg.Peer.SendMessage(false, commitSig) diff --git a/lnwallet/aux_signer.go b/lnwallet/aux_signer.go index ed2760d12f2..da2d0f5151f 100644 --- a/lnwallet/aux_signer.go +++ b/lnwallet/aux_signer.go @@ -77,6 +77,9 @@ type AuxSigJobResp struct { // blob SigBlob fn.Option[tlv.Blob] + // HtlcIndex is the index of the HTLC that was signed. + HtlcIndex uint64 + // Err is the error that occurred when executing the specified // signature job. In the case that no error occurred, this value will // be nil. diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 699a8274c62..e5ae4984153 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -3590,7 +3590,7 @@ func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, chanState *channeldb.OpenChannel, leaseExpiry uint32, remoteCommitView *commitment, - leafStore fn.Option[AuxLeafStore]) ([]SignJob, chan struct{}, error) { + leafStore fn.Option[AuxLeafStore]) ([]SignJob, []AuxSigJob, chan struct{}, error) { var ( isRemoteInitiator = !chanState.IsInitiator @@ -3607,9 +3607,10 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // With the keys generated, we'll make a slice with enough capacity to // hold potentially all the HTLCs. The actual slice may be a bit // smaller (than its total capacity) and some HTLCs may be dust. - numSigs := (len(remoteCommitView.incomingHTLCs) + - len(remoteCommitView.outgoingHTLCs)) + numSigs := len(remoteCommitView.incomingHTLCs) + + len(remoteCommitView.outgoingHTLCs) sigBatch := make([]SignJob, 0, numSigs) + auxSigBatch := make([]AuxSigJob, 0, numSigs) var err error cancelChan := make(chan struct{}) @@ -3619,7 +3620,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, *keyRing, ) if err != nil { - return nil, nil, fmt.Errorf("unable to fetch aux leaves: "+ + return nil, nil, nil, fmt.Errorf("unable to fetch aux leaves: "+ "%w", err) } @@ -3670,11 +3671,9 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, fn.FlattenOption(auxLeaf), ) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - // TODO(roasbeef): hook up signer interface here - // Construct a full hash cache as we may be signing a segwit v1 // sighash. txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] @@ -3706,6 +3705,12 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, } sigBatch = append(sigBatch, sigJob) + + auxSigJob := NewAuxSigJob( + sigJob, *keyRing, htlc, remoteCommitView.customBlob, + fn.FlattenOption(auxLeaf), cancelChan, + ) + auxSigBatch = append(auxSigBatch, auxSigJob) } for _, htlc := range remoteCommitView.outgoingHTLCs { if HtlcIsDust( @@ -3750,7 +3755,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, fn.FlattenOption(auxLeaf), ) if err != nil { - return nil, nil, err + return nil, nil, nil, err } // Construct a full hash cache as we may be signing a segwit v1 @@ -3783,9 +3788,15 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, } sigBatch = append(sigBatch, sigJob) + + auxSigJob := NewAuxSigJob( + sigJob, *keyRing, htlc, remoteCommitView.customBlob, + fn.FlattenOption(auxLeaf), cancelChan, + ) + auxSigBatch = append(auxSigBatch, auxSigJob) } - return sigBatch, cancelChan, nil + return sigBatch, auxSigBatch, cancelChan, nil } // createCommitDiff will create a commit diff given a new pending commitment @@ -3794,7 +3805,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // new commitment to the remote party. The commit diff returned contains all // information necessary for retransmission. func (lc *LightningChannel) createCommitDiff(newCommit *commitment, - commitSig lnwire.Sig, htlcSigs []lnwire.Sig) (*channeldb.CommitDiff, + commitSig lnwire.Sig, htlcSigs []lnwire.Sig, + auxSigs map[input.HtlcIndex]fn.Option[tlv.Blob]) (*channeldb.CommitDiff, error) { // First, we need to convert the funding outpoint into the ID that's @@ -3918,6 +3930,11 @@ func (lc *LightningChannel) createCommitDiff(newCommit *commitment, // disk. diskCommit := newCommit.toDiskCommit(false) + auxSigBlob, err := packSigs(auxSigs, lc.auxSigner) + if err != nil { + return nil, fmt.Errorf("error packing aux sigs: %w", err) + } + return &channeldb.CommitDiff{ Commitment: *diskCommit, CommitSig: &lnwire.CommitSig{ @@ -3926,6 +3943,7 @@ func (lc *LightningChannel) createCommitDiff(newCommit *commitment, ), CommitSig: commitSig, HtlcSigs: htlcSigs, + ExtraData: auxSigBlob, }, LogUpdates: logUpdates, OpenedCircuitKeys: openCircuitKeys, @@ -4375,6 +4393,10 @@ type CommitSigs struct { // PartialSig is the musig2 partial signature for taproot commitment // transactions. PartialSig lnwire.OptPartialSigWithNonceTLV + + // AuxSigBlob is the blob containing all the auxiliary signatures for + // this new commitment state. + AuxSigBlob tlv.Blob } // NewCommitState wraps the various signatures needed to properly @@ -4493,7 +4515,7 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { if lc.channelState.ChanType.HasLeaseExpiration() { leaseExpiry = lc.channelState.ThawHeight } - sigBatch, cancelChan, err := genRemoteHtlcSigJobs( + sigBatch, auxSigBatch, cancelChan, err := genRemoteHtlcSigJobs( keyRing, lc.channelState, leaseExpiry, newCommitView, lc.leafStore, ) @@ -4502,6 +4524,16 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { } lc.sigPool.SubmitSignBatch(sigBatch) + err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error { + return a.SubmitSecondLevelSigBatch( + lc.channelState, newCommitView.txn, auxSigBatch, + ) + }) + if err != nil { + return nil, fmt.Errorf("error submitting second level sig "+ + "batch: %w", err) + } + // While the jobs are being carried out, we'll Sign their version of // the new commitment transaction while we're waiting for the rest of // the HTLC signatures to be processed. @@ -4545,11 +4577,18 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { sort.Slice(sigBatch, func(i, j int) bool { return sigBatch[i].OutputIndex < sigBatch[j].OutputIndex }) + sort.Slice(auxSigBatch, func(i, j int) bool { + return auxSigBatch[i].OutputIndex < auxSigBatch[j].OutputIndex + }) // With the jobs sorted, we'll now iterate through all the responses to // gather each of the signatures in order. htlcSigs = make([]lnwire.Sig, 0, len(sigBatch)) - for _, htlcSigJob := range sigBatch { + auxSigs := make( + map[input.HtlcIndex]fn.Option[tlv.Blob], len(auxSigBatch), + ) + for i := range sigBatch { + htlcSigJob := sigBatch[i] jobResp := <-htlcSigJob.Resp // If an error occurred, then we'll cancel any other active @@ -4560,12 +4599,30 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { } htlcSigs = append(htlcSigs, jobResp.Sig) + + if lc.auxSigner.IsNone() { + continue + } + + auxHtlcSigJob := auxSigBatch[i] + auxJobResp := <-auxHtlcSigJob.Resp + + // If an error occurred, then we'll cancel any other active + // jobs. + if auxJobResp.Err != nil { + close(cancelChan) + return nil, auxJobResp.Err + } + + auxSigs[auxJobResp.HtlcIndex] = auxJobResp.SigBlob } // As we're about to proposer a new commitment state for the remote // party, we'll write this pending state to disk before we exit, so we // can retransmit it if necessary. - commitDiff, err := lc.createCommitDiff(newCommitView, sig, htlcSigs) + commitDiff, err := lc.createCommitDiff( + newCommitView, sig, htlcSigs, auxSigs, + ) if err != nil { return nil, err } @@ -4587,6 +4644,7 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) { CommitSig: sig, HtlcSigs: htlcSigs, PartialSig: lnwire.MaybePartialSigWithNonce(partialSig), + AuxSigBlob: commitDiff.CommitSig.ExtraData, }, PendingHTLCs: commitDiff.Commitment.Htlcs, }, nil @@ -5066,7 +5124,8 @@ func (lc *LightningChannel) computeView(view *HtlcView, remoteChain bool, func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, localCommitmentView *commitment, keyRing *CommitmentKeyRing, htlcSigs []lnwire.Sig, leaseExpiry uint32, - leafStore fn.Option[AuxLeafStore]) ([]VerifyJob, error) { + leafStore fn.Option[AuxLeafStore], auxSigner fn.Option[AuxSigner], + sigBlob fn.Option[tlv.Blob]) ([]VerifyJob, []AuxVerifyJob, error) { var ( isLocalInitiator = chanState.IsInitiator @@ -5085,13 +5144,22 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, numHtlcs := (len(localCommitmentView.incomingHTLCs) + len(localCommitmentView.outgoingHTLCs)) verifyJobs := make([]VerifyJob, 0, numHtlcs) + auxVerifyJobs := make([]AuxVerifyJob, 0, numHtlcs) auxLeaves, err := AuxLeavesFromCommit( chanState, *localCommitmentView.toDiskCommit(true), leafStore, *keyRing, ) if err != nil { - return nil, fmt.Errorf("unable to fetch aux leaves: %w", + return nil, nil, fmt.Errorf("unable to fetch aux leaves: %w", + err) + } + + // If we have a sig blob, then we'll attempt to map that to individual + // blobs for each HTLC we might need a signature for. + auxHtlcSigs, err := unpackSigs(sigBlob, auxSigner) + if err != nil { + return nil, nil, fmt.Errorf("error unpacking aux sigs: %w", err) } @@ -5105,6 +5173,8 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcIndex uint64 sigHash func() ([]byte, error) sig input.Signature + htlc *PaymentDescriptor + auxLeaf input.AuxTapLeaf err error ) @@ -5115,7 +5185,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // index, then this means that we need to generate an HTLC // success transaction in order to validate the signature. case localCommitmentView.incomingHTLCIndex[outputIndex] != nil: - htlc := localCommitmentView.incomingHTLCIndex[outputIndex] + htlc = localCommitmentView.incomingHTLCIndex[outputIndex] htlcIndex = htlc.HtlcIndex @@ -5128,20 +5198,20 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - auxLeaf := fn.MapOption(func( + leaf := fn.MapOption(func( l CommitAuxLeaves) input.AuxTapLeaf { leaves := l.IncomingHtlcLeaves idx := htlc.HtlcIndex return leaves[idx].SecondLevelLeaf })(auxLeaves) + auxLeaf = fn.FlattenOption(leaf) successTx, err := CreateHtlcSuccessTx( chanType, isLocalInitiator, op, outputAmt, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, - fn.FlattenOption(auxLeaf), + keyRing.ToLocalKey, auxLeaf, ) if err != nil { return nil, err @@ -5184,7 +5254,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // Make sure there are more signatures left. if i >= len(htlcSigs) { - return nil, fmt.Errorf("not enough HTLC " + + return nil, nil, fmt.Errorf("not enough HTLC " + "signatures") } @@ -5200,7 +5270,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // is valid. sig, err = htlcSigs[i].ToSignature() if err != nil { - return nil, err + return nil, nil, err } htlc.sig = sig @@ -5208,7 +5278,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // generate a timeout transaction so we can verify the // signature presented. case localCommitmentView.outgoingHTLCIndex[outputIndex] != nil: - htlc := localCommitmentView.outgoingHTLCIndex[outputIndex] + htlc = localCommitmentView.outgoingHTLCIndex[outputIndex] htlcIndex = htlc.HtlcIndex @@ -5221,21 +5291,21 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee - auxLeaf := fn.MapOption(func( + leaf := fn.MapOption(func( l CommitAuxLeaves) input.AuxTapLeaf { leaves := l.OutgoingHtlcLeaves idx := htlc.HtlcIndex return leaves[idx].SecondLevelLeaf })(auxLeaves) + auxLeaf = fn.FlattenOption(leaf) timeoutTx, err := CreateHtlcTimeoutTx( chanType, isLocalInitiator, op, outputAmt, htlc.Timeout, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, - fn.FlattenOption(auxLeaf), + keyRing.ToLocalKey, auxLeaf, ) if err != nil { return nil, err @@ -5280,7 +5350,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // Make sure there are more signatures left. if i >= len(htlcSigs) { - return nil, fmt.Errorf("not enough HTLC " + + return nil, nil, fmt.Errorf("not enough HTLC " + "signatures") } @@ -5296,7 +5366,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // is valid. sig, err = htlcSigs[i].ToSignature() if err != nil { - return nil, err + return nil, nil, err } htlc.sig = sig @@ -5313,16 +5383,26 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, }) i++ + + // TODO(roasbeef): meld aux six into tlv blob for htlc on disk? + + auxSig := auxHtlcSigs[htlcIndex] + auxVerifyJob := NewAuxVerifyJob( + auxSig, *keyRing, *htlc, + localCommitmentView.customBlob, auxLeaf, + ) + + auxVerifyJobs = append(auxVerifyJobs, auxVerifyJob) } // If we received a number of HTLC signatures that doesn't match our // commitment, we'll return an error now. if len(htlcSigs) != i { - return nil, fmt.Errorf("number of htlc sig mismatch. "+ + return nil, nil, fmt.Errorf("number of htlc sig mismatch. "+ "Expected %v sigs, got %v", i, len(htlcSigs)) } - return verifyJobs, nil + return verifyJobs, auxVerifyJobs, nil } // InvalidCommitSigError is a struct that implements the error interface to @@ -5484,6 +5564,11 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { }), ) + var auxSigBlob fn.Option[tlv.Blob] + if commitSigs.AuxSigBlob != nil { + auxSigBlob = fn.Some(commitSigs.AuxSigBlob) + } + // As an optimization, we'll generate a series of jobs for the worker // pool to verify each of the HTLC signatures presented. Once // generated, we'll submit these jobs to the worker pool. @@ -5491,9 +5576,10 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { if lc.channelState.ChanType.HasLeaseExpiration() { leaseExpiry = lc.channelState.ThawHeight } - verifyJobs, err := genHtlcSigValidationJobs( + verifyJobs, auxVerifyJobs, err := genHtlcSigValidationJobs( lc.channelState, localCommitmentView, keyRing, - commitSigs.HtlcSigs, leaseExpiry, lc.leafStore, + commitSigs.HtlcSigs, leaseExpiry, lc.leafStore, lc.auxSigner, + auxSigBlob, ) if err != nil { return err @@ -5642,6 +5728,17 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error { } } + // Now that we know all the normal sigs are valid, we'll also verify + // the aux jobs, if any exist. + err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error { + return a.VerifySecondLevelSigs( + lc.channelState, localCommitTx, auxVerifyJobs, + ) + }) + if err != nil { + return fmt.Errorf("unable to validate aux sigs: %w", err) + } + // The signature checks out, so we can now add the new commitment to // our local commitment chain. For regular channels, we can just // serialize the ECDSA sig. For taproot channels, we'll serialize the diff --git a/lnwallet/config.go b/lnwallet/config.go index 24961f38edb..425fe15dad1 100644 --- a/lnwallet/config.go +++ b/lnwallet/config.go @@ -67,4 +67,8 @@ type Config struct { // AuxLeafStore is an optional store that can be used to store auxiliary // leaves for certain custom channel types. AuxLeafStore fn.Option[AuxLeafStore] + + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[AuxSigner] } diff --git a/lnwallet/sigpool.go b/lnwallet/sigpool.go index 2424757f937..78e66d94867 100644 --- a/lnwallet/sigpool.go +++ b/lnwallet/sigpool.go @@ -43,6 +43,8 @@ type VerifyJob struct { // HtlcIndex is the index of the HTLC from the PoV of the remote // party's update log. + // + // TODO(roasbeef): remove -- never actually used? HtlcIndex uint64 // Cancel is a channel that should be closed if the caller wishes to diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 98d251e32b7..09567f45647 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -2602,6 +2602,9 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, l.Cfg.AuxLeafStore.WhenSome(func(s AuxLeafStore) { chanOpts = append(chanOpts, WithLeafStore(s)) }) + l.Cfg.AuxSigner.WhenSome(func(s AuxSigner) { + chanOpts = append(chanOpts, WithAuxSigner(s)) + }) // First, we'll obtain a fully signed commitment transaction so we can // pass into it on the chanvalidate package for verification. diff --git a/peer/brontide.go b/peer/brontide.go index 779206038e2..61a12f3404e 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -364,6 +364,10 @@ type Config struct { // leaves for certain custom channel types. AuxLeafStore fn.Option[lnwallet.AuxLeafStore] + // AuxSigner is an optional signer that can be used to sign auxiliary + // leaves for certain custom channel types. + AuxSigner fn.Option[lnwallet.AuxSigner] + // PongBuf is a slice we'll reuse instead of allocating memory on the // heap. Since only reads will occur and no writes, there is no need // for any synchronization primitives. As a result, it's safe to share @@ -902,6 +906,9 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) ( p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) lnChan, err := lnwallet.NewLightningChannel( p.cfg.Signer, dbChan, p.cfg.SigPool, chanOpts..., ) @@ -4030,6 +4037,9 @@ func (p *Brontide) addActiveChannel(c *lnpeer.NewChannel) error { p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) { chanOpts = append(chanOpts, lnwallet.WithLeafStore(s)) }) + p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) { + chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s)) + }) // If not already active, we'll add this channel to the set of active // channels, so we can look it up later easily according to its channel diff --git a/server.go b/server.go index 68a7feb7dce..cbbdc96d467 100644 --- a/server.go +++ b/server.go @@ -1249,6 +1249,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, return &pc.Incoming }, AuxLeafStore: implCfg.AuxLeafStore, + AuxSigner: implCfg.AuxSigner, }, dbs.ChanStateDB) // Select the configuration and funding parameters for Bitcoin. @@ -3913,6 +3914,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, DisallowRouteBlinding: s.cfg.ProtocolOptions.NoRouteBlinding(), Quit: s.quit, AuxLeafStore: s.implCfg.AuxLeafStore, + AuxSigner: s.implCfg.AuxSigner, MsgRouter: s.implCfg.MsgRouter, } From cfd4314f29aeef3802cc2c824c8ae5ff8d2c696c Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Mon, 8 Apr 2024 19:50:05 -0700 Subject: [PATCH 21/35] lnwire: modify PackRecords to prepend new records As is, we can't use PackRecords when some data may already be stored in the ExtraData field. To allow this use case, we add a failing test, then modify `PackRecords` to append the existing raw bytes (prepend the new records). Note that this doesn't 100% solve the problem, as for the stream to be cannonical we need unique IDs/types and also for them to be in ascending order. --- lnwire/extra_bytes.go | 2 +- lnwire/extra_bytes_test.go | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lnwire/extra_bytes.go b/lnwire/extra_bytes.go index 0ebf48f57d0..c8e18c182fb 100644 --- a/lnwire/extra_bytes.go +++ b/lnwire/extra_bytes.go @@ -71,7 +71,7 @@ func (e *ExtraOpaqueData) PackRecords(recordProducers ...tlv.RecordProducer) err return err } - *e = ExtraOpaqueData(extraBytesWriter.Bytes()) + *e = append(extraBytesWriter.Bytes(), *e...) return nil } diff --git a/lnwire/extra_bytes_test.go b/lnwire/extra_bytes_test.go index fd9f28841d4..bc1de8c5768 100644 --- a/lnwire/extra_bytes_test.go +++ b/lnwire/extra_bytes_test.go @@ -151,3 +151,25 @@ func TestExtraOpaqueDataPackUnpackRecords(t *testing.T) { t.Fatalf("type2 not found in typeMap") } } + +// TestPackRecordsPrepend tests that if an ExtraOpaqueData instance already a +// set of opaque bytes, then any records passed in are prepended to the +// existing bytes. +func TestPackRecordsPrepend(t *testing.T) { + t.Parallel() + + chanTypeRecord := tlv.NewPrimitiveRecord[tlv.TlvType1](uint8(2)) + + // Create some opaque data that is already pre-populated with some + // bytes. + existingBytes := bytes.Repeat([]byte{1}, 10) + extraBytes := ExtraOpaqueData(existingBytes) + + // Now we'll attempt to pack the records into the existing bytes. + err := extraBytes.PackRecords(&chanTypeRecord) + require.NoError(t, err) + + // After we've packed the records, the existing bytes should be at the + // very end. + require.True(t, bytes.HasSuffix(extraBytes[:], existingBytes)) +} From 8fb1753a8db9f69f43e6d05bede3ce376f9e3fa3 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 5 Apr 2024 17:54:26 +0200 Subject: [PATCH 22/35] cmd/lncli: move commands and export --- cmd/{lncli => commands}/arg_parse.go | 2 +- cmd/{lncli => commands}/arg_parse_test.go | 2 +- .../autopilotrpc_active.go | 2 +- .../autopilotrpc_default.go | 2 +- cmd/{lncli => commands}/chainrpc_active.go | 2 +- cmd/{lncli => commands}/chainrpc_default.go | 2 +- cmd/{lncli => commands}/cmd_custom.go | 2 +- cmd/{lncli => commands}/cmd_debug.go | 2 +- .../cmd_import_mission_control.go | 2 +- cmd/{lncli => commands}/cmd_invoice.go | 2 +- cmd/{lncli => commands}/cmd_macaroon.go | 2 +- .../cmd_mission_control.go | 2 +- cmd/{lncli => commands}/cmd_open_channel.go | 20 +- cmd/{lncli => commands}/cmd_payments.go | 2 +- cmd/{lncli => commands}/cmd_profile.go | 2 +- cmd/{lncli => commands}/cmd_state.go | 2 +- .../cmd_update_chan_status.go | 2 +- cmd/{lncli => commands}/cmd_version.go | 2 +- cmd/{lncli => commands}/cmd_walletunlocker.go | 2 +- cmd/{lncli => commands}/commands.go | 122 +++- cmd/{lncli => commands}/commands_test.go | 54 +- cmd/{lncli => commands}/devrpc_active.go | 2 +- cmd/{lncli => commands}/devrpc_default.go | 2 +- cmd/{lncli => commands}/invoicesrpc_active.go | 2 +- .../invoicesrpc_default.go | 2 +- cmd/{lncli => commands}/macaroon_jar.go | 2 +- cmd/{lncli => commands}/macaroon_jar_test.go | 2 +- cmd/commands/main.go | 594 ++++++++++++++++++ cmd/{lncli => commands}/neutrino_active.go | 2 +- cmd/{lncli => commands}/neutrino_default.go | 2 +- cmd/{lncli => commands}/peersrpc_active.go | 2 +- cmd/{lncli => commands}/peersrpc_default.go | 2 +- cmd/{lncli => commands}/profile.go | 2 +- cmd/{lncli => commands}/routerrpc.go | 2 +- cmd/{lncli => commands}/types.go | 2 +- cmd/{lncli => commands}/walletrpc_active.go | 2 +- cmd/{lncli => commands}/walletrpc_default.go | 2 +- cmd/{lncli => commands}/walletrpc_types.go | 2 +- cmd/{lncli => commands}/watchtower_active.go | 2 +- cmd/{lncli => commands}/watchtower_default.go | 2 +- cmd/{lncli => commands}/wtclient.go | 2 +- cmd/lncli/main.go | 587 +---------------- funding/manager.go | 3 + lnrpc/lightning.pb.go | 34 +- lnrpc/lightning.proto | 4 + lnrpc/lightning.swagger.json | 8 + lnwallet/wallet.go | 2 + rpcserver.go | 7 + 48 files changed, 844 insertions(+), 665 deletions(-) rename cmd/{lncli => commands}/arg_parse.go (98%) rename cmd/{lncli => commands}/arg_parse_test.go (99%) rename cmd/{lncli => commands}/autopilotrpc_active.go (99%) rename cmd/{lncli => commands}/autopilotrpc_default.go (92%) rename cmd/{lncli => commands}/chainrpc_active.go (99%) rename cmd/{lncli => commands}/chainrpc_default.go (91%) rename cmd/{lncli => commands}/cmd_custom.go (98%) rename cmd/{lncli => commands}/cmd_debug.go (99%) rename cmd/{lncli => commands}/cmd_import_mission_control.go (99%) rename cmd/{lncli => commands}/cmd_invoice.go (99%) rename cmd/{lncli => commands}/cmd_macaroon.go (99%) rename cmd/{lncli => commands}/cmd_mission_control.go (99%) rename cmd/{lncli => commands}/cmd_open_channel.go (99%) rename cmd/{lncli => commands}/cmd_payments.go (99%) rename cmd/{lncli => commands}/cmd_profile.go (99%) rename cmd/{lncli => commands}/cmd_state.go (98%) rename cmd/{lncli => commands}/cmd_update_chan_status.go (99%) rename cmd/{lncli => commands}/cmd_version.go (98%) rename cmd/{lncli => commands}/cmd_walletunlocker.go (99%) rename cmd/{lncli => commands}/commands.go (97%) rename cmd/{lncli => commands}/commands_test.go (71%) rename cmd/{lncli => commands}/devrpc_active.go (98%) rename cmd/{lncli => commands}/devrpc_default.go (90%) rename cmd/{lncli => commands}/invoicesrpc_active.go (99%) rename cmd/{lncli => commands}/invoicesrpc_default.go (92%) rename cmd/{lncli => commands}/macaroon_jar.go (99%) rename cmd/{lncli => commands}/macaroon_jar_test.go (99%) create mode 100644 cmd/commands/main.go rename cmd/{lncli => commands}/neutrino_active.go (99%) rename cmd/{lncli => commands}/neutrino_default.go (92%) rename cmd/{lncli => commands}/peersrpc_active.go (99%) rename cmd/{lncli => commands}/peersrpc_default.go (91%) rename cmd/{lncli => commands}/profile.go (99%) rename cmd/{lncli => commands}/routerrpc.go (95%) rename cmd/{lncli => commands}/types.go (99%) rename cmd/{lncli => commands}/walletrpc_active.go (99%) rename cmd/{lncli => commands}/walletrpc_default.go (91%) rename cmd/{lncli => commands}/walletrpc_types.go (98%) rename cmd/{lncli => commands}/watchtower_active.go (98%) rename cmd/{lncli => commands}/watchtower_default.go (92%) rename cmd/{lncli => commands}/wtclient.go (99%) diff --git a/cmd/lncli/arg_parse.go b/cmd/commands/arg_parse.go similarity index 98% rename from cmd/lncli/arg_parse.go rename to cmd/commands/arg_parse.go index 49d165d5569..045f35509a2 100644 --- a/cmd/lncli/arg_parse.go +++ b/cmd/commands/arg_parse.go @@ -1,4 +1,4 @@ -package main +package commands import ( "regexp" diff --git a/cmd/lncli/arg_parse_test.go b/cmd/commands/arg_parse_test.go similarity index 99% rename from cmd/lncli/arg_parse_test.go rename to cmd/commands/arg_parse_test.go index 571292d2c66..ead411fe61b 100644 --- a/cmd/lncli/arg_parse_test.go +++ b/cmd/commands/arg_parse_test.go @@ -1,4 +1,4 @@ -package main +package commands import ( "testing" diff --git a/cmd/lncli/autopilotrpc_active.go b/cmd/commands/autopilotrpc_active.go similarity index 99% rename from cmd/lncli/autopilotrpc_active.go rename to cmd/commands/autopilotrpc_active.go index 961e8599473..212ef45797d 100644 --- a/cmd/lncli/autopilotrpc_active.go +++ b/cmd/commands/autopilotrpc_active.go @@ -1,7 +1,7 @@ //go:build autopilotrpc // +build autopilotrpc -package main +package commands import ( "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc" diff --git a/cmd/lncli/autopilotrpc_default.go b/cmd/commands/autopilotrpc_default.go similarity index 92% rename from cmd/lncli/autopilotrpc_default.go rename to cmd/commands/autopilotrpc_default.go index 7fb88521709..393b6f124f4 100644 --- a/cmd/lncli/autopilotrpc_default.go +++ b/cmd/commands/autopilotrpc_default.go @@ -1,7 +1,7 @@ //go:build !autopilotrpc // +build !autopilotrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/chainrpc_active.go b/cmd/commands/chainrpc_active.go similarity index 99% rename from cmd/lncli/chainrpc_active.go rename to cmd/commands/chainrpc_active.go index 48946e0d5d3..0f1f8b61210 100644 --- a/cmd/lncli/chainrpc_active.go +++ b/cmd/commands/chainrpc_active.go @@ -1,7 +1,7 @@ //go:build chainrpc // +build chainrpc -package main +package commands import ( "bytes" diff --git a/cmd/lncli/chainrpc_default.go b/cmd/commands/chainrpc_default.go similarity index 91% rename from cmd/lncli/chainrpc_default.go rename to cmd/commands/chainrpc_default.go index fa1ea99e2c9..28440a839e6 100644 --- a/cmd/lncli/chainrpc_default.go +++ b/cmd/commands/chainrpc_default.go @@ -1,7 +1,7 @@ //go:build !chainrpc // +build !chainrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/cmd_custom.go b/cmd/commands/cmd_custom.go similarity index 98% rename from cmd/lncli/cmd_custom.go rename to cmd/commands/cmd_custom.go index 7ff5d8a71e3..728d70bd39a 100644 --- a/cmd/lncli/cmd_custom.go +++ b/cmd/commands/cmd_custom.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" diff --git a/cmd/lncli/cmd_debug.go b/cmd/commands/cmd_debug.go similarity index 99% rename from cmd/lncli/cmd_debug.go rename to cmd/commands/cmd_debug.go index 758bff576de..37024f5ecf7 100644 --- a/cmd/lncli/cmd_debug.go +++ b/cmd/commands/cmd_debug.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/lncli/cmd_import_mission_control.go b/cmd/commands/cmd_import_mission_control.go similarity index 99% rename from cmd/lncli/cmd_import_mission_control.go rename to cmd/commands/cmd_import_mission_control.go index 23935753d29..b4aa19931f3 100644 --- a/cmd/lncli/cmd_import_mission_control.go +++ b/cmd/commands/cmd_import_mission_control.go @@ -1,4 +1,4 @@ -package main +package commands import ( "context" diff --git a/cmd/lncli/cmd_invoice.go b/cmd/commands/cmd_invoice.go similarity index 99% rename from cmd/lncli/cmd_invoice.go rename to cmd/commands/cmd_invoice.go index 4c60294caad..785f6a6abe9 100644 --- a/cmd/lncli/cmd_invoice.go +++ b/cmd/commands/cmd_invoice.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" diff --git a/cmd/lncli/cmd_macaroon.go b/cmd/commands/cmd_macaroon.go similarity index 99% rename from cmd/lncli/cmd_macaroon.go rename to cmd/commands/cmd_macaroon.go index 54e03057ab9..ed299182f42 100644 --- a/cmd/lncli/cmd_macaroon.go +++ b/cmd/commands/cmd_macaroon.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/lncli/cmd_mission_control.go b/cmd/commands/cmd_mission_control.go similarity index 99% rename from cmd/lncli/cmd_mission_control.go rename to cmd/commands/cmd_mission_control.go index 323acdff6d2..da56339a45f 100644 --- a/cmd/lncli/cmd_mission_control.go +++ b/cmd/commands/cmd_mission_control.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/lncli/cmd_open_channel.go b/cmd/commands/cmd_open_channel.go similarity index 99% rename from cmd/lncli/cmd_open_channel.go rename to cmd/commands/cmd_open_channel.go index 74cb1668afb..0583b1c062b 100644 --- a/cmd/lncli/cmd_open_channel.go +++ b/cmd/commands/cmd_open_channel.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" @@ -65,7 +65,8 @@ Signed base64 encoded PSBT or hex encoded raw wire TX (or path to file): ` ) // TODO(roasbeef): change default number of confirmations. -var openChannelCommand = cli.Command{ + +var OpenChannelCommand = cli.Command{ Name: "openchannel", Category: "Channels", Usage: "Open a channel to a node or an existing peer.", @@ -284,10 +285,14 @@ var openChannelCommand = cli.Command{ allowed length is 500 characters`, }, }, - Action: actionDecorator(openChannel), + Action: actionDecorator(func(c *cli.Context) error { + return OpenChannel(c, nil) + }), } -func openChannel(ctx *cli.Context) error { +func OpenChannel(ctx *cli.Context, + reqDecorator RequestDecorator[*lnrpc.OpenChannelRequest]) error { + // TODO(roasbeef): add deadline to context ctxc := getContext() client, cleanUp := getClient(ctx) @@ -451,6 +456,13 @@ func openChannel(ctx *cli.Context) error { return fmt.Errorf("unsupported channel type %v", channelType) } + if reqDecorator != nil { + err = reqDecorator(ctx, req) + if err != nil { + return err + } + } + // PSBT funding is a more involved, interactive process that is too // large to also fit into this already long function. if ctx.Bool("psbt") { diff --git a/cmd/lncli/cmd_payments.go b/cmd/commands/cmd_payments.go similarity index 99% rename from cmd/lncli/cmd_payments.go rename to cmd/commands/cmd_payments.go index 550bb8f3e7b..87316ef4935 100644 --- a/cmd/lncli/cmd_payments.go +++ b/cmd/commands/cmd_payments.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/lncli/cmd_profile.go b/cmd/commands/cmd_profile.go similarity index 99% rename from cmd/lncli/cmd_profile.go rename to cmd/commands/cmd_profile.go index 21666be9645..45bd6904199 100644 --- a/cmd/lncli/cmd_profile.go +++ b/cmd/commands/cmd_profile.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/lncli/cmd_state.go b/cmd/commands/cmd_state.go similarity index 98% rename from cmd/lncli/cmd_state.go rename to cmd/commands/cmd_state.go index afca13e9d6e..c2522b721be 100644 --- a/cmd/lncli/cmd_state.go +++ b/cmd/commands/cmd_state.go @@ -1,4 +1,4 @@ -package main +package commands import ( "context" diff --git a/cmd/lncli/cmd_update_chan_status.go b/cmd/commands/cmd_update_chan_status.go similarity index 99% rename from cmd/lncli/cmd_update_chan_status.go rename to cmd/commands/cmd_update_chan_status.go index 23c22f0b166..3525f7c5c60 100644 --- a/cmd/lncli/cmd_update_chan_status.go +++ b/cmd/commands/cmd_update_chan_status.go @@ -1,4 +1,4 @@ -package main +package commands import ( "errors" diff --git a/cmd/lncli/cmd_version.go b/cmd/commands/cmd_version.go similarity index 98% rename from cmd/lncli/cmd_version.go rename to cmd/commands/cmd_version.go index 99cc7299539..9e7a2b0775b 100644 --- a/cmd/lncli/cmd_version.go +++ b/cmd/commands/cmd_version.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/lncli/cmd_walletunlocker.go b/cmd/commands/cmd_walletunlocker.go similarity index 99% rename from cmd/lncli/cmd_walletunlocker.go rename to cmd/commands/cmd_walletunlocker.go index 9227d10d4ff..8a9393adcd7 100644 --- a/cmd/lncli/cmd_walletunlocker.go +++ b/cmd/commands/cmd_walletunlocker.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bufio" diff --git a/cmd/lncli/commands.go b/cmd/commands/commands.go similarity index 97% rename from cmd/lncli/commands.go rename to cmd/commands/commands.go index 5ffca5c7393..3746ddaace3 100644 --- a/cmd/lncli/commands.go +++ b/cmd/commands/commands.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bufio" @@ -12,6 +12,7 @@ import ( "io/ioutil" "math" "os" + "regexp" "strconv" "strings" "sync" @@ -42,8 +43,44 @@ const ( defaultUtxoMinConf = 1 ) -var errBadChanPoint = errors.New("expecting chan_point to be in format of: " + - "txid:index") +var ( + errBadChanPoint = errors.New( + "expecting chan_point to be in format of: txid:index", + ) + + customDataPattern = regexp.MustCompile( + `"custom_channel_data":\s*"([0-9a-z]+)"`, + ) +) + +func replaceCustomData(jsonBytes []byte) ([]byte, error) { + if customDataPattern.Match(jsonBytes) { + jsonBytes = customDataPattern.ReplaceAllFunc( + jsonBytes, func(match []byte) []byte { + encoded := customDataPattern.FindStringSubmatch( + string(match), + )[1] + decoded, err := hex.DecodeString(encoded) + if err != nil { + return match + } + + return []byte("\"custom_channel_data\":" + + string(decoded)) + }, + ) + + var buf bytes.Buffer + err := json.Indent(&buf, jsonBytes, "", " ") + if err != nil { + return nil, err + } + + jsonBytes = buf.Bytes() + } + + return jsonBytes, nil +} func getContext() context.Context { shutdownInterceptor, err := signal.Intercept() @@ -67,7 +104,7 @@ func printJSON(resp interface{}) { } var out bytes.Buffer - json.Indent(&out, b, "", "\t") + json.Indent(&out, b, "", " ") out.WriteString("\n") out.WriteTo(os.Stdout) } @@ -79,7 +116,13 @@ func printRespJSON(resp proto.Message) { return } - fmt.Printf("%s\n", jsonBytes) + jsonBytesReplaced, err := replaceCustomData(jsonBytes) + if err != nil { + fmt.Println("unable to replace custom data: ", err) + jsonBytesReplaced = jsonBytes + } + + fmt.Printf("%s\n", jsonBytesReplaced) } // actionDecorator is used to add additional information and error handling @@ -1553,7 +1596,11 @@ func pendingChannels(ctx *cli.Context) error { return nil } -var listChannelsCommand = cli.Command{ +type RequestDecorator[T proto.Message] func(*cli.Context, T) error + +type ResponseDecorator[T proto.Message] func(*cli.Context, T) error + +var ListChannelsCommand = cli.Command{ Name: "listchannels", Category: "Channels", Usage: "List all open channels.", @@ -1586,35 +1633,14 @@ var listChannelsCommand = cli.Command{ "order to improve performance", }, }, - Action: actionDecorator(listChannels), + Action: actionDecorator(func(c *cli.Context) error { + return ListChannels(c, nil) + }), } -var listAliasesCommand = cli.Command{ - Name: "listaliases", - Category: "Channels", - Usage: "List all aliases.", - Flags: []cli.Flag{}, - Action: actionDecorator(listaliases), -} +func ListChannels(ctx *cli.Context, + respDecorator ResponseDecorator[*lnrpc.ListChannelsResponse]) error { -func listaliases(ctx *cli.Context) error { - ctxc := getContext() - client, cleanUp := getClient(ctx) - defer cleanUp() - - req := &lnrpc.ListAliasesRequest{} - - resp, err := client.ListAliases(ctxc, req) - if err != nil { - return err - } - - printRespJSON(resp) - - return nil -} - -func listChannels(ctx *cli.Context) error { ctxc := getContext() client, cleanUp := getClient(ctx) defer cleanUp() @@ -1651,6 +1677,38 @@ func listChannels(ctx *cli.Context) error { return err } + if respDecorator != nil { + err = respDecorator(ctx, resp) + if err != nil { + return err + } + } + + printRespJSON(resp) + + return nil +} + +var listAliasesCommand = cli.Command{ + Name: "listaliases", + Category: "Channels", + Usage: "List all aliases.", + Flags: []cli.Flag{}, + Action: actionDecorator(listAliases), +} + +func listAliases(ctx *cli.Context) error { + ctxc := getContext() + client, cleanUp := getClient(ctx) + defer cleanUp() + + req := &lnrpc.ListAliasesRequest{} + + resp, err := client.ListAliases(ctxc, req) + if err != nil { + return err + } + printRespJSON(resp) return nil diff --git a/cmd/lncli/commands_test.go b/cmd/commands/commands_test.go similarity index 71% rename from cmd/lncli/commands_test.go rename to cmd/commands/commands_test.go index a1f967561e4..76ed624eb85 100644 --- a/cmd/lncli/commands_test.go +++ b/cmd/commands/commands_test.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" @@ -120,3 +120,55 @@ func TestParseTimeLockDelta(t *testing.T) { } } } + +func TestReplaceCustomData(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + data string + replaceData string + expected string + }{ + { + name: "no replacement necessary", + data: "foo", + expected: "foo", + }, + { + name: "valid json with replacement", + data: "{\"foo\":\"bar\",\"custom_channel_data\":\"" + + hex.EncodeToString([]byte( + "{\"bar\":\"baz\"}", + )) + "\"}", + expected: `{ + "foo": "bar", + "custom_channel_data": { + "bar": "baz" + } +}`, + }, + { + name: "valid json with replacement and space", + data: "{\"foo\":\"bar\",\"custom_channel_data\": \"" + + hex.EncodeToString([]byte( + "{\"bar\":\"baz\"}", + )) + "\"}", + expected: `{ + "foo": "bar", + "custom_channel_data": { + "bar": "baz" + } +}`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := replaceCustomData([]byte(tc.data)) + require.NoError(t, err) + + require.Equal(t, tc.expected, string(result)) + }) + } +} diff --git a/cmd/lncli/devrpc_active.go b/cmd/commands/devrpc_active.go similarity index 98% rename from cmd/lncli/devrpc_active.go rename to cmd/commands/devrpc_active.go index da3f08a97d7..8d1960e461b 100644 --- a/cmd/lncli/devrpc_active.go +++ b/cmd/commands/devrpc_active.go @@ -1,7 +1,7 @@ //go:build dev // +build dev -package main +package commands import ( "fmt" diff --git a/cmd/lncli/devrpc_default.go b/cmd/commands/devrpc_default.go similarity index 90% rename from cmd/lncli/devrpc_default.go rename to cmd/commands/devrpc_default.go index b9362cb421f..1c5b482c324 100644 --- a/cmd/lncli/devrpc_default.go +++ b/cmd/commands/devrpc_default.go @@ -1,7 +1,7 @@ //go:build !dev // +build !dev -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/invoicesrpc_active.go b/cmd/commands/invoicesrpc_active.go similarity index 99% rename from cmd/lncli/invoicesrpc_active.go rename to cmd/commands/invoicesrpc_active.go index 2ce90069563..d0dbc011b0e 100644 --- a/cmd/lncli/invoicesrpc_active.go +++ b/cmd/commands/invoicesrpc_active.go @@ -1,7 +1,7 @@ //go:build invoicesrpc // +build invoicesrpc -package main +package commands import ( "encoding/hex" diff --git a/cmd/lncli/invoicesrpc_default.go b/cmd/commands/invoicesrpc_default.go similarity index 92% rename from cmd/lncli/invoicesrpc_default.go rename to cmd/commands/invoicesrpc_default.go index cca3c14e9f6..e925e55d694 100644 --- a/cmd/lncli/invoicesrpc_default.go +++ b/cmd/commands/invoicesrpc_default.go @@ -1,7 +1,7 @@ //go:build !invoicesrpc // +build !invoicesrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/macaroon_jar.go b/cmd/commands/macaroon_jar.go similarity index 99% rename from cmd/lncli/macaroon_jar.go rename to cmd/commands/macaroon_jar.go index f54f29a26c1..d3a4345b0ba 100644 --- a/cmd/lncli/macaroon_jar.go +++ b/cmd/commands/macaroon_jar.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/base64" diff --git a/cmd/lncli/macaroon_jar_test.go b/cmd/commands/macaroon_jar_test.go similarity index 99% rename from cmd/lncli/macaroon_jar_test.go rename to cmd/commands/macaroon_jar_test.go index 8e1d1c6bd41..6d76dce848b 100644 --- a/cmd/lncli/macaroon_jar_test.go +++ b/cmd/commands/macaroon_jar_test.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" diff --git a/cmd/commands/main.go b/cmd/commands/main.go new file mode 100644 index 00000000000..4aed13d7cd6 --- /dev/null +++ b/cmd/commands/main.go @@ -0,0 +1,594 @@ +// Copyright (c) 2013-2017 The btcsuite developers +// Copyright (c) 2015-2016 The Decred developers +// Copyright (C) 2015-2022 The Lightning Network Developers + +package commands + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightningnetwork/lnd" + "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/tor" + "github.com/urfave/cli" + "golang.org/x/term" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" +) + +const ( + defaultDataDir = "data" + defaultChainSubDir = "chain" + defaultTLSCertFilename = "tls.cert" + defaultMacaroonFilename = "admin.macaroon" + defaultRPCPort = "10009" + defaultRPCHostPort = "localhost:" + defaultRPCPort + + envVarRPCServer = "LNCLI_RPCSERVER" + envVarLNDDir = "LNCLI_LNDDIR" + envVarSOCKSProxy = "LNCLI_SOCKSPROXY" + envVarTLSCertPath = "LNCLI_TLSCERTPATH" + envVarChain = "LNCLI_CHAIN" + envVarNetwork = "LNCLI_NETWORK" + envVarMacaroonPath = "LNCLI_MACAROONPATH" + envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT" + envVarMacaroonIP = "LNCLI_MACAROONIP" + envVarProfile = "LNCLI_PROFILE" + envVarMacFromJar = "LNCLI_MACFROMJAR" +) + +var ( + DefaultLndDir = btcutil.AppDataDir("lnd", false) + defaultTLSCertPath = filepath.Join(DefaultLndDir, defaultTLSCertFilename) + + // maxMsgRecvSize is the largest message our client will receive. We + // set this to 200MiB atm. + maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize) +) + +func fatal(err error) { + fmt.Fprintf(os.Stderr, "[lncli] %v\n", err) + os.Exit(1) +} + +func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) { + conn := getClientConn(ctx, true) + + cleanUp := func() { + conn.Close() + } + + return lnrpc.NewWalletUnlockerClient(conn), cleanUp +} + +func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) { + conn := getClientConn(ctx, true) + + cleanUp := func() { + conn.Close() + } + + return lnrpc.NewStateClient(conn), cleanUp +} + +func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) { + conn := getClientConn(ctx, false) + + cleanUp := func() { + conn.Close() + } + + return lnrpc.NewLightningClient(conn), cleanUp +} + +func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn { + // First, we'll get the selected stored profile or an ephemeral one + // created from the global options in the CLI context. + profile, err := getGlobalOptions(ctx, skipMacaroons) + if err != nil { + fatal(fmt.Errorf("could not load global options: %w", err)) + } + + // Create a dial options array. + opts := []grpc.DialOption{ + grpc.WithUnaryInterceptor( + addMetadataUnaryInterceptor(profile.Metadata), + ), + grpc.WithStreamInterceptor( + addMetaDataStreamInterceptor(profile.Metadata), + ), + } + + if profile.Insecure { + opts = append(opts, grpc.WithInsecure()) + } else { + // Load the specified TLS certificate. + certPool, err := profile.cert() + if err != nil { + fatal(fmt.Errorf("could not create cert pool: %w", err)) + } + + // Build transport credentials from the certificate pool. If + // there is no certificate pool, we expect the server to use a + // non-self-signed certificate such as a certificate obtained + // from Let's Encrypt. + var creds credentials.TransportCredentials + if certPool != nil { + creds = credentials.NewClientTLSFromCert(certPool, "") + } else { + // Fallback to the system pool. Using an empty tls + // config is an alternative to x509.SystemCertPool(). + // That call is not supported on Windows. + creds = credentials.NewTLS(&tls.Config{}) + } + + opts = append(opts, grpc.WithTransportCredentials(creds)) + } + + // Only process macaroon credentials if --no-macaroons isn't set and + // if we're not skipping macaroon processing. + if !profile.NoMacaroons && !skipMacaroons { + // Find out which macaroon to load. + macName := profile.Macaroons.Default + if ctx.GlobalIsSet("macfromjar") { + macName = ctx.GlobalString("macfromjar") + } + var macEntry *macaroonEntry + for _, entry := range profile.Macaroons.Jar { + if entry.Name == macName { + macEntry = entry + break + } + } + if macEntry == nil { + fatal(fmt.Errorf("macaroon with name '%s' not found "+ + "in profile", macName)) + } + + // Get and possibly decrypt the specified macaroon. + // + // TODO(guggero): Make it possible to cache the password so we + // don't need to ask for it every time. + mac, err := macEntry.loadMacaroon(readPassword) + if err != nil { + fatal(fmt.Errorf("could not load macaroon: %w", err)) + } + + macConstraints := []macaroons.Constraint{ + // We add a time-based constraint to prevent replay of + // the macaroon. It's good for 60 seconds by default to + // make up for any discrepancy between client and server + // clocks, but leaking the macaroon before it becomes + // invalid makes it possible for an attacker to reuse + // the macaroon. In addition, the validity time of the + // macaroon is extended by the time the server clock is + // behind the client clock, or shortened by the time the + // server clock is ahead of the client clock (or invalid + // altogether if, in the latter case, this time is more + // than 60 seconds). + // TODO(aakselrod): add better anti-replay protection. + macaroons.TimeoutConstraint(profile.Macaroons.Timeout), + + // Lock macaroon down to a specific IP address. + macaroons.IPLockConstraint(profile.Macaroons.IP), + + // ... Add more constraints if needed. + } + + // Apply constraints to the macaroon. + constrainedMac, err := macaroons.AddConstraints( + mac, macConstraints..., + ) + if err != nil { + fatal(err) + } + + // Now we append the macaroon credentials to the dial options. + cred, err := macaroons.NewMacaroonCredential(constrainedMac) + if err != nil { + fatal(fmt.Errorf("error cloning mac: %w", err)) + } + opts = append(opts, grpc.WithPerRPCCredentials(cred)) + } + + // If a socksproxy server is specified we use a tor dialer + // to connect to the grpc server. + if ctx.GlobalIsSet("socksproxy") { + socksProxy := ctx.GlobalString("socksproxy") + torDialer := func(_ context.Context, addr string) (net.Conn, + error) { + + return tor.Dial( + addr, socksProxy, false, false, + tor.DefaultConnTimeout, + ) + } + opts = append(opts, grpc.WithContextDialer(torDialer)) + } else { + // We need to use a custom dialer so we can also connect to + // unix sockets and not just TCP addresses. + genericDialer := lncfg.ClientAddressDialer(defaultRPCPort) + opts = append(opts, grpc.WithContextDialer(genericDialer)) + } + + opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize)) + + conn, err := grpc.Dial(profile.RPCServer, opts...) + if err != nil { + fatal(fmt.Errorf("unable to connect to RPC server: %w", err)) + } + + return conn +} + +// addMetadataUnaryInterceptor returns a grpc client side interceptor that +// appends any key-value metadata strings to the outgoing context of a grpc +// unary call. +func addMetadataUnaryInterceptor( + md map[string]string) grpc.UnaryClientInterceptor { + + return func(ctx context.Context, method string, req, reply interface{}, + cc *grpc.ClientConn, invoker grpc.UnaryInvoker, + opts ...grpc.CallOption) error { + + outCtx := contextWithMetadata(ctx, md) + return invoker(outCtx, method, req, reply, cc, opts...) + } +} + +// addMetaDataStreamInterceptor returns a grpc client side interceptor that +// appends any key-value metadata strings to the outgoing context of a grpc +// stream call. +func addMetaDataStreamInterceptor( + md map[string]string) grpc.StreamClientInterceptor { + + return func(ctx context.Context, desc *grpc.StreamDesc, + cc *grpc.ClientConn, method string, streamer grpc.Streamer, + opts ...grpc.CallOption) (grpc.ClientStream, error) { + + outCtx := contextWithMetadata(ctx, md) + return streamer(outCtx, desc, cc, method, opts...) + } +} + +// contextWithMetaData appends the given metadata key-value pairs to the given +// context. +func contextWithMetadata(ctx context.Context, + md map[string]string) context.Context { + + kvPairs := make([]string, 0, 2*len(md)) + for k, v := range md { + kvPairs = append(kvPairs, k, v) + } + + return metadata.AppendToOutgoingContext(ctx, kvPairs...) +} + +// extractPathArgs parses the TLS certificate and macaroon paths from the +// command. +func extractPathArgs(ctx *cli.Context) (string, string, error) { + network := strings.ToLower(ctx.GlobalString("network")) + switch network { + case "mainnet", "testnet", "regtest", "simnet", "signet": + default: + return "", "", fmt.Errorf("unknown network: %v", network) + } + + // We'll now fetch the lnddir so we can make a decision on how to + // properly read the macaroons (if needed) and also the cert. This will + // either be the default, or will have been overwritten by the end + // user. + lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir")) + + // If the macaroon path as been manually provided, then we'll only + // target the specified file. + var macPath string + if ctx.GlobalString("macaroonpath") != "" { + macPath = lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath")) + } else { + // Otherwise, we'll go into the path: + // lnddir/data/chain// in order to fetch the + // macaroon that we need. + macPath = filepath.Join( + lndDir, defaultDataDir, defaultChainSubDir, + lnd.BitcoinChainName, network, defaultMacaroonFilename, + ) + } + + tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath")) + + // If a custom lnd directory was set, we'll also check if custom paths + // for the TLS cert and macaroon file were set as well. If not, we'll + // override their paths so they can be found within the custom lnd + // directory set. This allows us to set a custom lnd directory, along + // with custom paths to the TLS cert and macaroon file. + if lndDir != DefaultLndDir { + tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename) + } + + return tlsCertPath, macPath, nil +} + +// checkNotBothSet accepts two flag names, a and b, and checks that only flag a +// or flag b can be set, but not both. It returns the name of the flag or an +// error. +func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) { + if ctx.IsSet(a) && ctx.IsSet(b) { + return "", fmt.Errorf( + "either %s or %s should be set, but not both", a, b, + ) + } + + if ctx.IsSet(a) { + return a, nil + } + + return b, nil +} + +func Main() { + app := cli.NewApp() + app.Name = "lncli" + app.Version = build.Version() + " commit=" + build.Commit + app.Usage = "control plane for your Lightning Network Daemon (lnd)" + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "rpcserver", + Value: defaultRPCHostPort, + Usage: "The host:port of LN daemon.", + EnvVar: envVarRPCServer, + }, + cli.StringFlag{ + Name: "lnddir", + Value: DefaultLndDir, + Usage: "The path to lnd's base directory.", + TakesFile: true, + EnvVar: envVarLNDDir, + }, + cli.StringFlag{ + Name: "socksproxy", + Usage: "The host:port of a SOCKS proxy through " + + "which all connections to the LN " + + "daemon will be established over.", + EnvVar: envVarSOCKSProxy, + }, + cli.StringFlag{ + Name: "tlscertpath", + Value: defaultTLSCertPath, + Usage: "The path to lnd's TLS certificate.", + TakesFile: true, + EnvVar: envVarTLSCertPath, + }, + cli.StringFlag{ + Name: "chain, c", + Usage: "The chain lnd is running on, e.g. bitcoin.", + Value: "bitcoin", + EnvVar: envVarChain, + }, + cli.StringFlag{ + Name: "network, n", + Usage: "The network lnd is running on, e.g. mainnet, " + + "testnet, etc.", + Value: "mainnet", + EnvVar: envVarNetwork, + }, + cli.BoolFlag{ + Name: "no-macaroons", + Usage: "Disable macaroon authentication.", + }, + cli.StringFlag{ + Name: "macaroonpath", + Usage: "The path to macaroon file.", + TakesFile: true, + EnvVar: envVarMacaroonPath, + }, + cli.Int64Flag{ + Name: "macaroontimeout", + Value: 60, + Usage: "Anti-replay macaroon validity time in " + + "seconds.", + EnvVar: envVarMacaroonTimeout, + }, + cli.StringFlag{ + Name: "macaroonip", + Usage: "If set, lock macaroon to specific IP address.", + EnvVar: envVarMacaroonIP, + }, + cli.StringFlag{ + Name: "profile, p", + Usage: "Instead of reading settings from command " + + "line parameters or using the default " + + "profile, use a specific profile. If " + + "a default profile is set, this flag can be " + + "set to an empty string to disable reading " + + "values from the profiles file.", + EnvVar: envVarProfile, + }, + cli.StringFlag{ + Name: "macfromjar", + Usage: "Use this macaroon from the profile's " + + "macaroon jar instead of the default one. " + + "Can only be used if profiles are defined.", + EnvVar: envVarMacFromJar, + }, + cli.StringSliceFlag{ + Name: "metadata", + Usage: "This flag can be used to specify a key-value " + + "pair that should be appended to the " + + "outgoing context before the request is sent " + + "to lnd. This flag may be specified multiple " + + "times. The format is: \"key:value\".", + }, + cli.BoolFlag{ + Name: "insecure", + Usage: "Connect to the rpc server without TLS " + + "authentication", + Hidden: true, + }, + } + app.Commands = []cli.Command{ + createCommand, + createWatchOnlyCommand, + unlockCommand, + changePasswordCommand, + newAddressCommand, + estimateFeeCommand, + sendManyCommand, + sendCoinsCommand, + listUnspentCommand, + connectCommand, + disconnectCommand, + OpenChannelCommand, + batchOpenChannelCommand, + closeChannelCommand, + closeAllChannelsCommand, + abandonChannelCommand, + listPeersCommand, + walletBalanceCommand, + channelBalanceCommand, + getInfoCommand, + getDebugInfoCommand, + encryptDebugPackageCommand, + decryptDebugPackageCommand, + getRecoveryInfoCommand, + pendingChannelsCommand, + sendPaymentCommand, + payInvoiceCommand, + sendToRouteCommand, + addInvoiceCommand, + lookupInvoiceCommand, + listInvoicesCommand, + ListChannelsCommand, + closedChannelsCommand, + listPaymentsCommand, + describeGraphCommand, + getNodeMetricsCommand, + getChanInfoCommand, + getNodeInfoCommand, + queryRoutesCommand, + getNetworkInfoCommand, + debugLevelCommand, + decodePayReqCommand, + listChainTxnsCommand, + stopCommand, + signMessageCommand, + verifyMessageCommand, + feeReportCommand, + updateChannelPolicyCommand, + forwardingHistoryCommand, + exportChanBackupCommand, + verifyChanBackupCommand, + restoreChanBackupCommand, + bakeMacaroonCommand, + listMacaroonIDsCommand, + deleteMacaroonIDCommand, + listPermissionsCommand, + printMacaroonCommand, + constrainMacaroonCommand, + trackPaymentCommand, + versionCommand, + profileSubCommand, + getStateCommand, + deletePaymentsCommand, + sendCustomCommand, + subscribeCustomCommand, + fishCompletionCommand, + listAliasesCommand, + estimateRouteFeeCommand, + generateManPageCommand, + } + + // Add any extra commands determined by build flags. + app.Commands = append(app.Commands, autopilotCommands()...) + app.Commands = append(app.Commands, invoicesCommands()...) + app.Commands = append(app.Commands, neutrinoCommands()...) + app.Commands = append(app.Commands, routerCommands()...) + app.Commands = append(app.Commands, walletCommands()...) + app.Commands = append(app.Commands, watchtowerCommands()...) + app.Commands = append(app.Commands, wtclientCommands()...) + app.Commands = append(app.Commands, devCommands()...) + app.Commands = append(app.Commands, peersCommands()...) + app.Commands = append(app.Commands, chainCommands()...) + + if err := app.Run(os.Args); err != nil { + fatal(err) + } +} + +// readPassword reads a password from the terminal. This requires there to be an +// actual TTY so passing in a password from stdin won't work. +func readPassword(text string) ([]byte, error) { + fmt.Print(text) + + // The variable syscall.Stdin is of a different type in the Windows API + // that's why we need the explicit cast. And of course the linter + // doesn't like it either. + pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert + fmt.Println() + return pw, err +} + +// networkParams parses the global network flag into a chaincfg.Params. +func networkParams(ctx *cli.Context) (*chaincfg.Params, error) { + network := strings.ToLower(ctx.GlobalString("network")) + switch network { + case "mainnet": + return &chaincfg.MainNetParams, nil + + case "testnet": + return &chaincfg.TestNet3Params, nil + + case "regtest": + return &chaincfg.RegressionNetParams, nil + + case "simnet": + return &chaincfg.SimNetParams, nil + + case "signet": + return &chaincfg.SigNetParams, nil + + default: + return nil, fmt.Errorf("unknown network: %v", network) + } +} + +// parseCoinSelectionStrategy parses a coin selection strategy string +// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type. +func parseCoinSelectionStrategy(ctx *cli.Context) ( + lnrpc.CoinSelectionStrategy, error) { + + strategy := ctx.String(coinSelectionStrategyFlag.Name) + if !ctx.IsSet(coinSelectionStrategyFlag.Name) { + return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG, + nil + } + + switch strategy { + case "global-config": + return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG, + nil + + case "largest": + return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil + + case "random": + return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil + + default: + return 0, fmt.Errorf("unknown coin selection strategy "+ + "%v", strategy) + } +} diff --git a/cmd/lncli/neutrino_active.go b/cmd/commands/neutrino_active.go similarity index 99% rename from cmd/lncli/neutrino_active.go rename to cmd/commands/neutrino_active.go index 099da46c6e8..f34c7cc0e27 100644 --- a/cmd/lncli/neutrino_active.go +++ b/cmd/commands/neutrino_active.go @@ -1,7 +1,7 @@ //go:build neutrinorpc // +build neutrinorpc -package main +package commands import ( "github.com/lightningnetwork/lnd/lnrpc/neutrinorpc" diff --git a/cmd/lncli/neutrino_default.go b/cmd/commands/neutrino_default.go similarity index 92% rename from cmd/lncli/neutrino_default.go rename to cmd/commands/neutrino_default.go index f1f1de404ba..b269e123863 100644 --- a/cmd/lncli/neutrino_default.go +++ b/cmd/commands/neutrino_default.go @@ -1,7 +1,7 @@ //go:build !neutrinorpc // +build !neutrinorpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/peersrpc_active.go b/cmd/commands/peersrpc_active.go similarity index 99% rename from cmd/lncli/peersrpc_active.go rename to cmd/commands/peersrpc_active.go index c044166d360..0736750c734 100644 --- a/cmd/lncli/peersrpc_active.go +++ b/cmd/commands/peersrpc_active.go @@ -1,7 +1,7 @@ //go:build peersrpc // +build peersrpc -package main +package commands import ( "fmt" diff --git a/cmd/lncli/peersrpc_default.go b/cmd/commands/peersrpc_default.go similarity index 91% rename from cmd/lncli/peersrpc_default.go rename to cmd/commands/peersrpc_default.go index 24cb2b8134c..57c8aa7a97f 100644 --- a/cmd/lncli/peersrpc_default.go +++ b/cmd/commands/peersrpc_default.go @@ -1,7 +1,7 @@ //go:build !peersrpc // +build !peersrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/profile.go b/cmd/commands/profile.go similarity index 99% rename from cmd/lncli/profile.go rename to cmd/commands/profile.go index 90ac69c0ee6..e9f6369a250 100644 --- a/cmd/lncli/profile.go +++ b/cmd/commands/profile.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/lncli/routerrpc.go b/cmd/commands/routerrpc.go similarity index 95% rename from cmd/lncli/routerrpc.go rename to cmd/commands/routerrpc.go index 30b8922249b..82211affdff 100644 --- a/cmd/lncli/routerrpc.go +++ b/cmd/commands/routerrpc.go @@ -1,4 +1,4 @@ -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/types.go b/cmd/commands/types.go similarity index 99% rename from cmd/lncli/types.go rename to cmd/commands/types.go index b878811a0a3..1faed7f9b1f 100644 --- a/cmd/lncli/types.go +++ b/cmd/commands/types.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" diff --git a/cmd/lncli/walletrpc_active.go b/cmd/commands/walletrpc_active.go similarity index 99% rename from cmd/lncli/walletrpc_active.go rename to cmd/commands/walletrpc_active.go index e9e5cc5757f..2be509a4bbf 100644 --- a/cmd/lncli/walletrpc_active.go +++ b/cmd/commands/walletrpc_active.go @@ -1,7 +1,7 @@ //go:build walletrpc // +build walletrpc -package main +package commands import ( "bytes" diff --git a/cmd/lncli/walletrpc_default.go b/cmd/commands/walletrpc_default.go similarity index 91% rename from cmd/lncli/walletrpc_default.go rename to cmd/commands/walletrpc_default.go index d6670e44997..90c627c2a5a 100644 --- a/cmd/lncli/walletrpc_default.go +++ b/cmd/commands/walletrpc_default.go @@ -1,7 +1,7 @@ //go:build !walletrpc // +build !walletrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/walletrpc_types.go b/cmd/commands/walletrpc_types.go similarity index 98% rename from cmd/lncli/walletrpc_types.go rename to cmd/commands/walletrpc_types.go index b6680a6ede0..8edc31d4871 100644 --- a/cmd/lncli/walletrpc_types.go +++ b/cmd/commands/walletrpc_types.go @@ -1,4 +1,4 @@ -package main +package commands import "github.com/lightningnetwork/lnd/lnrpc/walletrpc" diff --git a/cmd/lncli/watchtower_active.go b/cmd/commands/watchtower_active.go similarity index 98% rename from cmd/lncli/watchtower_active.go rename to cmd/commands/watchtower_active.go index 9c31c6ec4b2..bc5cd196958 100644 --- a/cmd/lncli/watchtower_active.go +++ b/cmd/commands/watchtower_active.go @@ -1,7 +1,7 @@ //go:build watchtowerrpc // +build watchtowerrpc -package main +package commands import ( "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc" diff --git a/cmd/lncli/watchtower_default.go b/cmd/commands/watchtower_default.go similarity index 92% rename from cmd/lncli/watchtower_default.go rename to cmd/commands/watchtower_default.go index e3db3ccf36d..c958a66bdc1 100644 --- a/cmd/lncli/watchtower_default.go +++ b/cmd/commands/watchtower_default.go @@ -1,7 +1,7 @@ //go:build !watchtowerrpc // +build !watchtowerrpc -package main +package commands import "github.com/urfave/cli" diff --git a/cmd/lncli/wtclient.go b/cmd/commands/wtclient.go similarity index 99% rename from cmd/lncli/wtclient.go rename to cmd/commands/wtclient.go index d73f6ca612a..075861b981c 100644 --- a/cmd/lncli/wtclient.go +++ b/cmd/commands/wtclient.go @@ -1,4 +1,4 @@ -package main +package commands import ( "encoding/hex" diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go index b1554fb0705..52d682eab0b 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -4,591 +4,8 @@ package main -import ( - "context" - "crypto/tls" - "fmt" - "net" - "os" - "path/filepath" - "strings" - "syscall" - - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/chaincfg" - "github.com/lightningnetwork/lnd" - "github.com/lightningnetwork/lnd/build" - "github.com/lightningnetwork/lnd/lncfg" - "github.com/lightningnetwork/lnd/lnrpc" - "github.com/lightningnetwork/lnd/macaroons" - "github.com/lightningnetwork/lnd/tor" - "github.com/urfave/cli" - "golang.org/x/term" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/metadata" -) - -const ( - defaultDataDir = "data" - defaultChainSubDir = "chain" - defaultTLSCertFilename = "tls.cert" - defaultMacaroonFilename = "admin.macaroon" - defaultRPCPort = "10009" - defaultRPCHostPort = "localhost:" + defaultRPCPort - - envVarRPCServer = "LNCLI_RPCSERVER" - envVarLNDDir = "LNCLI_LNDDIR" - envVarSOCKSProxy = "LNCLI_SOCKSPROXY" - envVarTLSCertPath = "LNCLI_TLSCERTPATH" - envVarChain = "LNCLI_CHAIN" - envVarNetwork = "LNCLI_NETWORK" - envVarMacaroonPath = "LNCLI_MACAROONPATH" - envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT" - envVarMacaroonIP = "LNCLI_MACAROONIP" - envVarProfile = "LNCLI_PROFILE" - envVarMacFromJar = "LNCLI_MACFROMJAR" -) - -var ( - defaultLndDir = btcutil.AppDataDir("lnd", false) - defaultTLSCertPath = filepath.Join(defaultLndDir, defaultTLSCertFilename) - - // maxMsgRecvSize is the largest message our client will receive. We - // set this to 200MiB atm. - maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize) -) - -func fatal(err error) { - fmt.Fprintf(os.Stderr, "[lncli] %v\n", err) - os.Exit(1) -} - -func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) { - conn := getClientConn(ctx, true) - - cleanUp := func() { - conn.Close() - } - - return lnrpc.NewWalletUnlockerClient(conn), cleanUp -} - -func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) { - conn := getClientConn(ctx, true) - - cleanUp := func() { - conn.Close() - } - - return lnrpc.NewStateClient(conn), cleanUp -} - -func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) { - conn := getClientConn(ctx, false) - - cleanUp := func() { - conn.Close() - } - - return lnrpc.NewLightningClient(conn), cleanUp -} - -func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn { - // First, we'll get the selected stored profile or an ephemeral one - // created from the global options in the CLI context. - profile, err := getGlobalOptions(ctx, skipMacaroons) - if err != nil { - fatal(fmt.Errorf("could not load global options: %w", err)) - } - - // Create a dial options array. - opts := []grpc.DialOption{ - grpc.WithUnaryInterceptor( - addMetadataUnaryInterceptor(profile.Metadata), - ), - grpc.WithStreamInterceptor( - addMetaDataStreamInterceptor(profile.Metadata), - ), - } - - if profile.Insecure { - opts = append(opts, grpc.WithInsecure()) - } else { - // Load the specified TLS certificate. - certPool, err := profile.cert() - if err != nil { - fatal(fmt.Errorf("could not create cert pool: %w", err)) - } - - // Build transport credentials from the certificate pool. If - // there is no certificate pool, we expect the server to use a - // non-self-signed certificate such as a certificate obtained - // from Let's Encrypt. - var creds credentials.TransportCredentials - if certPool != nil { - creds = credentials.NewClientTLSFromCert(certPool, "") - } else { - // Fallback to the system pool. Using an empty tls - // config is an alternative to x509.SystemCertPool(). - // That call is not supported on Windows. - creds = credentials.NewTLS(&tls.Config{}) - } - - opts = append(opts, grpc.WithTransportCredentials(creds)) - } - - // Only process macaroon credentials if --no-macaroons isn't set and - // if we're not skipping macaroon processing. - if !profile.NoMacaroons && !skipMacaroons { - // Find out which macaroon to load. - macName := profile.Macaroons.Default - if ctx.GlobalIsSet("macfromjar") { - macName = ctx.GlobalString("macfromjar") - } - var macEntry *macaroonEntry - for _, entry := range profile.Macaroons.Jar { - if entry.Name == macName { - macEntry = entry - break - } - } - if macEntry == nil { - fatal(fmt.Errorf("macaroon with name '%s' not found "+ - "in profile", macName)) - } - - // Get and possibly decrypt the specified macaroon. - // - // TODO(guggero): Make it possible to cache the password so we - // don't need to ask for it every time. - mac, err := macEntry.loadMacaroon(readPassword) - if err != nil { - fatal(fmt.Errorf("could not load macaroon: %w", err)) - } - - macConstraints := []macaroons.Constraint{ - // We add a time-based constraint to prevent replay of - // the macaroon. It's good for 60 seconds by default to - // make up for any discrepancy between client and server - // clocks, but leaking the macaroon before it becomes - // invalid makes it possible for an attacker to reuse - // the macaroon. In addition, the validity time of the - // macaroon is extended by the time the server clock is - // behind the client clock, or shortened by the time the - // server clock is ahead of the client clock (or invalid - // altogether if, in the latter case, this time is more - // than 60 seconds). - // TODO(aakselrod): add better anti-replay protection. - macaroons.TimeoutConstraint(profile.Macaroons.Timeout), - - // Lock macaroon down to a specific IP address. - macaroons.IPLockConstraint(profile.Macaroons.IP), - - // ... Add more constraints if needed. - } - - // Apply constraints to the macaroon. - constrainedMac, err := macaroons.AddConstraints( - mac, macConstraints..., - ) - if err != nil { - fatal(err) - } - - // Now we append the macaroon credentials to the dial options. - cred, err := macaroons.NewMacaroonCredential(constrainedMac) - if err != nil { - fatal(fmt.Errorf("error cloning mac: %w", err)) - } - opts = append(opts, grpc.WithPerRPCCredentials(cred)) - } - - // If a socksproxy server is specified we use a tor dialer - // to connect to the grpc server. - if ctx.GlobalIsSet("socksproxy") { - socksProxy := ctx.GlobalString("socksproxy") - torDialer := func(_ context.Context, addr string) (net.Conn, - error) { - - return tor.Dial( - addr, socksProxy, false, false, - tor.DefaultConnTimeout, - ) - } - opts = append(opts, grpc.WithContextDialer(torDialer)) - } else { - // We need to use a custom dialer so we can also connect to - // unix sockets and not just TCP addresses. - genericDialer := lncfg.ClientAddressDialer(defaultRPCPort) - opts = append(opts, grpc.WithContextDialer(genericDialer)) - } - - opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize)) - - conn, err := grpc.Dial(profile.RPCServer, opts...) - if err != nil { - fatal(fmt.Errorf("unable to connect to RPC server: %w", err)) - } - - return conn -} - -// addMetadataUnaryInterceptor returns a grpc client side interceptor that -// appends any key-value metadata strings to the outgoing context of a grpc -// unary call. -func addMetadataUnaryInterceptor( - md map[string]string) grpc.UnaryClientInterceptor { - - return func(ctx context.Context, method string, req, reply interface{}, - cc *grpc.ClientConn, invoker grpc.UnaryInvoker, - opts ...grpc.CallOption) error { - - outCtx := contextWithMetadata(ctx, md) - return invoker(outCtx, method, req, reply, cc, opts...) - } -} - -// addMetaDataStreamInterceptor returns a grpc client side interceptor that -// appends any key-value metadata strings to the outgoing context of a grpc -// stream call. -func addMetaDataStreamInterceptor( - md map[string]string) grpc.StreamClientInterceptor { - - return func(ctx context.Context, desc *grpc.StreamDesc, - cc *grpc.ClientConn, method string, streamer grpc.Streamer, - opts ...grpc.CallOption) (grpc.ClientStream, error) { - - outCtx := contextWithMetadata(ctx, md) - return streamer(outCtx, desc, cc, method, opts...) - } -} - -// contextWithMetaData appends the given metadata key-value pairs to the given -// context. -func contextWithMetadata(ctx context.Context, - md map[string]string) context.Context { - - kvPairs := make([]string, 0, 2*len(md)) - for k, v := range md { - kvPairs = append(kvPairs, k, v) - } - - return metadata.AppendToOutgoingContext(ctx, kvPairs...) -} - -// extractPathArgs parses the TLS certificate and macaroon paths from the -// command. -func extractPathArgs(ctx *cli.Context) (string, string, error) { - network := strings.ToLower(ctx.GlobalString("network")) - switch network { - case "mainnet", "testnet", "regtest", "simnet", "signet": - default: - return "", "", fmt.Errorf("unknown network: %v", network) - } - - // We'll now fetch the lnddir so we can make a decision on how to - // properly read the macaroons (if needed) and also the cert. This will - // either be the default, or will have been overwritten by the end - // user. - lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir")) - - // If the macaroon path as been manually provided, then we'll only - // target the specified file. - var macPath string - if ctx.GlobalString("macaroonpath") != "" { - macPath = lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath")) - } else { - // Otherwise, we'll go into the path: - // lnddir/data/chain// in order to fetch the - // macaroon that we need. - macPath = filepath.Join( - lndDir, defaultDataDir, defaultChainSubDir, - lnd.BitcoinChainName, network, defaultMacaroonFilename, - ) - } - - tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath")) - - // If a custom lnd directory was set, we'll also check if custom paths - // for the TLS cert and macaroon file were set as well. If not, we'll - // override their paths so they can be found within the custom lnd - // directory set. This allows us to set a custom lnd directory, along - // with custom paths to the TLS cert and macaroon file. - if lndDir != defaultLndDir { - tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename) - } - - return tlsCertPath, macPath, nil -} - -// checkNotBothSet accepts two flag names, a and b, and checks that only flag a -// or flag b can be set, but not both. It returns the name of the flag or an -// error. -func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) { - if ctx.IsSet(a) && ctx.IsSet(b) { - return "", fmt.Errorf( - "either %s or %s should be set, but not both", a, b, - ) - } - - if ctx.IsSet(a) { - return a, nil - } - - return b, nil -} +import "github.com/lightningnetwork/lnd/cmd/commands" func main() { - app := cli.NewApp() - app.Name = "lncli" - app.Version = build.Version() + " commit=" + build.Commit - app.Usage = "control plane for your Lightning Network Daemon (lnd)" - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "rpcserver", - Value: defaultRPCHostPort, - Usage: "The host:port of LN daemon.", - EnvVar: envVarRPCServer, - }, - cli.StringFlag{ - Name: "lnddir", - Value: defaultLndDir, - Usage: "The path to lnd's base directory.", - TakesFile: true, - EnvVar: envVarLNDDir, - }, - cli.StringFlag{ - Name: "socksproxy", - Usage: "The host:port of a SOCKS proxy through " + - "which all connections to the LN " + - "daemon will be established over.", - EnvVar: envVarSOCKSProxy, - }, - cli.StringFlag{ - Name: "tlscertpath", - Value: defaultTLSCertPath, - Usage: "The path to lnd's TLS certificate.", - TakesFile: true, - EnvVar: envVarTLSCertPath, - }, - cli.StringFlag{ - Name: "chain, c", - Usage: "The chain lnd is running on, e.g. bitcoin.", - Value: "bitcoin", - EnvVar: envVarChain, - }, - cli.StringFlag{ - Name: "network, n", - Usage: "The network lnd is running on, e.g. mainnet, " + - "testnet, etc.", - Value: "mainnet", - EnvVar: envVarNetwork, - }, - cli.BoolFlag{ - Name: "no-macaroons", - Usage: "Disable macaroon authentication.", - }, - cli.StringFlag{ - Name: "macaroonpath", - Usage: "The path to macaroon file.", - TakesFile: true, - EnvVar: envVarMacaroonPath, - }, - cli.Int64Flag{ - Name: "macaroontimeout", - Value: 60, - Usage: "Anti-replay macaroon validity time in " + - "seconds.", - EnvVar: envVarMacaroonTimeout, - }, - cli.StringFlag{ - Name: "macaroonip", - Usage: "If set, lock macaroon to specific IP address.", - EnvVar: envVarMacaroonIP, - }, - cli.StringFlag{ - Name: "profile, p", - Usage: "Instead of reading settings from command " + - "line parameters or using the default " + - "profile, use a specific profile. If " + - "a default profile is set, this flag can be " + - "set to an empty string to disable reading " + - "values from the profiles file.", - EnvVar: envVarProfile, - }, - cli.StringFlag{ - Name: "macfromjar", - Usage: "Use this macaroon from the profile's " + - "macaroon jar instead of the default one. " + - "Can only be used if profiles are defined.", - EnvVar: envVarMacFromJar, - }, - cli.StringSliceFlag{ - Name: "metadata", - Usage: "This flag can be used to specify a key-value " + - "pair that should be appended to the " + - "outgoing context before the request is sent " + - "to lnd. This flag may be specified multiple " + - "times. The format is: \"key:value\".", - }, - cli.BoolFlag{ - Name: "insecure", - Usage: "Connect to the rpc server without TLS " + - "authentication", - Hidden: true, - }, - } - app.Commands = []cli.Command{ - createCommand, - createWatchOnlyCommand, - unlockCommand, - changePasswordCommand, - newAddressCommand, - estimateFeeCommand, - sendManyCommand, - sendCoinsCommand, - listUnspentCommand, - connectCommand, - disconnectCommand, - openChannelCommand, - batchOpenChannelCommand, - closeChannelCommand, - closeAllChannelsCommand, - abandonChannelCommand, - listPeersCommand, - walletBalanceCommand, - channelBalanceCommand, - getInfoCommand, - getDebugInfoCommand, - encryptDebugPackageCommand, - decryptDebugPackageCommand, - getRecoveryInfoCommand, - pendingChannelsCommand, - sendPaymentCommand, - payInvoiceCommand, - sendToRouteCommand, - addInvoiceCommand, - lookupInvoiceCommand, - listInvoicesCommand, - listChannelsCommand, - closedChannelsCommand, - listPaymentsCommand, - describeGraphCommand, - getNodeMetricsCommand, - getChanInfoCommand, - getNodeInfoCommand, - queryRoutesCommand, - getNetworkInfoCommand, - debugLevelCommand, - decodePayReqCommand, - listChainTxnsCommand, - stopCommand, - signMessageCommand, - verifyMessageCommand, - feeReportCommand, - updateChannelPolicyCommand, - forwardingHistoryCommand, - exportChanBackupCommand, - verifyChanBackupCommand, - restoreChanBackupCommand, - bakeMacaroonCommand, - listMacaroonIDsCommand, - deleteMacaroonIDCommand, - listPermissionsCommand, - printMacaroonCommand, - constrainMacaroonCommand, - trackPaymentCommand, - versionCommand, - profileSubCommand, - getStateCommand, - deletePaymentsCommand, - sendCustomCommand, - subscribeCustomCommand, - fishCompletionCommand, - listAliasesCommand, - estimateRouteFeeCommand, - generateManPageCommand, - } - - // Add any extra commands determined by build flags. - app.Commands = append(app.Commands, autopilotCommands()...) - app.Commands = append(app.Commands, invoicesCommands()...) - app.Commands = append(app.Commands, neutrinoCommands()...) - app.Commands = append(app.Commands, routerCommands()...) - app.Commands = append(app.Commands, walletCommands()...) - app.Commands = append(app.Commands, watchtowerCommands()...) - app.Commands = append(app.Commands, wtclientCommands()...) - app.Commands = append(app.Commands, devCommands()...) - app.Commands = append(app.Commands, peersCommands()...) - app.Commands = append(app.Commands, chainCommands()...) - - if err := app.Run(os.Args); err != nil { - fatal(err) - } -} - -// readPassword reads a password from the terminal. This requires there to be an -// actual TTY so passing in a password from stdin won't work. -func readPassword(text string) ([]byte, error) { - fmt.Print(text) - - // The variable syscall.Stdin is of a different type in the Windows API - // that's why we need the explicit cast. And of course the linter - // doesn't like it either. - pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert - fmt.Println() - return pw, err -} - -// networkParams parses the global network flag into a chaincfg.Params. -func networkParams(ctx *cli.Context) (*chaincfg.Params, error) { - network := strings.ToLower(ctx.GlobalString("network")) - switch network { - case "mainnet": - return &chaincfg.MainNetParams, nil - - case "testnet": - return &chaincfg.TestNet3Params, nil - - case "regtest": - return &chaincfg.RegressionNetParams, nil - - case "simnet": - return &chaincfg.SimNetParams, nil - - case "signet": - return &chaincfg.SigNetParams, nil - - default: - return nil, fmt.Errorf("unknown network: %v", network) - } -} - -// parseCoinSelectionStrategy parses a coin selection strategy string -// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type. -func parseCoinSelectionStrategy(ctx *cli.Context) ( - lnrpc.CoinSelectionStrategy, error) { - - strategy := ctx.String(coinSelectionStrategyFlag.Name) - if !ctx.IsSet(coinSelectionStrategyFlag.Name) { - return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG, - nil - } - - switch strategy { - case "global-config": - return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG, - nil - - case "largest": - return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil - - case "random": - return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil - - default: - return 0, fmt.Errorf("unknown coin selection strategy "+ - "%v", strategy) - } + commands.Main() } diff --git a/funding/manager.go b/funding/manager.go index 8c338c6f3e9..d40f545950f 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -299,6 +299,8 @@ type InitFundingMsg struct { // channel that will be useful to our future selves. Memo []byte + CustomChannelData []byte + // Updates is a channel which updates to the opening status of the // channel are sent on. Updates chan *lnrpc.OpenStatusUpdate @@ -4697,6 +4699,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { ScidAliasFeature: scidFeatureVal, Memo: msg.Memo, TapscriptRoot: tapscriptRoot, + CustomChannelData: msg.CustomChannelData, } reservation, err := f.cfg.Wallet.InitChannelReservation(req) diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index 46badbe84b1..92fe8c78d23 100644 --- a/lnrpc/lightning.pb.go +++ b/lnrpc/lightning.pb.go @@ -4684,7 +4684,8 @@ type Channel struct { // An optional note-to-self to go along with the channel containing some // useful information. This is only ever stored locally and in no way impacts // the channel's operation. - Memo string `protobuf:"bytes,36,opt,name=memo,proto3" json:"memo,omitempty"` + Memo string `protobuf:"bytes,36,opt,name=memo,proto3" json:"memo,omitempty"` + CustomChannelData []byte `protobuf:"bytes,37,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"` } func (x *Channel) Reset() { @@ -4975,6 +4976,13 @@ func (x *Channel) GetMemo() string { return "" } +func (x *Channel) GetCustomChannelData() []byte { + if x != nil { + return x.CustomChannelData + } + return nil +} + type ListChannelsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -7660,7 +7668,8 @@ type OpenChannelRequest struct { // the channel's operation. Memo string `protobuf:"bytes,27,opt,name=memo,proto3" json:"memo,omitempty"` // A list of selected outpoints that are allocated for channel funding. - Outpoints []*OutPoint `protobuf:"bytes,28,rep,name=outpoints,proto3" json:"outpoints,omitempty"` + Outpoints []*OutPoint `protobuf:"bytes,28,rep,name=outpoints,proto3" json:"outpoints,omitempty"` + CustomChannelData []byte `protobuf:"bytes,29,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"` } func (x *OpenChannelRequest) Reset() { @@ -7893,6 +7902,13 @@ func (x *OpenChannelRequest) GetOutpoints() []*OutPoint { return nil } +func (x *OpenChannelRequest) GetCustomChannelData() []byte { + if x != nil { + return x.CustomChannelData + } + return nil +} + type OpenStatusUpdate struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -18415,7 +18431,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x73, 0x61, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x48, 0x74, 0x6c, 0x63, - 0x73, 0x22, 0xad, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, + 0x73, 0x22, 0xdd, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, @@ -18506,7 +18522,10 @@ var file_lightning_proto_rawDesc = []byte{ 0x6c, 0x69, 0x61, 0x73, 0x18, 0x23, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0d, 0x70, 0x65, 0x65, 0x72, 0x53, 0x63, 0x69, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x24, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, - 0x6f, 0x22, 0xdf, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6f, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x25, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, + 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, + 0x61, 0x22, 0xdf, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, @@ -18905,7 +18924,7 @@ var file_lightning_proto_rawDesc = []byte{ 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x70, 0x65, 0x6e, 0x64, - 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x22, 0xcb, 0x08, 0x0a, 0x12, + 0x69, 0x6e, 0x67, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x22, 0xfb, 0x08, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x22, 0x0a, 0x0d, 0x73, 0x61, 0x74, 0x5f, 0x70, 0x65, 0x72, 0x5f, 0x76, 0x62, 0x79, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x61, 0x74, 0x50, 0x65, @@ -18974,7 +18993,10 @@ var file_lightning_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x12, 0x2d, 0x0a, 0x09, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x1c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x09, - 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x22, 0xf3, 0x01, 0x0a, 0x10, 0x4f, 0x70, + 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x1d, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x22, 0xf3, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x39, 0x0a, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto index 7a343b49198..d6643e29865 100644 --- a/lnrpc/lightning.proto +++ b/lnrpc/lightning.proto @@ -1590,6 +1590,8 @@ message Channel { the channel's operation. */ string memo = 36; + + bytes custom_channel_data = 37; } message ListChannelsRequest { @@ -2426,6 +2428,8 @@ message OpenChannelRequest { A list of selected outpoints that are allocated for channel funding. */ repeated OutPoint outpoints = 28; + + bytes custom_channel_data = 29; } message OpenStatusUpdate { oneof update { diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index 3c1b2a81069..f27d18ca03e 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -3805,6 +3805,10 @@ "memo": { "type": "string", "description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way impacts\nthe channel's operation." + }, + "custom_channel_data": { + "type": "string", + "format": "byte" } } }, @@ -6122,6 +6126,10 @@ "$ref": "#/definitions/lnrpcOutPoint" }, "description": "A list of selected outpoints that are allocated for channel funding." + }, + "custom_channel_data": { + "type": "string", + "format": "byte" } } }, diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 09567f45647..79ced58c401 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -229,6 +229,8 @@ type InitFundingReserveMsg struct { // channel that will be useful to our future selves. Memo []byte + CustomChannelData []byte + // TapscriptRoot is an optional tapscript root that if provided, will // be used to create the combined key for musig2 based channels. TapscriptRoot fn.Option[chainhash.Hash] diff --git a/rpcserver.go b/rpcserver.go index 7f2ca16da95..c08291a9eff 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -2282,6 +2282,7 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest, FundUpToMaxAmt: fundUpToMaxAmt, MinFundAmt: minFundAmt, Memo: []byte(in.Memo), + CustomChannelData: in.CustomChannelData, Outpoints: outpoints, }, nil } @@ -4399,6 +4400,11 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel, // is returned and peerScidAlias will be an empty ShortChannelID. peerScidAlias, _ := r.server.aliasMgr.GetPeerAlias(chanID) + var customChannelData []byte + dbChannel.CustomBlob.WhenSome(func(blob tlv.Blob) { + customChannelData = blob + }) + channel := &lnrpc.Channel{ Active: isActive, Private: isPrivate(dbChannel), @@ -4431,6 +4437,7 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel, ZeroConf: dbChannel.IsZeroConf(), ZeroConfConfirmedScid: dbChannel.ZeroConfRealScid().ToUint64(), Memo: string(dbChannel.Memo), + CustomChannelData: customChannelData, // TODO: remove the following deprecated fields CsvDelay: uint32(dbChannel.LocalChanCfg.CsvDelay), LocalChanReserveSat: int64(dbChannel.LocalChanCfg.ChanReserve), From 5e1dd2a2ae1e52113127e0d18310c6450ccd4ac0 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 11 Apr 2024 14:20:31 +0200 Subject: [PATCH 23/35] lnwallet: add AddHeight and RemoveHeight funcs --- lnwallet/channel.go | 55 ++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index e5ae4984153..7f0cb5688ea 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -385,6 +385,28 @@ type PaymentDescriptor struct { BlindingPoint lnwire.BlindingPointRecord } +// AddHeight returns a pointer to the height at which the HTLC was added to the +// commitment chain. The height is returned based on the chain the HTLC is +// being added to (local or remote chain). +func AddHeight(htlc *PaymentDescriptor, remoteChain bool) *uint64 { + if remoteChain { + return &htlc.addCommitHeightRemote + } + + return &htlc.addCommitHeightLocal +} + +// RemoveHeight returns a pointer to the height at which the HTLC was removed +// from the commitment chain. The height is returned based on the chain the HTLC +// is being removed from (local or remote chain). +func RemoveHeight(htlc *PaymentDescriptor, remoteChain bool) *uint64 { + if remoteChain { + return &htlc.removeCommitHeightRemote + } + + return &htlc.removeCommitHeightLocal +} + // PayDescsFromRemoteLogUpdates converts a slice of LogUpdates received from the // remote peer into PaymentDescriptors to inform a link's forwarding decisions. // @@ -3469,13 +3491,7 @@ func processAddEntry(htlc *PaymentDescriptor, ourBalance, theirBalance *lnwire.M // a new commitment), then we'll may be updating the height this entry // was added to the chain. Otherwise, we may be updating the entry's // height w.r.t the local chain. - var addHeight *uint64 - if remoteChain { - addHeight = &htlc.addCommitHeightRemote - } else { - addHeight = &htlc.addCommitHeightLocal - } - + addHeight := AddHeight(htlc, remoteChain) if *addHeight != 0 { return } @@ -3506,14 +3522,8 @@ func processRemoveEntry(htlc *PaymentDescriptor, ourBalance, theirBalance *lnwire.MilliSatoshi, nextHeight uint64, remoteChain bool, isIncoming, mutateState bool) { - var removeHeight *uint64 - if remoteChain { - removeHeight = &htlc.removeCommitHeightRemote - } else { - removeHeight = &htlc.removeCommitHeightLocal - } - // Ignore any removal entries which have already been processed. + removeHeight := RemoveHeight(htlc, remoteChain) if *removeHeight != 0 { return } @@ -3557,15 +3567,8 @@ func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, // Fee updates are applied for all commitments after they are // sent/received, so we consider them being added and removed at the // same height. - var addHeight *uint64 - var removeHeight *uint64 - if remoteChain { - addHeight = &feeUpdate.addCommitHeightRemote - removeHeight = &feeUpdate.removeCommitHeightRemote - } else { - addHeight = &feeUpdate.addCommitHeightLocal - removeHeight = &feeUpdate.removeCommitHeightLocal - } + addHeight := AddHeight(feeUpdate, remoteChain) + removeHeight := RemoveHeight(feeUpdate, remoteChain) if *addHeight != 0 { return @@ -5041,10 +5044,12 @@ func (lc *LightningChannel) computeView(view *HtlcView, remoteChain bool, // number of outstanding HTLCs has changed. if lc.channelState.IsInitiator { ourBalance += lnwire.NewMSatFromSatoshis( - commitChain.tip().fee) + commitChain.tip().fee, + ) } else if !lc.channelState.IsInitiator { theirBalance += lnwire.NewMSatFromSatoshis( - commitChain.tip().fee) + commitChain.tip().fee, + ) } nextHeight := commitChain.tip().height + 1 From fa61f2626a008f788967bbda7c161737e7df8051 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 11 Apr 2024 14:22:17 +0200 Subject: [PATCH 24/35] lnwallet: export AnchorSize --- lnwallet/channel.go | 8 ++++---- lnwallet/channel_test.go | 10 +++++----- lnwallet/commitment.go | 10 +++++----- lnwallet/reservation.go | 2 +- lnwallet/test_utils.go | 2 +- lnwallet/transactions_test.go | 2 +- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 7f0cb5688ea..fcf9b9d1cb5 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -8575,7 +8575,7 @@ func NewAnchorResolution(chanState *channeldb.OpenChannel, WitnessScript: anchorWitnessScript, Output: &wire.TxOut{ PkScript: localAnchor.PkScript(), - Value: int64(anchorSize), + Value: int64(AnchorSize), }, HashType: sweepSigHash(chanState.ChanType), } @@ -8587,7 +8587,7 @@ func NewAnchorResolution(chanState *channeldb.OpenChannel, //nolint:lll signDesc.PrevOutputFetcher = txscript.NewCannedPrevOutputFetcher( - localAnchor.PkScript(), int64(anchorSize), + localAnchor.PkScript(), int64(AnchorSize), ) // For anchor outputs with taproot channels, the key desc is @@ -9080,7 +9080,7 @@ func (lc *LightningChannel) LocalBalanceDust() bool { // regain the stats allocated to the anchor outputs with the co-op // close transaction. if chanState.ChanType.HasAnchors() && chanState.IsInitiator { - localBalance += 2 * anchorSize + localBalance += 2 * AnchorSize } return localBalance <= chanState.LocalChanCfg.DustLimit @@ -9100,7 +9100,7 @@ func (lc *LightningChannel) RemoteBalanceDust() bool { // regain the stats allocated to the anchor outputs with the co-op // close transaction. if chanState.ChanType.HasAnchors() && !chanState.IsInitiator { - remoteBalance += 2 * anchorSize + remoteBalance += 2 * AnchorSize } return remoteBalance <= chanState.RemoteChanCfg.DustLimit diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 1d0763ae961..594caf9b90b 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -713,7 +713,7 @@ func TestCooperativeChannelClosure(t *testing.T) { testCoopClose(t, &coopCloseTestCase{ chanType: channeldb.SingleFunderTweaklessBit | channeldb.AnchorOutputsBit, - anchorAmt: anchorSize * 2, + anchorAmt: AnchorSize * 2, }) }) } @@ -823,7 +823,7 @@ func TestForceClose(t *testing.T) { chanType: channeldb.SingleFunderTweaklessBit | channeldb.AnchorOutputsBit, expectedCommitWeight: input.AnchorCommitWeight, - anchorAmt: anchorSize * 2, + anchorAmt: AnchorSize * 2, }) }) t.Run("taproot", func(t *testing.T) { @@ -832,7 +832,7 @@ func TestForceClose(t *testing.T) { channeldb.AnchorOutputsBit | channeldb.SimpleTaprootFeatureBit, expectedCommitWeight: input.TaprootCommitWeight, - anchorAmt: anchorSize * 2, + anchorAmt: AnchorSize * 2, }) }) t.Run("taproot with tapscript root", func(t *testing.T) { @@ -842,7 +842,7 @@ func TestForceClose(t *testing.T) { channeldb.SimpleTaprootFeatureBit | channeldb.TapscriptRootBit, expectedCommitWeight: input.TaprootCommitWeight, - anchorAmt: anchorSize * 2, + anchorAmt: AnchorSize * 2, }) }) } @@ -928,7 +928,7 @@ func testForceClose(t *testing.T, testCase *forceCloseTestCase) { t.Fatal("commit tx not referenced by anchor res") } if anchorRes.AnchorSignDescriptor.Output.Value != - int64(anchorSize) { + int64(AnchorSize) { t.Fatal("unexpected anchor size") } diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index b0c7fc645b6..cb34d39ed35 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -18,8 +18,8 @@ import ( "github.com/lightningnetwork/lnd/tlv" ) -// anchorSize is the constant anchor output size. -const anchorSize = btcutil.Amount(330) +// AnchorSize is the constant anchor output size. +const AnchorSize = btcutil.Amount(330) // DefaultAnchorsCommitMaxFeeRateSatPerVByte is the default max fee rate in // sat/vbyte the initiator will use for anchor channels. This should be enough @@ -1184,7 +1184,7 @@ func CreateCommitTx(chanType channeldb.ChannelType, if localOutput || numHTLCs > 0 { commitTx.AddTxOut(&wire.TxOut{ PkScript: localAnchor.PkScript(), - Value: int64(anchorSize), + Value: int64(AnchorSize), }) } @@ -1193,7 +1193,7 @@ func CreateCommitTx(chanType channeldb.ChannelType, if remoteOutput || numHTLCs > 0 { commitTx.AddTxOut(&wire.TxOut{ PkScript: remoteAnchor.PkScript(), - Value: int64(anchorSize), + Value: int64(AnchorSize), }) } } @@ -1218,7 +1218,7 @@ func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool, // Since the initiator's balance also is stored after subtracting the // anchor values, add that back in case this was an anchor commitment. if chanType.HasAnchors() { - initiatorDelta += 2 * anchorSize + initiatorDelta += 2 * AnchorSize } // The initiator will pay the full coop close fee, subtract that value diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 2dbe9382828..dc63cafa2f6 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -268,7 +268,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, // addition to the two anchor outputs. feeMSat := lnwire.NewMSatFromSatoshis(commitFee) if req.CommitType.HasAnchors() { - feeMSat += 2 * lnwire.NewMSatFromSatoshis(anchorSize) + feeMSat += 2 * lnwire.NewMSatFromSatoshis(AnchorSize) } // Used to cut down on verbosity. diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index 61ab72c2043..488064c42b1 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -254,7 +254,7 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType, commitFee := calcStaticFee(chanType, 0) var anchorAmt btcutil.Amount if chanType.HasAnchors() { - anchorAmt += 2 * anchorSize + anchorAmt += 2 * AnchorSize } aliceBalance := lnwire.NewMSatFromSatoshis( diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index 14c11b7f764..fc7ba902ecd 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -923,7 +923,7 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp var anchorAmt btcutil.Amount if chanType.HasAnchors() { - anchorAmt = 2 * anchorSize + anchorAmt = 2 * AnchorSize } remoteCommitTx, localCommitTx, err := CreateCommitmentTxns( From 4bda93ae565086533ab977cb9834d571faa6f6f1 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 11 Apr 2024 14:23:59 +0200 Subject: [PATCH 25/35] lnwallet: export GenTaprootHtlcScript --- lnwallet/commitment.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index cb34d39ed35..e9deca0ce3d 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -1316,9 +1316,9 @@ func genSegwitV0HtlcScript(chanType channeldb.ChannelType, }, nil } -// genTaprootHtlcScript generates the HTLC scripts for a taproot+musig2 +// GenTaprootHtlcScript generates the HTLC scripts for a taproot+musig2 // channel. -func genTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, +func GenTaprootHtlcScript(isIncoming, ourCommit bool, timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing, auxLeaf input.AuxTapLeaf) (*input.HtlcScriptTree, error) { @@ -1388,7 +1388,7 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming, ourCommit bool, ) } - return genTaprootHtlcScript( + return GenTaprootHtlcScript( isIncoming, ourCommit, timeout, rHash, keyRing, auxLeaf, ) } From 15abb1403f384cd93a5b161efe441f67fe6d8d54 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 11 Apr 2024 14:26:15 +0200 Subject: [PATCH 26/35] lnwallet: add Tree() method, fix formatting --- input/script_desc.go | 11 ++++--- input/script_utils.go | 75 ++++++++++++++++++++++++++---------------- lnwallet/commitment.go | 28 ++++++++-------- 3 files changed, 66 insertions(+), 48 deletions(-) diff --git a/input/script_desc.go b/input/script_desc.go index a32ae66316f..17b58b72196 100644 --- a/input/script_desc.go +++ b/input/script_desc.go @@ -33,17 +33,17 @@ const ( ScriptPathDelay ) -// ScriptDesciptor is an interface that abstracts over the various ways a +// ScriptDescriptor is an interface that abstracts over the various ways a // pkScript can be spent from an output. This supports both normal p2wsh -// (witness script, etc), and also tapscript paths which have distinct +// (witness script, etc.), and also tapscript paths which have distinct // tapscript leaves. type ScriptDescriptor interface { // PkScript is the public key script that commits to the final // contract. PkScript() []byte - // WitnessScript returns the witness script that we'll use when signing - // for the remote party, and also verifying signatures on our + // WitnessScriptToSign returns the witness script that we'll use when + // signing for the remote party, and also verifying signatures on our // transactions. As an example, when we create an outgoing HTLC for the // remote party, we want to sign their success path. // @@ -73,6 +73,9 @@ type TapscriptDescriptor interface { // TapScriptTree returns the underlying tapscript tree. TapScriptTree() *txscript.IndexedTapScriptTree + + // Tree returns the underlying ScriptTree. + Tree() ScriptTree } // ScriptTree holds the contents needed to spend a script within a tapscript diff --git a/input/script_utils.go b/input/script_utils.go index 08110ed2c20..7f719082cc7 100644 --- a/input/script_utils.go +++ b/input/script_utils.go @@ -733,8 +733,8 @@ func (h *HtlcScriptTree) WitnessScriptForPath(path ScriptPath) ([]byte, error) { // CtrlBlockForPath returns the control block for the given spending path. For // script types that don't have a control block, nil is returned. -func (h *HtlcScriptTree) CtrlBlockForPath(path ScriptPath, -) (*txscript.ControlBlock, error) { +func (h *HtlcScriptTree) CtrlBlockForPath( + path ScriptPath) (*txscript.ControlBlock, error) { switch path { case ScriptPathSuccess: @@ -752,6 +752,11 @@ func (h *HtlcScriptTree) CtrlBlockForPath(path ScriptPath, } } +// Tree returns the underlying ScriptTree of the HtlcScriptTree. +func (h *HtlcScriptTree) Tree() ScriptTree { + return h.ScriptTree +} + // A compile time check to ensure HtlcScriptTree implements the // TapscriptMultiplexer interface. var _ TapscriptDescriptor = (*HtlcScriptTree)(nil) @@ -1752,9 +1757,9 @@ func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey, }, nil } -// WitnessScript returns the witness script that we'll use when signing for the -// remote party, and also verifying signatures on our transactions. As an -// example, when we create an outgoing HTLC for the remote party, we want to +// WitnessScriptToSign returns the witness script that we'll use when signing +// for the remote party, and also verifying signatures on our transactions. As +// an example, when we create an outgoing HTLC for the remote party, we want to // sign their success path. func (s *SecondLevelScriptTree) WitnessScriptToSign() []byte { return s.SuccessTapLeaf.Script @@ -1762,8 +1767,8 @@ func (s *SecondLevelScriptTree) WitnessScriptToSign() []byte { // WitnessScriptForPath returns the witness script for the given spending path. // An error is returned if the path is unknown. -func (s *SecondLevelScriptTree) WitnessScriptForPath(path ScriptPath, -) ([]byte, error) { +func (s *SecondLevelScriptTree) WitnessScriptForPath( + path ScriptPath) ([]byte, error) { switch path { case ScriptPathDelay: @@ -1778,8 +1783,8 @@ func (s *SecondLevelScriptTree) WitnessScriptForPath(path ScriptPath, // CtrlBlockForPath returns the control block for the given spending path. For // script types that don't have a control block, nil is returned. -func (s *SecondLevelScriptTree) CtrlBlockForPath(path ScriptPath, -) (*txscript.ControlBlock, error) { +func (s *SecondLevelScriptTree) CtrlBlockForPath( + path ScriptPath) (*txscript.ControlBlock, error) { switch path { case ScriptPathDelay: @@ -1795,6 +1800,11 @@ func (s *SecondLevelScriptTree) CtrlBlockForPath(path ScriptPath, } } +// Tree returns the underlying ScriptTree of the SecondLevelScriptTree. +func (s *SecondLevelScriptTree) Tree() ScriptTree { + return s.ScriptTree +} + // A compile time check to ensure SecondLevelScriptTree implements the // TapscriptDescriptor interface. var _ TapscriptDescriptor = (*SecondLevelScriptTree)(nil) @@ -2137,9 +2147,9 @@ type CommitScriptTree struct { // TapscriptDescriptor interface. var _ TapscriptDescriptor = (*CommitScriptTree)(nil) -// WitnessScript returns the witness script that we'll use when signing for the -// remote party, and also verifying signatures on our transactions. As an -// example, when we create an outgoing HTLC for the remote party, we want to +// WitnessScriptToSign returns the witness script that we'll use when signing +// for the remote party, and also verifying signatures on our transactions. As +// an example, when we create an outgoing HTLC for the remote party, we want to // sign their success path. func (c *CommitScriptTree) WitnessScriptToSign() []byte { // TODO(roasbeef): abstraction leak here? always dependent @@ -2148,8 +2158,8 @@ func (c *CommitScriptTree) WitnessScriptToSign() []byte { // WitnessScriptForPath returns the witness script for the given spending path. // An error is returned if the path is unknown. -func (c *CommitScriptTree) WitnessScriptForPath(path ScriptPath, -) ([]byte, error) { +func (c *CommitScriptTree) WitnessScriptForPath( + path ScriptPath) ([]byte, error) { switch path { // For the commitment output, the delay and success path are the same, @@ -2167,8 +2177,8 @@ func (c *CommitScriptTree) WitnessScriptForPath(path ScriptPath, // CtrlBlockForPath returns the control block for the given spending path. For // script types that don't have a control block, nil is returned. -func (c *CommitScriptTree) CtrlBlockForPath(path ScriptPath, -) (*txscript.ControlBlock, error) { +func (c *CommitScriptTree) CtrlBlockForPath( + path ScriptPath) (*txscript.ControlBlock, error) { switch path { case ScriptPathDelay: @@ -2188,6 +2198,11 @@ func (c *CommitScriptTree) CtrlBlockForPath(path ScriptPath, } } +// Tree returns the underlying ScriptTree of the CommitScriptTree. +func (c *CommitScriptTree) Tree() ScriptTree { + return c.ScriptTree +} + // NewLocalCommitScriptTree returns a new CommitScript tree that can be used to // create and spend the commitment output for the local party. func NewLocalCommitScriptTree(csvTimeout uint32, selfKey, @@ -2315,7 +2330,7 @@ func TaprootCommitScriptToSelf(csvTimeout uint32, return commitScriptTree.TaprootKey, nil } -// MakeTaprootSCtrlBlock takes a leaf script, the internal key (usually the +// MakeTaprootCtrlBlock takes a leaf script, the internal key (usually the // revoke key), and a script tree and creates a valid control block for a spend // of the leaf. func MakeTaprootCtrlBlock(leafScript []byte, internalKey *btcec.PublicKey, @@ -2370,9 +2385,6 @@ func TaprootCommitSpendSuccess(signer Signer, signDesc *SignDescriptor, witnessStack[0] = maybeAppendSighash(sweepSig, signDesc.HashType) witnessStack[1] = signDesc.WitnessScript witnessStack[2] = ctrlBlockBytes - if err != nil { - return nil, err - } return witnessStack, nil } @@ -2854,8 +2866,8 @@ type AnchorScriptTree struct { // NewAnchorScriptTree makes a new script tree for an anchor output with the // passed anchor key. -func NewAnchorScriptTree(anchorKey *btcec.PublicKey, -) (*AnchorScriptTree, error) { +func NewAnchorScriptTree( + anchorKey *btcec.PublicKey) (*AnchorScriptTree, error) { // The main script used is just a OP_16 CSV (anyone can sweep after 16 // blocks). @@ -2891,9 +2903,9 @@ func NewAnchorScriptTree(anchorKey *btcec.PublicKey, }, nil } -// WitnessScript returns the witness script that we'll use when signing for the -// remote party, and also verifying signatures on our transactions. As an -// example, when we create an outgoing HTLC for the remote party, we want to +// WitnessScriptToSign returns the witness script that we'll use when signing +// for the remote party, and also verifying signatures on our transactions. As +// an example, when we create an outgoing HTLC for the remote party, we want to // sign their success path. func (a *AnchorScriptTree) WitnessScriptToSign() []byte { return a.SweepLeaf.Script @@ -2901,8 +2913,8 @@ func (a *AnchorScriptTree) WitnessScriptToSign() []byte { // WitnessScriptForPath returns the witness script for the given spending path. // An error is returned if the path is unknown. -func (a *AnchorScriptTree) WitnessScriptForPath(path ScriptPath, -) ([]byte, error) { +func (a *AnchorScriptTree) WitnessScriptForPath( + path ScriptPath) ([]byte, error) { switch path { case ScriptPathDelay: @@ -2917,8 +2929,8 @@ func (a *AnchorScriptTree) WitnessScriptForPath(path ScriptPath, // CtrlBlockForPath returns the control block for the given spending path. For // script types that don't have a control block, nil is returned. -func (a *AnchorScriptTree) CtrlBlockForPath(path ScriptPath, -) (*txscript.ControlBlock, error) { +func (a *AnchorScriptTree) CtrlBlockForPath( + path ScriptPath) (*txscript.ControlBlock, error) { switch path { case ScriptPathDelay: @@ -2934,6 +2946,11 @@ func (a *AnchorScriptTree) CtrlBlockForPath(path ScriptPath, } } +// Tree returns the underlying ScriptTree of the AnchorScriptTree. +func (a *AnchorScriptTree) Tree() ScriptTree { + return a.ScriptTree +} + // A compile time check to ensure AnchorScriptTree implements the // TapscriptDescriptor interface. var _ TapscriptDescriptor = (*AnchorScriptTree)(nil) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index e9deca0ce3d..fc7a059b301 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -201,9 +201,9 @@ func (w *WitnessScriptDesc) PkScript() []byte { return w.OutputScript } -// WitnessScript returns the witness script that we'll use when signing for the -// remote party, and also verifying signatures on our transactions. As an -// example, when we create an outgoing HTLC for the remote party, we want to +// WitnessScriptToSign returns the witness script that we'll use when signing +// for the remote party, and also verifying signatures on our transactions. As +// an example, when we create an outgoing HTLC for the remote party, we want to // sign their success path. func (w *WitnessScriptDesc) WitnessScriptToSign() []byte { return w.WitnessScript @@ -211,10 +211,10 @@ func (w *WitnessScriptDesc) WitnessScriptToSign() []byte { // WitnessScriptForPath returns the witness script for the given spending path. // An error is returned if the path is unknown. This is useful as when -// constructing a contrl block for a given path, one also needs witness script +// constructing a control block for a given path, one also needs witness script // being signed. -func (w *WitnessScriptDesc) WitnessScriptForPath(_ input.ScriptPath, -) ([]byte, error) { +func (w *WitnessScriptDesc) WitnessScriptForPath( + _ input.ScriptPath) ([]byte, error) { return w.WitnessScript, nil } @@ -532,8 +532,8 @@ func CommitScriptAnchors(chanType channeldb.ChannelType, input.ScriptDescriptor, input.ScriptDescriptor, error) { var ( - anchorScript func(key *btcec.PublicKey) ( - input.ScriptDescriptor, error) + anchorScript func( + key *btcec.PublicKey) (input.ScriptDescriptor, error) keySelector func(*channeldb.ChannelConfig, bool) *btcec.PublicKey @@ -544,12 +544,10 @@ func CommitScriptAnchors(chanType channeldb.ChannelType, // level key is now the (relative) local delay and remote public key, // since these are fully revealed once the commitment hits the chain. case chanType.IsTaproot(): - anchorScript = func(key *btcec.PublicKey, - ) (input.ScriptDescriptor, error) { + anchorScript = func( + key *btcec.PublicKey) (input.ScriptDescriptor, error) { - return input.NewAnchorScriptTree( - key, - ) + return input.NewAnchorScriptTree(key) } keySelector = func(cfg *channeldb.ChannelConfig, @@ -567,8 +565,8 @@ func CommitScriptAnchors(chanType channeldb.ChannelType, default: // For normal channels, we'll create a p2wsh script based on // the target key. - anchorScript = func(key *btcec.PublicKey, - ) (input.ScriptDescriptor, error) { + anchorScript = func( + key *btcec.PublicKey) (input.ScriptDescriptor, error) { script, err := input.CommitScriptAnchor(key) if err != nil { From 011ff00bfe8b9f179737212d05fb3b924dbeb6bd Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 23 Apr 2024 14:31:38 +0100 Subject: [PATCH 27/35] invoices: add invoice settlement interceptor service This commit introduces a new invoice settlement interceptor service that intercepts invoices during their settlement phase. It forwards invoices to subscribed clients to determine their settlement outcomes. This commit also introduces an interface to facilitate integrating the interceptor with other packages. --- invoices/interface.go | 39 +++++ invoices/settlement_interceptor.go | 229 +++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 invoices/settlement_interceptor.go diff --git a/invoices/interface.go b/invoices/interface.go index 490db1be568..93741794633 100644 --- a/invoices/interface.go +++ b/invoices/interface.go @@ -198,3 +198,42 @@ type InvoiceUpdater interface { // Finalize finalizes the update before it is written to the database. Finalize(updateType UpdateType) error } + +// InterceptClientRequest is the request that is passed to the client via +// callback during an interceptor session. The request contains the invoice that +// is being intercepted and supporting information. +type InterceptClientRequest struct { + // ExitHtlcCircuitKey is the circuit key that identifies the HTLC which + // is involved in the invoice settlement. + ExitHtlcCircuitKey CircuitKey + + // ExitHtlcAmt is the amount of the HTLC which is involved in the + // invoice settlement. + ExitHtlcAmt lnwire.MilliSatoshi + + // ExitHtlcExpiry is the expiry time of the HTLC which is involved in + // the invoice settlement. + ExitHtlcExpiry uint32 + + // CurrentHeight is the current block height. + CurrentHeight uint32 + + // Invoice is the invoice that is being intercepted. + Invoice Invoice +} + +// InterceptorClientCallback is a function that is called when an invoice is +// intercepted by the invoice interceptor. +type InterceptorClientCallback func(InterceptClientRequest) error + +// SettlementInterceptorInterface is an interface that allows the caller to +// intercept and specify invoice settlement outcomes. +type SettlementInterceptorInterface interface { + // SetClientCallback sets the client callback function that is called + // when an invoice is intercepted. + SetClientCallback(InterceptorClientCallback) + + // Resolve is called by the caller to settle an invoice with the + // corresponding resolution. + Resolve(lntypes.Hash, bool) error +} diff --git a/invoices/settlement_interceptor.go b/invoices/settlement_interceptor.go new file mode 100644 index 00000000000..bbb57baefe4 --- /dev/null +++ b/invoices/settlement_interceptor.go @@ -0,0 +1,229 @@ +package invoices + +import ( + "fmt" + "sync" + + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnutils" +) + +// SafeCallback is a thread safe wrapper around an InterceptorClientCallback. +type SafeCallback struct { + // mu is a mutex that protects the callback field. + mu sync.Mutex + + // callback is the client callback function that is called when an + // invoice is intercepted. This function gives the client the ability to + // determine how the invoice should be settled. + callback InterceptorClientCallback +} + +// Set sets the client callback function. +func (sc *SafeCallback) Set(callback InterceptorClientCallback) { + sc.mu.Lock() + defer sc.mu.Unlock() + + sc.callback = callback +} + +// Exec calls the client callback function. +func (sc *SafeCallback) Exec(req InterceptClientRequest) error { + sc.mu.Lock() + defer sc.mu.Unlock() + + if sc.callback == nil { + return fmt.Errorf("client callback not set") + } + + return sc.callback(req) +} + +// IsSet returns true if the client callback function is set. +func (sc *SafeCallback) IsSet() bool { + sc.mu.Lock() + defer sc.mu.Unlock() + + return sc.callback != nil +} + +// InterceptClientResponse is the response that is sent from the client during +// an interceptor session. The response contains modifications to the invoice +// settlement process. +type InterceptClientResponse struct { + // SkipAmountCheck is a flag that indicates whether the amount check + // should be skipped during the invoice settlement process. + SkipAmountCheck bool +} + +// InterceptSession creates a session that is returned to the caller when an +// invoice is submitted to this service. This session allows the caller to block +// until the invoice is processed. +type InterceptSession struct { + InterceptClientRequest + + // ClientResponseChannel is a channel that is populated with the + // client's interceptor response during an interceptor session. + ClientResponseChannel chan InterceptClientResponse + + // Quit is a channel that is closed when the session is no longer + // needed. + Quit chan struct{} +} + +// SettlementInterceptor is a service that intercepts invoices during the +// settlement phase, enabling a subscribed client to determine the settlement +// outcome. +type SettlementInterceptor struct { + wg sync.WaitGroup + + // callback is a client defined function that is called when an invoice + // is intercepted. This function gives the client the ability to + // determine the settlement outcome. + clientCallback SafeCallback + + // activeSessions is a map of active intercept sessions that are used to + // manage the client query/response for a given invoice payment hash. + activeSessions lnutils.SyncMap[lntypes.Hash, InterceptSession] +} + +// NewSettlementInterceptor creates a new SettlementInterceptor. +func NewSettlementInterceptor() *SettlementInterceptor { + return &SettlementInterceptor{ + activeSessions: lnutils.SyncMap[ + lntypes.Hash, InterceptSession, + ]{}, + } +} + +// Intercept generates a new intercept session for the given invoice. The +// session is returned to the caller so that they can block until the +// client resolution is received. +func (s *SettlementInterceptor) Intercept( + clientRequest InterceptClientRequest) fn.Option[InterceptSession] { + + // If there is no client callback set we will not handle the invoice + // further. + if !s.clientCallback.IsSet() { + return fn.None[InterceptSession]() + } + + // Create and store a new intercept session for the invoice. We will use + // the payment hash as the storage/retrieval key for the session. + paymentHash := clientRequest.Invoice.Terms.PaymentPreimage.Hash() + session := InterceptSession{ + InterceptClientRequest: clientRequest, + ClientResponseChannel: make(chan InterceptClientResponse, 1), + Quit: make(chan struct{}, 1), + } + s.activeSessions.Store(paymentHash, session) + + // The callback function will block at the client's discretion. We will + // therefore execute it in a separate goroutine. + s.wg.Add(1) + go func() { + defer s.wg.Done() + + // By this point, we've already checked that the client callback + // is set. However, if the client callback has been set to nil + // since that check then Exec will return an error. + err := s.clientCallback.Exec(clientRequest) + if err != nil { + log.Errorf("client callback failed: %v", err) + } + }() + + // Return the session to the caller so that they can block until the + // resolution is received. + return fn.Some(session) +} + +// Resolve passes a client specified resolution to the session resolution +// channel associated with the given invoice payment hash. +func (s *SettlementInterceptor) Resolve(invoicePaymentHash lntypes.Hash, + skipAmountCheck bool) error { + + // Retrieve the intercept session for the invoice payment hash. + session, ok := s.activeSessions.LoadAndDelete( + invoicePaymentHash, + ) + if !ok { + return fmt.Errorf("invoice intercept session not found "+ + "(payment_hash=%s)", invoicePaymentHash.String()) + } + + // Send the resolution to the session resolution channel. + resolution := InterceptClientResponse{ + SkipAmountCheck: skipAmountCheck, + } + sendSuccessful := fn.SendOrQuit( + session.ClientResponseChannel, resolution, session.Quit, + ) + if !sendSuccessful { + return fmt.Errorf("failed to send resolution to client") + } + + return nil +} + +// SetClientCallback sets the client callback function that will be called when +// an invoice is intercepted. +func (s *SettlementInterceptor) SetClientCallback( + callback InterceptorClientCallback) { + + s.clientCallback.Set(callback) +} + +// QuitSession closes the quit channel for the session associated with the +// given invoice. This signals to the client that the session has ended. +func (s *SettlementInterceptor) QuitSession(session InterceptSession) error { + // Retrieve the intercept session and delete it from the local cache. + paymentHash := session.Invoice.Terms.PaymentPreimage.Hash() + session, ok := s.activeSessions.LoadAndDelete(paymentHash) + if !ok { + // If the session is not found, no further action is necessary. + return nil + } + + // Send to the quit channel to signal the client that the session has + // ended. + session.Quit <- struct{}{} + + return nil +} + +// QuitActiveSessions quits all active sessions by sending on each session quit +// channel. +func (s *SettlementInterceptor) QuitActiveSessions() error { + s.activeSessions.Range(func(_ lntypes.Hash, session InterceptSession) bool { //nolint:lll + session.Quit <- struct{}{} + + return true + }) + + // Empty the intercept sessions map. + s.activeSessions = lnutils.SyncMap[lntypes.Hash, InterceptSession]{} + + return nil +} + +// Start starts the service. +func (s *SettlementInterceptor) Start() error { + return nil +} + +// Stop stops the service. +func (s *SettlementInterceptor) Stop() error { + // If the service is stopping, we will quit all active sessions. + err := s.QuitActiveSessions() + if err != nil { + return err + } + + return nil +} + +// Ensure that SettlementInterceptor implements the HtlcResolutionInterceptor +// interface. +var _ SettlementInterceptorInterface = (*SettlementInterceptor)(nil) From 14a56fc2a502028793e11b2fbc24d7e35151a8a0 Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 30 Apr 2024 17:50:12 +0100 Subject: [PATCH 28/35] invoices: add `SkipAmountCheck` field to `invoiceUpdateCtx` type This commit introduces the `SkipAmountCheck` field to the `invoiceUpdateCtx` type. This field serves as a flag to determine whether to bypass the amount verification during the invoice settlement process. It is set based on the client's input, allowing the invoice to be settled even if the HTLC amount is less than the stated invoice amount. --- invoices/update.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/invoices/update.go b/invoices/update.go index ed60c278ca1..855d774bdaa 100644 --- a/invoices/update.go +++ b/invoices/update.go @@ -23,6 +23,10 @@ type invoiceUpdateCtx struct { mpp *record.MPP amp *record.AMP metadata []byte + + // SkipAmountCheck is a flag that indicates whether the amount check + // should be skipped during the invoice settlement process. + SkipAmountCheck bool } // invoiceRef returns an identifier that can be used to lookup or update the @@ -189,13 +193,13 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, } // Don't accept zero-valued sets. - if ctx.mpp.TotalMsat() == 0 { + if !ctx.SkipAmountCheck && ctx.mpp.TotalMsat() == 0 { return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil } // Check that the total amt of the htlc set is high enough. In case this // is a zero-valued invoice, it will always be enough. - if ctx.mpp.TotalMsat() < inv.Terms.Value { + if !ctx.SkipAmountCheck && ctx.mpp.TotalMsat() < inv.Terms.Value { return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil } @@ -204,7 +208,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, // Check whether total amt matches other htlcs in the set. var newSetTotal lnwire.MilliSatoshi for _, htlc := range htlcSet { - if ctx.mpp.TotalMsat() != htlc.MppTotalAmt { + if !ctx.SkipAmountCheck && ctx.mpp.TotalMsat() != htlc.MppTotalAmt { //nolint:lll return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil } @@ -239,7 +243,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc, // If the invoice cannot be settled yet, only record the htlc. setComplete := newSetTotal >= ctx.mpp.TotalMsat() - if !setComplete { + if !ctx.SkipAmountCheck && !setComplete { return &update, ctx.acceptRes(resultPartialAccepted), nil } From 434e8b0dbbe0aa9a5bae4c7a8e667a4169a63e0b Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 23 Apr 2024 14:33:31 +0100 Subject: [PATCH 29/35] invoices: integrate settlement interceptor with invoice registry This commit updates the invoice registry to utilize the settlement interceptor during the invoice settlement routine. It allows the interceptor to capture the invoice, providing interception clients an opportunity to determine the settlement outcome. --- invoices/invoiceregistry.go | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index de731b4740b..3481ae801d0 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -9,6 +9,7 @@ import ( "time" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/queue" @@ -74,6 +75,11 @@ type RegistryConfig struct { // KeysendHoldTime indicates for how long we want to accept and hold // spontaneous keysend payments. KeysendHoldTime time.Duration + + // SettlementInterceptor is a service that intercepts invoices during + // the settlement phase, enabling a subscribed client to determine the + // settlement outcome. + SettlementInterceptor *SettlementInterceptor } // htlcReleaseEvent describes an htlc auto-release event. It is used to release @@ -998,6 +1004,41 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( ) callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) { + // Provide the invoice to the settlement interceptor to allow + // the interceptor's client an opportunity to manipulate the + // settlement process. + var interceptSession fn.Option[InterceptSession] + if i.cfg.SettlementInterceptor != nil { + clientReq := InterceptClientRequest{ + ExitHtlcCircuitKey: ctx.circuitKey, + ExitHtlcAmt: ctx.amtPaid, + ExitHtlcExpiry: ctx.expiry, + CurrentHeight: uint32(ctx.currentHeight), + Invoice: *inv, + } + interceptSession = + i.cfg.SettlementInterceptor.Intercept(clientReq) + } + + // If the interceptor service has provided a response, we'll + // use the interceptor session to wait for the client to respond + // with a settlement resolution. + interceptSession.WhenSome(func(session InterceptSession) { + log.Debug("Waiting for client response from " + + "settlement interceptor session") + + select { + case resp := <-session.ClientResponseChannel: + log.Debugf("Received settlement interceptor "+ + "response: %v", resp) + ctx.SkipAmountCheck = resp.SkipAmountCheck + + case <-session.Quit: + // At this point, the interceptor session has + // quit. + } + }) + updateDesc, res, err := updateInvoice(ctx, inv) if err != nil { return nil, err @@ -1051,6 +1092,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( var invoiceToExpire invoiceExpiry + log.Debugf("Settlement resolution: %T %v", resolution, resolution) + switch res := resolution.(type) { case *HtlcFailResolution: // Inspect latest htlc state on the invoice. If it is found, @@ -1099,6 +1142,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( // with our peer. setID := ctx.setID() settledHtlcSet := invoice.HTLCSet(setID, HtlcStateSettled) + log.Debugf("Settled htlc set size: %d", len(settledHtlcSet)) + for key, htlc := range settledHtlcSet { preimage := res.Preimage if htlc.AMP != nil && htlc.AMP.Preimage != nil { @@ -1183,7 +1228,7 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( } // Now that the links have been notified of any state changes to their - // HTLCs, we'll go ahead and notify any clients wiaiting on the invoice + // HTLCs, we'll go ahead and notify any clients waiting on the invoice // state changes. if updateSubscribers { // We'll add a setID onto the notification, but only if this is From aa443251142842315fa7056a36527d9e27d74bfb Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 23 Apr 2024 14:38:47 +0100 Subject: [PATCH 30/35] lnd: initialize invoice settlement interceptor at server startup This commit initiates the invoice settlement interceptor during the main server startup, assigning it a handle within the server. --- server.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/server.go b/server.go index cbbdc96d467..36b59a5af8f 100644 --- a/server.go +++ b/server.go @@ -260,6 +260,8 @@ type server struct { invoices *invoices.InvoiceRegistry + invoiceSettlementInterceptor *invoices.SettlementInterceptor + channelNotifier *channelnotifier.ChannelNotifier peerNotifier *peernotifier.PeerNotifier @@ -556,6 +558,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr, return nil, err } + invoiceSettlementInterceptor := invoices.NewSettlementInterceptor() + registryConfig := invoices.RegistryConfig{ FinalCltvRejectDelta: lncfg.DefaultFinalCltvRejectDelta, HtlcHoldDuration: invoices.DefaultHtlcHoldDuration, @@ -565,6 +569,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, GcCanceledInvoicesOnStartup: cfg.GcCanceledInvoicesOnStartup, GcCanceledInvoicesOnTheFly: cfg.GcCanceledInvoicesOnTheFly, KeysendHoldTime: cfg.KeysendHoldTime, + SettlementInterceptor: invoiceSettlementInterceptor, } s := &server{ @@ -613,6 +618,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr, peerConnectedListeners: make(map[string][]chan<- lnpeer.Peer), peerDisconnectedListeners: make(map[string][]chan<- struct{}), + invoiceSettlementInterceptor: invoiceSettlementInterceptor, + customMessageServer: subscribe.NewServer(), tlsManager: tlsManager, @@ -2006,6 +2013,12 @@ func (s *server) Start() error { } cleanup = cleanup.add(s.interceptableSwitch.Stop) + if err := s.invoiceSettlementInterceptor.Start(); err != nil { + startErr = err + return + } + cleanup = cleanup.add(s.invoiceSettlementInterceptor.Stop) + if err := s.chainArb.Start(); err != nil { startErr = err return From 4e7acd4c855a6dcf47a0b2393ebc5910c7d7920f Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 23 Apr 2024 14:48:42 +0100 Subject: [PATCH 31/35] invoicesrpc: add settlement interceptor to invoices RPC server This commit integrates the settlement interceptor service into the invoices RPC server. --- lnd.go | 3 ++- lnrpc/invoicesrpc/config_active.go | 5 +++++ rpcserver.go | 17 ++++++++++------- subrpcserver_config.go | 5 +++++ 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lnd.go b/lnd.go index 9f628ff8125..526deb61bec 100644 --- a/lnd.go +++ b/lnd.go @@ -617,7 +617,8 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg, // start the RPC server. err = rpcServer.addDeps( server, interceptorChain.MacaroonService(), cfg.SubRPCServers, - atplManager, server.invoices, tower, multiAcceptor, + atplManager, server.invoices, + server.invoiceSettlementInterceptor, tower, multiAcceptor, ) if err != nil { return mkErr("unable to add deps to RPC server: %v", err) diff --git a/lnrpc/invoicesrpc/config_active.go b/lnrpc/invoicesrpc/config_active.go index a5b29c32a25..3324c6f82fe 100644 --- a/lnrpc/invoicesrpc/config_active.go +++ b/lnrpc/invoicesrpc/config_active.go @@ -30,6 +30,11 @@ type Config struct { // created by the daemon. InvoiceRegistry *invoices.InvoiceRegistry + // InvoiceSettlementInterceptor is a service which intercepts invoices + // during the settlement phase, enabling a subscribed client to + // determine the settlement outcome. + InvoiceSettlementInterceptor invoices.SettlementInterceptorInterface + // IsChannelActive is used to generate valid hop hints. IsChannelActive func(chanID lnwire.ChannelID) bool diff --git a/rpcserver.go b/rpcserver.go index c08291a9eff..83ca271ff80 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -659,7 +659,9 @@ func newRPCServer(cfg *Config, interceptorChain *rpcperms.InterceptorChain, // be started, and start accepting RPC calls. func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, subServerCgs *subRPCServerConfigs, atpl *autopilot.Manager, - invoiceRegistry *invoices.InvoiceRegistry, tower *watchtower.Standalone, + invoiceRegistry *invoices.InvoiceRegistry, + invoiceSettlementInterceptor *invoices.SettlementInterceptor, + tower *watchtower.Standalone, chanPredicate chanacceptor.MultiplexAcceptor) error { // Set up router rpc backend. @@ -758,12 +760,13 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, // TODO(roasbeef): extend sub-sever config to have both (local vs remote) DB err = subServerCgs.PopulateDependencies( r.cfg, s.cc, r.cfg.networkDir, macService, atpl, invoiceRegistry, - s.htlcSwitch, r.cfg.ActiveNetParams.Params, s.chanRouter, - routerBackend, s.nodeSigner, s.graphDB, s.chanStateDB, - s.sweeper, tower, s.towerClientMgr, r.cfg.net.ResolveTCPAddr, - genInvoiceFeatures, genAmpInvoiceFeatures, - s.getNodeAnnouncement, s.updateAndBrodcastSelfNode, parseAddr, - rpcsLog, s.aliasMgr.GetPeerAlias, + invoiceSettlementInterceptor, s.htlcSwitch, + r.cfg.ActiveNetParams.Params, s.chanRouter, routerBackend, + s.nodeSigner, s.graphDB, s.chanStateDB, s.sweeper, tower, + s.towerClientMgr, r.cfg.net.ResolveTCPAddr, genInvoiceFeatures, + genAmpInvoiceFeatures, s.getNodeAnnouncement, + s.updateAndBrodcastSelfNode, parseAddr, rpcsLog, + s.aliasMgr.GetPeerAlias, ) if err != nil { return err diff --git a/subrpcserver_config.go b/subrpcserver_config.go index 6687f71a7d4..d1584661068 100644 --- a/subrpcserver_config.go +++ b/subrpcserver_config.go @@ -104,6 +104,7 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, networkDir string, macService *macaroons.Service, atpl *autopilot.Manager, invoiceRegistry *invoices.InvoiceRegistry, + invoiceSettlementInterceptor *invoices.SettlementInterceptor, htlcSwitch *htlcswitch.Switch, activeNetParams *chaincfg.Params, chanRouter *routing.ChannelRouter, @@ -238,6 +239,10 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config, subCfgValue.FieldByName("InvoiceRegistry").Set( reflect.ValueOf(invoiceRegistry), ) + //nolint:lll + subCfgValue.FieldByName("InvoiceSettlementInterceptor").Set( + reflect.ValueOf(invoiceSettlementInterceptor), + ) subCfgValue.FieldByName("IsChannelActive").Set( reflect.ValueOf(htlcSwitch.HasActiveLink), ) From 471816ef447c6e899969756b6fe13e8b845c3fdf Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 23 Apr 2024 14:58:29 +0100 Subject: [PATCH 32/35] invoicesrpc: add `InvoiceAcceptor` RPC endpoint and acceptor RPC server This commit introduces a singleton invoice acceptor RPC server and an endpoint to activate it. The server interfaces with the internal invoice settlement interpreter, handling the marshalling between RPC types and internal formats. Named "acceptor," it allows clients to accept invoice settlements, but not to reject them. --- lnrpc/invoicesrpc/invoice_acceptor.go | 111 +++++++ lnrpc/invoicesrpc/invoices.pb.go | 391 ++++++++++++++++++++---- lnrpc/invoicesrpc/invoices.proto | 49 +++ lnrpc/invoicesrpc/invoices.swagger.json | 45 +++ lnrpc/invoicesrpc/invoices_grpc.pb.go | 74 +++++ lnrpc/invoicesrpc/invoices_server.go | 36 +++ 6 files changed, 651 insertions(+), 55 deletions(-) create mode 100644 lnrpc/invoicesrpc/invoice_acceptor.go diff --git a/lnrpc/invoicesrpc/invoice_acceptor.go b/lnrpc/invoicesrpc/invoice_acceptor.go new file mode 100644 index 00000000000..3048a9e52ae --- /dev/null +++ b/lnrpc/invoicesrpc/invoice_acceptor.go @@ -0,0 +1,111 @@ +package invoicesrpc + +import ( + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightningnetwork/lnd/invoices" + "github.com/lightningnetwork/lnd/lntypes" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// invoiceAcceptorConfig contains the configuration for an RPC invoice acceptor +// server. +type invoiceAcceptorConfig struct { + // chainParams is required to properly marshall an invoice for RPC. + chainParams *chaincfg.Params + + // rpcServer is a bidirectional RPC server to send invoices to a client + // and receive accept responses from the client. + rpcServer Invoices_InvoiceAcceptorServer + + // interceptor is the invoice interceptor that will be used to intercept + // and resolve invoices. + interceptor invoices.SettlementInterceptorInterface +} + +// invoiceAcceptor is a helper struct that handles the lifecycle of an RPC +// invoice acceptor server instance. +// +// This struct handles passing send and receive RPC messages between the client +// and the invoice service. +type invoiceAcceptor struct { + // cfg contains the configuration for the invoice acceptor. + cfg invoiceAcceptorConfig +} + +// newInvoiceAcceptor creates a new RPC invoice acceptor handler. +func newInvoiceAcceptor(cfg invoiceAcceptorConfig) *invoiceAcceptor { + return &invoiceAcceptor{ + cfg: cfg, + } +} + +// run sends the intercepted invoices to the client and receives the +// corresponding responses. +func (r *invoiceAcceptor) run() error { + // Register our invoice interceptor. + r.cfg.interceptor.SetClientCallback(r.onIntercept) + defer r.cfg.interceptor.SetClientCallback(nil) + + // Listen for a response from the client in a loop. + for { + resp, err := r.cfg.rpcServer.Recv() + if err != nil { + return err + } + + if err := r.resolveFromClient(resp); err != nil { + return err + } + } +} + +// onIntercept is called when an invoice is intercepted by the invoice +// interceptor. This method sends the invoice to the client. +func (r *invoiceAcceptor) onIntercept( + req invoices.InterceptClientRequest) error { + + // Convert the circuit key to an RPC circuit key. + rpcCircuitKey := &CircuitKey{ + ChanId: req.ExitHtlcCircuitKey.ChanID.ToUint64(), + HtlcId: req.ExitHtlcCircuitKey.HtlcID, + } + + // Convert the invoice to an RPC invoice. + rpcInvoice, err := CreateRPCInvoice(&req.Invoice, r.cfg.chainParams) + if err != nil { + return err + } + + return r.cfg.rpcServer.Send(&InvoiceAcceptorRequest{ + Invoice: rpcInvoice, + ExitHtlcCircuitKey: rpcCircuitKey, + ExitHtlcAmt: uint64(req.ExitHtlcAmt), + ExitHtlcExpiry: req.ExitHtlcExpiry, + CurrentHeight: req.CurrentHeight, + }) +} + +// resolveFromClient handles an invoice resolution received from the client. +func (r *invoiceAcceptor) resolveFromClient( + in *InvoiceAcceptorResponse) error { + + log.Tracef("Resolving invoice acceptor response %v", in) + + // Parse the invoice preimage from the response. + if len(in.Preimage) != lntypes.HashSize { + return status.Errorf(codes.InvalidArgument, + "Preimage has invalid length: %d", len(in.Preimage)) + } + preimage, err := lntypes.MakePreimage(in.Preimage) + if err != nil { + return status.Errorf(codes.InvalidArgument, + "Preimage is invalid: %v", err) + } + + // Derive the payment hash from the preimage. + paymentHash := preimage.Hash() + + // Pass the resolution to the interceptor. + return r.cfg.interceptor.Resolve(paymentHash, in.SkipAmountCheck) +} diff --git a/lnrpc/invoicesrpc/invoices.pb.go b/lnrpc/invoicesrpc/invoices.pb.go index 27cf424d30d..e33f270a82a 100644 --- a/lnrpc/invoicesrpc/invoices.pb.go +++ b/lnrpc/invoicesrpc/invoices.pb.go @@ -617,6 +617,212 @@ func (*LookupInvoiceMsg_PaymentAddr) isLookupInvoiceMsg_InvoiceRef() {} func (*LookupInvoiceMsg_SetId) isLookupInvoiceMsg_InvoiceRef() {} +// CircuitKey is a unique identifier for an HTLC. +type CircuitKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // / The id of the channel that the is part of this circuit. + ChanId uint64 `protobuf:"varint,1,opt,name=chan_id,json=chanId,proto3" json:"chan_id,omitempty"` + // / The index of the incoming htlc in the incoming channel. + HtlcId uint64 `protobuf:"varint,2,opt,name=htlc_id,json=htlcId,proto3" json:"htlc_id,omitempty"` +} + +func (x *CircuitKey) Reset() { + *x = CircuitKey{} + if protoimpl.UnsafeEnabled { + mi := &file_invoicesrpc_invoices_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CircuitKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CircuitKey) ProtoMessage() {} + +func (x *CircuitKey) ProtoReflect() protoreflect.Message { + mi := &file_invoicesrpc_invoices_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CircuitKey.ProtoReflect.Descriptor instead. +func (*CircuitKey) Descriptor() ([]byte, []int) { + return file_invoicesrpc_invoices_proto_rawDescGZIP(), []int{8} +} + +func (x *CircuitKey) GetChanId() uint64 { + if x != nil { + return x.ChanId + } + return 0 +} + +func (x *CircuitKey) GetHtlcId() uint64 { + if x != nil { + return x.HtlcId + } + return 0 +} + +// InvoiceAcceptorRequest is a message that the server sends to the client to +// request the client to accept an invoice. +type InvoiceAcceptorRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // invoice is the invoice that the client should consider accepting. + Invoice *lnrpc.Invoice `protobuf:"bytes,1,opt,name=invoice,proto3" json:"invoice,omitempty"` + // exit_htlc_circuit_key is the key of the HTLC for which we is involved in + // the invoice settlement attempt. + ExitHtlcCircuitKey *CircuitKey `protobuf:"bytes,2,opt,name=exit_htlc_circuit_key,json=exitHtlcCircuitKey,proto3" json:"exit_htlc_circuit_key,omitempty"` + // exit_htlc_amt is the amount (millisats) that the client should consider + // accepting. + ExitHtlcAmt uint64 `protobuf:"varint,3,opt,name=exit_htlc_amt,json=exitHtlcAmt,proto3" json:"exit_htlc_amt,omitempty"` + // exit_htlc_expiry is the expiry time of the exit HTLC. + ExitHtlcExpiry uint32 `protobuf:"varint,4,opt,name=exit_htlc_expiry,json=exitHtlcExpiry,proto3" json:"exit_htlc_expiry,omitempty"` + // current_height is the current block height. + CurrentHeight uint32 `protobuf:"varint,5,opt,name=current_height,json=currentHeight,proto3" json:"current_height,omitempty"` +} + +func (x *InvoiceAcceptorRequest) Reset() { + *x = InvoiceAcceptorRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_invoicesrpc_invoices_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InvoiceAcceptorRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvoiceAcceptorRequest) ProtoMessage() {} + +func (x *InvoiceAcceptorRequest) ProtoReflect() protoreflect.Message { + mi := &file_invoicesrpc_invoices_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvoiceAcceptorRequest.ProtoReflect.Descriptor instead. +func (*InvoiceAcceptorRequest) Descriptor() ([]byte, []int) { + return file_invoicesrpc_invoices_proto_rawDescGZIP(), []int{9} +} + +func (x *InvoiceAcceptorRequest) GetInvoice() *lnrpc.Invoice { + if x != nil { + return x.Invoice + } + return nil +} + +func (x *InvoiceAcceptorRequest) GetExitHtlcCircuitKey() *CircuitKey { + if x != nil { + return x.ExitHtlcCircuitKey + } + return nil +} + +func (x *InvoiceAcceptorRequest) GetExitHtlcAmt() uint64 { + if x != nil { + return x.ExitHtlcAmt + } + return 0 +} + +func (x *InvoiceAcceptorRequest) GetExitHtlcExpiry() uint32 { + if x != nil { + return x.ExitHtlcExpiry + } + return 0 +} + +func (x *InvoiceAcceptorRequest) GetCurrentHeight() uint32 { + if x != nil { + return x.CurrentHeight + } + return 0 +} + +// InvoiceAcceptorResponse is a message that the client sends to the server to +// indicate whether it accepts the invoice or not. +type InvoiceAcceptorResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // preimage is the preimage of the invoice. + Preimage []byte `protobuf:"bytes,1,opt,name=preimage,proto3" json:"preimage,omitempty"` + // skip_amount_check is a flag that indicates whether the client wants to + // skip the amount check during the invoice settlement process. + SkipAmountCheck bool `protobuf:"varint,2,opt,name=skip_amount_check,json=skipAmountCheck,proto3" json:"skip_amount_check,omitempty"` +} + +func (x *InvoiceAcceptorResponse) Reset() { + *x = InvoiceAcceptorResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_invoicesrpc_invoices_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InvoiceAcceptorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InvoiceAcceptorResponse) ProtoMessage() {} + +func (x *InvoiceAcceptorResponse) ProtoReflect() protoreflect.Message { + mi := &file_invoicesrpc_invoices_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InvoiceAcceptorResponse.ProtoReflect.Descriptor instead. +func (*InvoiceAcceptorResponse) Descriptor() ([]byte, []int) { + return file_invoicesrpc_invoices_proto_rawDescGZIP(), []int{10} +} + +func (x *InvoiceAcceptorResponse) GetPreimage() []byte { + if x != nil { + return x.Preimage + } + return nil +} + +func (x *InvoiceAcceptorResponse) GetSkipAmountCheck() bool { + if x != nil { + return x.SkipAmountCheck + } + return false +} + var File_invoicesrpc_invoices_proto protoreflect.FileDescriptor var file_invoicesrpc_invoices_proto_rawDesc = []byte{ @@ -678,41 +884,73 @@ var file_invoicesrpc_invoices_proto_rawDesc = []byte{ 0x73, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0e, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x42, 0x0d, 0x0a, 0x0b, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x72, - 0x65, 0x66, 0x2a, 0x44, 0x0a, 0x0e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, - 0x66, 0x69, 0x65, 0x72, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, - 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x4e, - 0x4c, 0x59, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, - 0x5f, 0x42, 0x4c, 0x41, 0x4e, 0x4b, 0x10, 0x02, 0x32, 0x9b, 0x03, 0x0a, 0x08, 0x49, 0x6e, 0x76, - 0x6f, 0x69, 0x63, 0x65, 0x73, 0x12, 0x56, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, - 0x2a, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, - 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, - 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x30, 0x01, 0x12, 0x4e, 0x0a, - 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, - 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, - 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, - 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, - 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x55, 0x0a, - 0x0e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, - 0x22, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, - 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, - 0x63, 0x2e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x12, 0x4e, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, - 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, - 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x12, 0x40, 0x0a, 0x0f, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, - 0x76, 0x6f, 0x69, 0x63, 0x65, 0x56, 0x32, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f, - 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, - 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f, - 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x65, 0x66, 0x22, 0x3e, 0x0a, 0x0a, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, 0x79, + 0x12, 0x17, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x74, 0x6c, + 0x63, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x74, 0x6c, 0x63, + 0x49, 0x64, 0x22, 0x83, 0x02, 0x0a, 0x16, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, + 0x07, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, + 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x07, + 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x15, 0x65, 0x78, 0x69, 0x74, 0x5f, + 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x63, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x5f, 0x6b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x52, + 0x12, 0x65, 0x78, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x43, 0x69, 0x72, 0x63, 0x75, 0x69, 0x74, + 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0d, 0x65, 0x78, 0x69, 0x74, 0x5f, 0x68, 0x74, 0x6c, 0x63, + 0x5f, 0x61, 0x6d, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x65, 0x78, 0x69, 0x74, + 0x48, 0x74, 0x6c, 0x63, 0x41, 0x6d, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x65, 0x78, 0x69, 0x74, 0x5f, + 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0e, 0x65, 0x78, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x78, 0x70, 0x69, 0x72, + 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x65, 0x69, + 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x63, 0x75, 0x72, 0x72, 0x65, + 0x6e, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x61, 0x0a, 0x17, 0x49, 0x6e, 0x76, 0x6f, + 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, + 0x2a, 0x0a, 0x11, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x73, 0x6b, 0x69, 0x70, + 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x2a, 0x44, 0x0a, 0x0e, 0x4c, + 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x0b, 0x0a, + 0x07, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x48, 0x54, + 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x01, 0x12, 0x12, 0x0a, + 0x0e, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x42, 0x4c, 0x41, 0x4e, 0x4b, 0x10, + 0x02, 0x32, 0xfd, 0x03, 0x0a, 0x08, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x12, 0x56, + 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x53, 0x69, 0x6e, 0x67, 0x6c, + 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x2a, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, + 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, + 0x6f, 0x69, 0x63, 0x65, 0x30, 0x01, 0x12, 0x4e, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, + 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, + 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, + 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, + 0x73, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x49, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x55, 0x0a, 0x0e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, + 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x22, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x48, 0x6f, 0x6c, 0x64, 0x49, 0x6e, + 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x69, + 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x48, 0x6f, + 0x6c, 0x64, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x4e, 0x0a, + 0x0d, 0x53, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x1d, + 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, + 0x74, 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, 0x1e, 0x2e, + 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x74, 0x74, + 0x6c, 0x65, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x40, 0x0a, + 0x0f, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x56, 0x32, + 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x4c, + 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x4d, 0x73, 0x67, 0x1a, + 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, + 0x60, 0x0a, 0x0f, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, + 0x6f, 0x72, 0x12, 0x24, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, + 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x23, 0x2e, 0x69, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x28, 0x01, 0x30, + 0x01, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x69, 0x6e, 0x76, 0x6f, 0x69, + 0x63, 0x65, 0x73, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -728,7 +966,7 @@ func file_invoicesrpc_invoices_proto_rawDescGZIP() []byte { } var file_invoicesrpc_invoices_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_invoicesrpc_invoices_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_invoicesrpc_invoices_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_invoicesrpc_invoices_proto_goTypes = []interface{}{ (LookupModifier)(0), // 0: invoicesrpc.LookupModifier (*CancelInvoiceMsg)(nil), // 1: invoicesrpc.CancelInvoiceMsg @@ -739,27 +977,34 @@ var file_invoicesrpc_invoices_proto_goTypes = []interface{}{ (*SettleInvoiceResp)(nil), // 6: invoicesrpc.SettleInvoiceResp (*SubscribeSingleInvoiceRequest)(nil), // 7: invoicesrpc.SubscribeSingleInvoiceRequest (*LookupInvoiceMsg)(nil), // 8: invoicesrpc.LookupInvoiceMsg - (*lnrpc.RouteHint)(nil), // 9: lnrpc.RouteHint - (*lnrpc.Invoice)(nil), // 10: lnrpc.Invoice + (*CircuitKey)(nil), // 9: invoicesrpc.CircuitKey + (*InvoiceAcceptorRequest)(nil), // 10: invoicesrpc.InvoiceAcceptorRequest + (*InvoiceAcceptorResponse)(nil), // 11: invoicesrpc.InvoiceAcceptorResponse + (*lnrpc.RouteHint)(nil), // 12: lnrpc.RouteHint + (*lnrpc.Invoice)(nil), // 13: lnrpc.Invoice } var file_invoicesrpc_invoices_proto_depIdxs = []int32{ - 9, // 0: invoicesrpc.AddHoldInvoiceRequest.route_hints:type_name -> lnrpc.RouteHint + 12, // 0: invoicesrpc.AddHoldInvoiceRequest.route_hints:type_name -> lnrpc.RouteHint 0, // 1: invoicesrpc.LookupInvoiceMsg.lookup_modifier:type_name -> invoicesrpc.LookupModifier - 7, // 2: invoicesrpc.Invoices.SubscribeSingleInvoice:input_type -> invoicesrpc.SubscribeSingleInvoiceRequest - 1, // 3: invoicesrpc.Invoices.CancelInvoice:input_type -> invoicesrpc.CancelInvoiceMsg - 3, // 4: invoicesrpc.Invoices.AddHoldInvoice:input_type -> invoicesrpc.AddHoldInvoiceRequest - 5, // 5: invoicesrpc.Invoices.SettleInvoice:input_type -> invoicesrpc.SettleInvoiceMsg - 8, // 6: invoicesrpc.Invoices.LookupInvoiceV2:input_type -> invoicesrpc.LookupInvoiceMsg - 10, // 7: invoicesrpc.Invoices.SubscribeSingleInvoice:output_type -> lnrpc.Invoice - 2, // 8: invoicesrpc.Invoices.CancelInvoice:output_type -> invoicesrpc.CancelInvoiceResp - 4, // 9: invoicesrpc.Invoices.AddHoldInvoice:output_type -> invoicesrpc.AddHoldInvoiceResp - 6, // 10: invoicesrpc.Invoices.SettleInvoice:output_type -> invoicesrpc.SettleInvoiceResp - 10, // 11: invoicesrpc.Invoices.LookupInvoiceV2:output_type -> lnrpc.Invoice - 7, // [7:12] is the sub-list for method output_type - 2, // [2:7] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 13, // 2: invoicesrpc.InvoiceAcceptorRequest.invoice:type_name -> lnrpc.Invoice + 9, // 3: invoicesrpc.InvoiceAcceptorRequest.exit_htlc_circuit_key:type_name -> invoicesrpc.CircuitKey + 7, // 4: invoicesrpc.Invoices.SubscribeSingleInvoice:input_type -> invoicesrpc.SubscribeSingleInvoiceRequest + 1, // 5: invoicesrpc.Invoices.CancelInvoice:input_type -> invoicesrpc.CancelInvoiceMsg + 3, // 6: invoicesrpc.Invoices.AddHoldInvoice:input_type -> invoicesrpc.AddHoldInvoiceRequest + 5, // 7: invoicesrpc.Invoices.SettleInvoice:input_type -> invoicesrpc.SettleInvoiceMsg + 8, // 8: invoicesrpc.Invoices.LookupInvoiceV2:input_type -> invoicesrpc.LookupInvoiceMsg + 11, // 9: invoicesrpc.Invoices.InvoiceAcceptor:input_type -> invoicesrpc.InvoiceAcceptorResponse + 13, // 10: invoicesrpc.Invoices.SubscribeSingleInvoice:output_type -> lnrpc.Invoice + 2, // 11: invoicesrpc.Invoices.CancelInvoice:output_type -> invoicesrpc.CancelInvoiceResp + 4, // 12: invoicesrpc.Invoices.AddHoldInvoice:output_type -> invoicesrpc.AddHoldInvoiceResp + 6, // 13: invoicesrpc.Invoices.SettleInvoice:output_type -> invoicesrpc.SettleInvoiceResp + 13, // 14: invoicesrpc.Invoices.LookupInvoiceV2:output_type -> lnrpc.Invoice + 10, // 15: invoicesrpc.Invoices.InvoiceAcceptor:output_type -> invoicesrpc.InvoiceAcceptorRequest + 10, // [10:16] is the sub-list for method output_type + 4, // [4:10] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_invoicesrpc_invoices_proto_init() } @@ -864,6 +1109,42 @@ func file_invoicesrpc_invoices_proto_init() { return nil } } + file_invoicesrpc_invoices_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CircuitKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_invoicesrpc_invoices_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InvoiceAcceptorRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_invoicesrpc_invoices_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InvoiceAcceptorResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_invoicesrpc_invoices_proto_msgTypes[7].OneofWrappers = []interface{}{ (*LookupInvoiceMsg_PaymentHash)(nil), @@ -876,7 +1157,7 @@ func file_invoicesrpc_invoices_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_invoicesrpc_invoices_proto_rawDesc, NumEnums: 1, - NumMessages: 8, + NumMessages: 11, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/invoicesrpc/invoices.proto b/lnrpc/invoicesrpc/invoices.proto index 7afffba4e3a..251d0d09479 100644 --- a/lnrpc/invoicesrpc/invoices.proto +++ b/lnrpc/invoicesrpc/invoices.proto @@ -59,6 +59,14 @@ service Invoices { using either its payment hash, payment address, or set ID. */ rpc LookupInvoiceV2 (LookupInvoiceMsg) returns (lnrpc.Invoice); + + /* + InvoiceAcceptor is a bi-directional streaming RPC that allows a client to + accept invoices. The server will send invoices to the client and the client + can respond with whether it accepts the invoice or not. + */ + rpc InvoiceAcceptor (stream InvoiceAcceptorResponse) + returns (stream InvoiceAcceptorRequest); } message CancelInvoiceMsg { @@ -192,3 +200,44 @@ message LookupInvoiceMsg { LookupModifier lookup_modifier = 4; } + +// CircuitKey is a unique identifier for an HTLC. +message CircuitKey { + /// The id of the channel that the is part of this circuit. + uint64 chan_id = 1; + + /// The index of the incoming htlc in the incoming channel. + uint64 htlc_id = 2; +} + +// InvoiceAcceptorRequest is a message that the server sends to the client to +// request the client to accept an invoice. +message InvoiceAcceptorRequest { + // invoice is the invoice that the client should consider accepting. + lnrpc.Invoice invoice = 1; + + // exit_htlc_circuit_key is the key of the HTLC for which we is involved in + // the invoice settlement attempt. + CircuitKey exit_htlc_circuit_key = 2; + + // exit_htlc_amt is the amount (millisats) that the client should consider + // accepting. + uint64 exit_htlc_amt = 3; + + // exit_htlc_expiry is the expiry time of the exit HTLC. + uint32 exit_htlc_expiry = 4; + + // current_height is the current block height. + uint32 current_height = 5; +} + +// InvoiceAcceptorResponse is a message that the client sends to the server to +// indicate whether it accepts the invoice or not. +message InvoiceAcceptorResponse { + // preimage is the preimage of the invoice. + bytes preimage = 1; + + // skip_amount_check is a flag that indicates whether the client wants to + // skip the amount check during the invoice settlement process. + bool skip_amount_check = 2; +} diff --git a/lnrpc/invoicesrpc/invoices.swagger.json b/lnrpc/invoicesrpc/invoices.swagger.json index b54d9fb38e1..cfefaff4300 100644 --- a/lnrpc/invoicesrpc/invoices.swagger.json +++ b/lnrpc/invoicesrpc/invoices.swagger.json @@ -317,6 +317,51 @@ "invoicesrpcCancelInvoiceResp": { "type": "object" }, + "invoicesrpcCircuitKey": { + "type": "object", + "properties": { + "chan_id": { + "type": "string", + "format": "uint64", + "description": "/ The id of the channel that the is part of this circuit." + }, + "htlc_id": { + "type": "string", + "format": "uint64", + "description": "/ The index of the incoming htlc in the incoming channel." + } + }, + "description": "CircuitKey is a unique identifier for an HTLC." + }, + "invoicesrpcInvoiceAcceptorRequest": { + "type": "object", + "properties": { + "invoice": { + "$ref": "#/definitions/lnrpcInvoice", + "description": "invoice is the invoice that the client should consider accepting." + }, + "exit_htlc_circuit_key": { + "$ref": "#/definitions/invoicesrpcCircuitKey", + "description": "exit_htlc_circuit_key is the key of the HTLC for which we is involved in\nthe invoice settlement attempt." + }, + "exit_htlc_amt": { + "type": "string", + "format": "uint64", + "description": "exit_htlc_amt is the amount (millisats) that the client should consider\naccepting." + }, + "exit_htlc_expiry": { + "type": "integer", + "format": "int64", + "description": "exit_htlc_expiry is the expiry time of the exit HTLC." + }, + "current_height": { + "type": "integer", + "format": "int64", + "description": "current_height is the current block height." + } + }, + "description": "InvoiceAcceptorRequest is a message that the server sends to the client to\nrequest the client to accept an invoice." + }, "invoicesrpcLookupModifier": { "type": "string", "enum": [ diff --git a/lnrpc/invoicesrpc/invoices_grpc.pb.go b/lnrpc/invoicesrpc/invoices_grpc.pb.go index bc2f24ac101..398a2479e50 100644 --- a/lnrpc/invoicesrpc/invoices_grpc.pb.go +++ b/lnrpc/invoicesrpc/invoices_grpc.pb.go @@ -39,6 +39,10 @@ type InvoicesClient interface { // LookupInvoiceV2 attempts to look up at invoice. An invoice can be refrenced // using either its payment hash, payment address, or set ID. LookupInvoiceV2(ctx context.Context, in *LookupInvoiceMsg, opts ...grpc.CallOption) (*lnrpc.Invoice, error) + // InvoiceAcceptor is a bi-directional streaming RPC that allows a client to + // accept invoices. The server will send invoices to the client and the client + // can respond with whether it accepts the invoice or not. + InvoiceAcceptor(ctx context.Context, opts ...grpc.CallOption) (Invoices_InvoiceAcceptorClient, error) } type invoicesClient struct { @@ -117,6 +121,37 @@ func (c *invoicesClient) LookupInvoiceV2(ctx context.Context, in *LookupInvoiceM return out, nil } +func (c *invoicesClient) InvoiceAcceptor(ctx context.Context, opts ...grpc.CallOption) (Invoices_InvoiceAcceptorClient, error) { + stream, err := c.cc.NewStream(ctx, &Invoices_ServiceDesc.Streams[1], "/invoicesrpc.Invoices/InvoiceAcceptor", opts...) + if err != nil { + return nil, err + } + x := &invoicesInvoiceAcceptorClient{stream} + return x, nil +} + +type Invoices_InvoiceAcceptorClient interface { + Send(*InvoiceAcceptorResponse) error + Recv() (*InvoiceAcceptorRequest, error) + grpc.ClientStream +} + +type invoicesInvoiceAcceptorClient struct { + grpc.ClientStream +} + +func (x *invoicesInvoiceAcceptorClient) Send(m *InvoiceAcceptorResponse) error { + return x.ClientStream.SendMsg(m) +} + +func (x *invoicesInvoiceAcceptorClient) Recv() (*InvoiceAcceptorRequest, error) { + m := new(InvoiceAcceptorRequest) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // InvoicesServer is the server API for Invoices service. // All implementations must embed UnimplementedInvoicesServer // for forward compatibility @@ -141,6 +176,10 @@ type InvoicesServer interface { // LookupInvoiceV2 attempts to look up at invoice. An invoice can be refrenced // using either its payment hash, payment address, or set ID. LookupInvoiceV2(context.Context, *LookupInvoiceMsg) (*lnrpc.Invoice, error) + // InvoiceAcceptor is a bi-directional streaming RPC that allows a client to + // accept invoices. The server will send invoices to the client and the client + // can respond with whether it accepts the invoice or not. + InvoiceAcceptor(Invoices_InvoiceAcceptorServer) error mustEmbedUnimplementedInvoicesServer() } @@ -163,6 +202,9 @@ func (UnimplementedInvoicesServer) SettleInvoice(context.Context, *SettleInvoice func (UnimplementedInvoicesServer) LookupInvoiceV2(context.Context, *LookupInvoiceMsg) (*lnrpc.Invoice, error) { return nil, status.Errorf(codes.Unimplemented, "method LookupInvoiceV2 not implemented") } +func (UnimplementedInvoicesServer) InvoiceAcceptor(Invoices_InvoiceAcceptorServer) error { + return status.Errorf(codes.Unimplemented, "method InvoiceAcceptor not implemented") +} func (UnimplementedInvoicesServer) mustEmbedUnimplementedInvoicesServer() {} // UnsafeInvoicesServer may be embedded to opt out of forward compatibility for this service. @@ -269,6 +311,32 @@ func _Invoices_LookupInvoiceV2_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _Invoices_InvoiceAcceptor_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(InvoicesServer).InvoiceAcceptor(&invoicesInvoiceAcceptorServer{stream}) +} + +type Invoices_InvoiceAcceptorServer interface { + Send(*InvoiceAcceptorRequest) error + Recv() (*InvoiceAcceptorResponse, error) + grpc.ServerStream +} + +type invoicesInvoiceAcceptorServer struct { + grpc.ServerStream +} + +func (x *invoicesInvoiceAcceptorServer) Send(m *InvoiceAcceptorRequest) error { + return x.ServerStream.SendMsg(m) +} + +func (x *invoicesInvoiceAcceptorServer) Recv() (*InvoiceAcceptorResponse, error) { + m := new(InvoiceAcceptorResponse) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // Invoices_ServiceDesc is the grpc.ServiceDesc for Invoices service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -299,6 +367,12 @@ var Invoices_ServiceDesc = grpc.ServiceDesc{ Handler: _Invoices_SubscribeSingleInvoice_Handler, ServerStreams: true, }, + { + StreamName: "InvoiceAcceptor", + Handler: _Invoices_InvoiceAcceptor_Handler, + ServerStreams: true, + ClientStreams: true, + }, }, Metadata: "invoicesrpc/invoices.proto", } diff --git a/lnrpc/invoicesrpc/invoices_server.go b/lnrpc/invoicesrpc/invoices_server.go index b62538543df..7ec03930058 100644 --- a/lnrpc/invoicesrpc/invoices_server.go +++ b/lnrpc/invoicesrpc/invoices_server.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "os" "path/filepath" + "sync/atomic" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/lightningnetwork/lnd/invoices" @@ -31,6 +32,12 @@ const ( ) var ( + // ErrInvoiceAcceptorAlreadyExists signals that an invoice acceptor + // already exists. The user must disconnect an existing invoice accptor + // prior to opening another stream. + ErrInvoiceAcceptorAlreadyExists = errors.New("interceptor already " + + "exists") + // macaroonOps are the set of capabilities that our minted macaroon (if // it doesn't already exist) will have. macaroonOps = []bakery.Op{ @@ -66,6 +73,10 @@ var ( Entity: "invoices", Action: "write", }}, + "/invoicesrpc.Invoices/InvoiceAcceptor": {{ + Entity: "invoices", + Action: "write", + }}, } // DefaultInvoicesMacFilename is the default name of the invoices @@ -88,6 +99,12 @@ type Server struct { // Required by the grpc-gateway/v2 library for forward compatibility. UnimplementedInvoicesServer + // invoiceAcceptorActive is an atomic flag that indicates whether an + // invoice acceptor RPC server is currently active. It is used to ensure + // that only one invoice acceptor RPC server instance is running at a + // time. + invoiceAcceptorActive atomic.Int32 + quit chan struct{} cfg *Config @@ -447,3 +464,22 @@ func (s *Server) LookupInvoiceV2(ctx context.Context, return CreateRPCInvoice(&invoice, s.cfg.ChainParams) } + +// InvoiceAcceptor is a bidirectional streaming RPC that allows a client to +// inspect and optionally accept invoices. +func (s *Server) InvoiceAcceptor( + acceptorServer Invoices_InvoiceAcceptorServer) error { + + // Ensure that there is only one invoice acceptor RPC server instance. + if !s.invoiceAcceptorActive.CompareAndSwap(0, 1) { + return ErrInvoiceAcceptorAlreadyExists + } + defer s.invoiceAcceptorActive.CompareAndSwap(1, 0) + + // Run the invoice acceptor. + return newInvoiceAcceptor(invoiceAcceptorConfig{ + chainParams: s.cfg.ChainParams, + interceptor: s.cfg.InvoiceSettlementInterceptor, + rpcServer: acceptorServer, + }).run() +} From 91d56c702e9ef2e293b37cf5ef73c2e06a1eace2 Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 30 Apr 2024 17:51:43 +0100 Subject: [PATCH 33/35] lntest: add `InvoiceAcceptor` support to node RPC harness This commit enhances the itest LND node harness to include support for the new `InvoiceAcceptor` RPC endpoint. --- lntest/harness.go | 34 ++++++++++++++++++++++++++++++++++ lntest/rpc/router.go | 18 ++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lntest/harness.go b/lntest/harness.go index c291e7cf396..037ee657995 100644 --- a/lntest/harness.go +++ b/lntest/harness.go @@ -16,6 +16,7 @@ import ( "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/kvdb/etcd" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest/node" @@ -2183,6 +2184,39 @@ func (h *HarnessTest) ReceiveHtlcInterceptor( return nil } +// ReceiveInvoiceAcceptor waits until a message is received on the invoice +// acceptor interceptor stream or the timeout is reached. +func (h *HarnessTest) ReceiveInvoiceAcceptor( + stream rpc.InvoiceAcceptorClient) *invoicesrpc.InvoiceAcceptorRequest { + + chanMsg := make(chan *invoicesrpc.InvoiceAcceptorRequest) + errChan := make(chan error) + go func() { + // Consume one message. This will block until the message is + // received. + resp, err := stream.Recv() + if err != nil { + errChan <- err + return + } + chanMsg <- resp + }() + + select { + case <-time.After(DefaultTimeout): + require.Fail(h, "timeout", "timeout invoice acceptor") + + case err := <-errChan: + require.Failf(h, "err from stream", + "received err from stream: %v", err) + + case updateMsg := <-chanMsg: + return updateMsg + } + + return nil +} + // ReceiveChannelEvent waits until a message is received from the // ChannelEventsClient stream or the timeout is reached. func (h *HarnessTest) ReceiveChannelEvent( diff --git a/lntest/rpc/router.go b/lntest/rpc/router.go index fbf44cb18a2..872e44bf56a 100644 --- a/lntest/rpc/router.go +++ b/lntest/rpc/router.go @@ -4,6 +4,7 @@ import ( "context" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/stretchr/testify/require" ) @@ -193,6 +194,23 @@ func (h *HarnessRPC) HtlcInterceptor() (InterceptorClient, context.CancelFunc) { return resp, cancel } +type InvoiceAcceptorClient invoicesrpc.Invoices_InvoiceAcceptorClient + +// InvoiceAcceptor makes an RPC call to the node's RouterClient and asserts. +func (h *HarnessRPC) InvoiceAcceptor() (InvoiceAcceptorClient, + context.CancelFunc) { + + // InvoiceAcceptor needs to have the context alive for the entire test + // case as the returned client will be used for send and receive events + // stream. Therefore, we use cancel context here instead of a timeout + // context. + ctxt, cancel := context.WithCancel(h.runCtx) + resp, err := h.Invoice.InvoiceAcceptor(ctxt) + h.NoError(err, "InvoiceAcceptor") + + return resp, cancel +} + type TrackPaymentsClient routerrpc.Router_TrackPaymentsClient // TrackPayments makes a RPC call to the node's RouterClient and asserts. From 989cc26068ba88c77cd8eacd6bfd0385c9ca2655 Mon Sep 17 00:00:00 2001 From: ffranr Date: Tue, 30 Apr 2024 17:51:50 +0100 Subject: [PATCH 34/35] itest: add basic invoice acceptor integration test This commit introduces a basic integration test for the invoice acceptor. The test covers scenarios where an invoice is settled with a payment that is less than the invoice amount, facilitated by the invoice settlement acceptor. --- itest/list_on_test.go | 4 + itest/lnd_invoice_acceptor_test.go | 230 +++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 itest/lnd_invoice_acceptor_test.go diff --git a/itest/list_on_test.go b/itest/list_on_test.go index b4f586fa213..4d3d58d3ec9 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -422,6 +422,10 @@ var allTestCases = []*lntest.TestCase{ Name: "forward interceptor", TestFunc: testForwardInterceptorBasic, }, + { + Name: "invoice acceptor basic", + TestFunc: testInvoiceAcceptorBasic, + }, { Name: "zero conf channel open", TestFunc: testZeroConfChannelOpen, diff --git a/itest/lnd_invoice_acceptor_test.go b/itest/lnd_invoice_acceptor_test.go new file mode 100644 index 00000000000..7a08caba25d --- /dev/null +++ b/itest/lnd_invoice_acceptor_test.go @@ -0,0 +1,230 @@ +package itest + +import ( + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +// testInvoiceAcceptorBasic tests the basic functionality of the invoice +// acceptor RPC server. +func testInvoiceAcceptorBasic(ht *lntest.HarnessTest) { + ts := newAcceptorTestScenario(ht) + + alice, bob, carol := ts.alice, ts.bob, ts.carol + + // Open and wait for channels. + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} + reqs := []*lntest.OpenChannelRequest{ + {Local: alice, Remote: bob, Param: p}, + {Local: bob, Remote: carol, Param: p}, + } + resp := ht.OpenMultiChannelsAsync(reqs) + cpAB, cpBC := resp[0], resp[1] + + // Make sure Alice is aware of channel Bob=>Carol. + ht.AssertTopologyChannelOpen(alice, cpBC) + + // Initiate Carol's invoice acceptor. + invoiceAcceptor, cancelInvoiceAcceptor := carol.RPC.InvoiceAcceptor() + + // Prepare the test cases. + testCases := ts.prepareTestCases() + + for tcIdx, tc := range testCases { + ht.Logf("Running test case: %d", tcIdx) + + // Initiate a payment from Alice to Carol in a separate + // goroutine. We use a separate goroutine to avoid blocking the + // main goroutine where we will make use of the invoice + // acceptor. + sendPaymentDone := make(chan struct{}) + go func() { + // Signal that all the payments have been sent. + defer close(sendPaymentDone) + + _ = ts.sendPayment(tc) + }() + + acceptorRequest := ht.ReceiveInvoiceAcceptor(invoiceAcceptor) + + // Sanity check the acceptor request. + require.EqualValues(ht, tc.invoiceAmountMsat, + acceptorRequest.Invoice.ValueMsat) + require.EqualValues(ht, tc.sendAmountMsat, + acceptorRequest.ExitHtlcAmt) + + preimage, err := lntypes.MakePreimage(tc.invoice.RPreimage) + require.NoError(ht, err, "failed to parse invoice preimage") + + // For all other packets we resolve according to the test case. + err = invoiceAcceptor.Send( + &invoicesrpc.InvoiceAcceptorResponse{ + Preimage: preimage[:], + SkipAmountCheck: tc.skipAmtCheck, + }, + ) + require.NoError(ht, err, "failed to send request") + + ht.Log("Waiting for payment send to complete") + select { + case <-sendPaymentDone: + ht.Log("Payment send attempt complete") + case <-time.After(defaultTimeout): + require.Fail(ht, "timeout waiting for payment send") + } + + ht.Log("Ensure invoice status is settled") + require.Eventually(ht, func() bool { + updatedInvoice := carol.RPC.LookupInvoice( + tc.invoice.RHash, + ) + + return updatedInvoice.State == tc.finalInvoiceState + }, defaultTimeout, 1*time.Second) + } + + cancelInvoiceAcceptor() + + // Finally, close channels. + ht.CloseChannel(alice, cpAB) + ht.CloseChannel(bob, cpBC) +} + +// acceptorTestCase is a helper struct to hold test case data. +type acceptorTestCase struct { + // invoiceAmountMsat is the amount of the invoice. + invoiceAmountMsat int64 + + // sendAmountMsat is the amount that will be sent in the payment. + sendAmountMsat int64 + + // skipAmtCheck is a flag that indicates whether the amount checks + // should be skipped during the invoice settlement process. + skipAmtCheck bool + + // finalInvoiceState is the expected eventual final state of the + // invoice. + finalInvoiceState lnrpc.Invoice_InvoiceState + + // payAddr is the payment address of the invoice. + payAddr []byte + + // invoice is the invoice that will be paid. + invoice *lnrpc.Invoice +} + +// acceptorTestScenario is a helper struct to hold the test context and provides +// helpful functionality. +type acceptorTestScenario struct { + ht *lntest.HarnessTest + alice, bob, carol *node.HarnessNode +} + +// newAcceptorTestScenario initializes a new test scenario with three nodes and +// connects them to have the following topology, +// +// Alice --> Bob --> Carol +// +// Among them, Alice and Bob are standby nodes and Carol is a new node. +func newAcceptorTestScenario( + ht *lntest.HarnessTest) *acceptorTestScenario { + + alice, bob := ht.Alice, ht.Bob + carol := ht.NewNode("carol", nil) + + ht.EnsureConnected(alice, bob) + ht.EnsureConnected(bob, carol) + + return &acceptorTestScenario{ + ht: ht, + alice: alice, + bob: bob, + carol: carol, + } +} + +// prepareTestCases prepares test cases. +func (c *acceptorTestScenario) prepareTestCases() []*acceptorTestCase { + cases := []*acceptorTestCase{ + // Send a payment with amount less than the invoice amount. + // Amount checking is skipped during the invoice settlement + // process. The sent payment should eventually result in the + // invoice being settled. + { + invoiceAmountMsat: 9000, + sendAmountMsat: 1000, + skipAmtCheck: true, + finalInvoiceState: lnrpc.Invoice_SETTLED, + }, + } + + for _, t := range cases { + inv := &lnrpc.Invoice{ValueMsat: t.invoiceAmountMsat} + addResponse := c.carol.RPC.AddInvoice(inv) + invoice := c.carol.RPC.LookupInvoice(addResponse.RHash) + + // We'll need to also decode the returned invoice so we can + // grab the payment address which is now required for ALL + // payments. + payReq := c.carol.RPC.DecodePayReq(invoice.PaymentRequest) + + t.invoice = invoice + t.payAddr = payReq.PaymentAddr + } + + return cases +} + +// buildRoute is a helper function to build a route with given hops. +func (c *acceptorTestScenario) buildRoute(amtMsat int64, + hops []*node.HarnessNode, payAddr []byte) *lnrpc.Route { + + rpcHops := make([][]byte, 0, len(hops)) + for _, hop := range hops { + k := hop.PubKeyStr + pubkey, err := route.NewVertexFromStr(k) + require.NoErrorf(c.ht, err, "error parsing %v: %v", k, err) + rpcHops = append(rpcHops, pubkey[:]) + } + + req := &routerrpc.BuildRouteRequest{ + AmtMsat: amtMsat, + FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta, + HopPubkeys: rpcHops, + PaymentAddr: payAddr, + } + + routeResp := c.alice.RPC.BuildRoute(req) + + return routeResp.Route +} + +// sendPaymentAndAssertAction sends a payment from alice to carol. +func (c *acceptorTestScenario) sendPayment( + tc *acceptorTestCase) *lnrpc.HTLCAttempt { + + // Build a route from alice to carol. + aliceBobCarolRoute := c.buildRoute( + tc.sendAmountMsat, []*node.HarnessNode{c.bob, c.carol}, + tc.payAddr, + ) + + // Send the payment. + sendReq := &routerrpc.SendToRouteRequest{ + PaymentHash: tc.invoice.RHash, + Route: aliceBobCarolRoute, + } + + return c.alice.RPC.SendToRouteV2(sendReq) +} From 8ba92d7616d4e3befee5fca10fd2ea2ebb8d19d6 Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 8 May 2024 18:17:05 +0100 Subject: [PATCH 35/35] multi: pass `UpdateAddHtlc` message custom records to invoice acceptor --- .../htlc_incoming_contest_resolver.go | 4 +++- contractcourt/interfaces.go | 6 +++++- contractcourt/mock_registry_test.go | 3 ++- htlcswitch/interfaces.go | 6 +++++- htlcswitch/link.go | 2 +- htlcswitch/mock.go | 9 ++++++--- invoices/interface.go | 6 ++++++ invoices/invoiceregistry.go | 10 ++++++++-- invoices/update.go | 18 ++++++++++++++---- lnrpc/invoicesrpc/invoice_acceptor.go | 18 +++++++++++++----- lnrpc/invoicesrpc/invoices.pb.go | 18 ++++++++++++++++-- lnrpc/invoicesrpc/invoices.proto | 4 ++++ lnrpc/invoicesrpc/invoices.swagger.json | 5 +++++ 13 files changed, 88 insertions(+), 21 deletions(-) diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index e73e3e45b2f..39dd1b38e11 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -18,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/queue" + "github.com/lightningnetwork/lnd/tlv" ) // htlcIncomingContestResolver is a ContractResolver that's able to resolve an @@ -308,7 +309,8 @@ func (h *htlcIncomingContestResolver) Resolve( resolution, err := h.Registry.NotifyExitHopHtlc( h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight, - circuitKey, hodlQueue.ChanIn(), payload, + circuitKey, fn.None[tlv.Blob](), hodlQueue.ChanIn(), + payload, ) if err != nil { return nil, err diff --git a/contractcourt/interfaces.go b/contractcourt/interfaces.go index 0d53b07b665..57196df3aba 100644 --- a/contractcourt/interfaces.go +++ b/contractcourt/interfaces.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/invoices" @@ -14,6 +15,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/sweep" + "github.com/lightningnetwork/lnd/tlv" ) // Registry is an interface which represents the invoice registry. @@ -29,7 +31,9 @@ type Registry interface { // the resolution is sent on the passed in hodlChan later. NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey models.CircuitKey, hodlChan chan<- interface{}, + circuitKey models.CircuitKey, + msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload invoices.Payload) (invoices.HtlcResolution, error) // HodlUnsubscribeAll unsubscribes from all htlc resolutions. diff --git a/contractcourt/mock_registry_test.go b/contractcourt/mock_registry_test.go index 7acac67ec5e..22329ee9c71 100644 --- a/contractcourt/mock_registry_test.go +++ b/contractcourt/mock_registry_test.go @@ -25,7 +25,8 @@ type mockRegistry struct { func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey models.CircuitKey, hodlChan chan<- interface{}, + circuitKey models.CircuitKey, msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload invoices.Payload) (invoices.HtlcResolution, error) { r.notifyChan <- notifyExitHopData{ diff --git a/htlcswitch/interfaces.go b/htlcswitch/interfaces.go index 2c27d8ab037..b73d5c1f8d9 100644 --- a/htlcswitch/interfaces.go +++ b/htlcswitch/interfaces.go @@ -6,11 +6,13 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" ) // InvoiceDatabase is an interface which represents the persistent subsystem @@ -29,7 +31,9 @@ type InvoiceDatabase interface { // for decoding purposes. NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey models.CircuitKey, hodlChan chan<- interface{}, + circuitKey models.CircuitKey, + msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload invoices.Payload) (invoices.HtlcResolution, error) // CancelInvoice attempts to cancel the invoice corresponding to the diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 010ad03bf08..0d8f0ed073a 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -3564,7 +3564,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor, event, err := l.cfg.Registry.NotifyExitHopHtlc( invoiceHash, pd.Amount, pd.Timeout, int32(heightNow), - circuitKey, l.hodlQueue.ChanIn(), payload, + circuitKey, pd.CustomRecords, l.hodlQueue.ChanIn(), payload, ) if err != nil { return err diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index a0f38c74fef..8b1b5765100 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -27,6 +27,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/contractcourt" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lnpeer" @@ -35,6 +36,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/ticker" + "github.com/lightningnetwork/lnd/tlv" ) func isAlias(scid lnwire.ShortChannelID) bool { @@ -1029,12 +1031,13 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice( func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash, amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey models.CircuitKey, hodlChan chan<- interface{}, + circuitKey models.CircuitKey, msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload invoices.Payload) (invoices.HtlcResolution, error) { event, err := i.registry.NotifyExitHopHtlc( - rhash, amt, expiry, currentHeight, circuitKey, hodlChan, - payload, + rhash, amt, expiry, currentHeight, circuitKey, msgCustomRecords, + hodlChan, payload, ) if err != nil { return nil, err diff --git a/invoices/interface.go b/invoices/interface.go index 93741794633..e9070034a8c 100644 --- a/invoices/interface.go +++ b/invoices/interface.go @@ -5,9 +5,11 @@ import ( "time" "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" ) // InvoiceDB is the database that stores the information about invoices. @@ -203,6 +205,10 @@ type InvoiceUpdater interface { // callback during an interceptor session. The request contains the invoice that // is being intercepted and supporting information. type InterceptClientRequest struct { + // MsgCustomRecords is the custom records that were parsed from the + // HTLC p2p message. + MsgCustomRecords fn.Option[tlv.Blob] + // ExitHtlcCircuitKey is the circuit key that identifies the HTLC which // is involved in the invoice settlement. ExitHtlcCircuitKey CircuitKey diff --git a/invoices/invoiceregistry.go b/invoices/invoiceregistry.go index 3481ae801d0..d546d9d21ae 100644 --- a/invoices/invoiceregistry.go +++ b/invoices/invoiceregistry.go @@ -14,6 +14,7 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/queue" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" ) var ( @@ -892,8 +893,8 @@ func (i *InvoiceRegistry) processAMP(ctx invoiceUpdateCtx) error { // held htlc. func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32, - circuitKey CircuitKey, hodlChan chan<- interface{}, - payload Payload) (HtlcResolution, error) { + circuitKey CircuitKey, msgCustomRecords fn.Option[tlv.Blob], + hodlChan chan<- interface{}, payload Payload) (HtlcResolution, error) { // Create the update context containing the relevant details of the // incoming htlc. @@ -904,6 +905,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, expiry: expiry, currentHeight: currentHeight, finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta, + msgCustomRecords: msgCustomRecords, customRecords: payload.CustomRecords(), mpp: payload.MultiPath(), amp: payload.AMPRecord(), @@ -1004,12 +1006,16 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked( ) callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) { + //setID := ctx.setID() + //htlcSet := inv.HTLCSet(setID, HtlcStateAccepted) + // Provide the invoice to the settlement interceptor to allow // the interceptor's client an opportunity to manipulate the // settlement process. var interceptSession fn.Option[InterceptSession] if i.cfg.SettlementInterceptor != nil { clientReq := InterceptClientRequest{ + MsgCustomRecords: ctx.msgCustomRecords, ExitHtlcCircuitKey: ctx.circuitKey, ExitHtlcAmt: ctx.amtPaid, ExitHtlcExpiry: ctx.expiry, diff --git a/invoices/update.go b/invoices/update.go index 855d774bdaa..438463087dc 100644 --- a/invoices/update.go +++ b/invoices/update.go @@ -5,9 +5,11 @@ import ( "errors" "github.com/lightningnetwork/lnd/amp" + "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" + "github.com/lightningnetwork/lnd/tlv" ) // invoiceUpdateCtx is an object that describes the context for the invoice @@ -19,10 +21,18 @@ type invoiceUpdateCtx struct { expiry uint32 currentHeight int32 finalCltvRejectDelta int32 - customRecords record.CustomSet - mpp *record.MPP - amp *record.AMP - metadata []byte + + // msgCustomRecords is the custom records that were included with the + // HTLC p2p message. + msgCustomRecords fn.Option[tlv.Blob] + + // customRecords is a map of custom records that were included with the + // HTLC onion payload. + customRecords record.CustomSet + + mpp *record.MPP + amp *record.AMP + metadata []byte // SkipAmountCheck is a flag that indicates whether the amount check // should be skipped during the invoice settlement process. diff --git a/lnrpc/invoicesrpc/invoice_acceptor.go b/lnrpc/invoicesrpc/invoice_acceptor.go index 3048a9e52ae..c1852b01a3c 100644 --- a/lnrpc/invoicesrpc/invoice_acceptor.go +++ b/lnrpc/invoicesrpc/invoice_acceptor.go @@ -4,6 +4,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/tlv" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -77,12 +78,19 @@ func (r *invoiceAcceptor) onIntercept( return err } + // Unpack the message custom records from the option. + var msgCustomRecords tlv.Blob + req.MsgCustomRecords.WhenSome(func(cr tlv.Blob) { + msgCustomRecords = cr + }) + return r.cfg.rpcServer.Send(&InvoiceAcceptorRequest{ - Invoice: rpcInvoice, - ExitHtlcCircuitKey: rpcCircuitKey, - ExitHtlcAmt: uint64(req.ExitHtlcAmt), - ExitHtlcExpiry: req.ExitHtlcExpiry, - CurrentHeight: req.CurrentHeight, + Invoice: rpcInvoice, + ExitHtlcCircuitKey: rpcCircuitKey, + ExitHtlcAmt: uint64(req.ExitHtlcAmt), + ExitHtlcExpiry: req.ExitHtlcExpiry, + CurrentHeight: req.CurrentHeight, + ExitHtlcMsgCustomRecords: msgCustomRecords, }) } diff --git a/lnrpc/invoicesrpc/invoices.pb.go b/lnrpc/invoicesrpc/invoices.pb.go index e33f270a82a..342a2bfcb78 100644 --- a/lnrpc/invoicesrpc/invoices.pb.go +++ b/lnrpc/invoicesrpc/invoices.pb.go @@ -694,6 +694,9 @@ type InvoiceAcceptorRequest struct { ExitHtlcExpiry uint32 `protobuf:"varint,4,opt,name=exit_htlc_expiry,json=exitHtlcExpiry,proto3" json:"exit_htlc_expiry,omitempty"` // current_height is the current block height. CurrentHeight uint32 `protobuf:"varint,5,opt,name=current_height,json=currentHeight,proto3" json:"current_height,omitempty"` + // exit_htlc_msg_custom_records is the p2p message custom records of the + // exit HTLC. + ExitHtlcMsgCustomRecords []byte `protobuf:"bytes,6,opt,name=exit_htlc_msg_custom_records,json=exitHtlcMsgCustomRecords,proto3" json:"exit_htlc_msg_custom_records,omitempty"` } func (x *InvoiceAcceptorRequest) Reset() { @@ -763,6 +766,13 @@ func (x *InvoiceAcceptorRequest) GetCurrentHeight() uint32 { return 0 } +func (x *InvoiceAcceptorRequest) GetExitHtlcMsgCustomRecords() []byte { + if x != nil { + return x.ExitHtlcMsgCustomRecords + } + return nil +} + // InvoiceAcceptorResponse is a message that the client sends to the server to // indicate whether it accepts the invoice or not. type InvoiceAcceptorResponse struct { @@ -888,7 +898,7 @@ var file_invoicesrpc_invoices_proto_rawDesc = []byte{ 0x12, 0x17, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x74, 0x6c, 0x63, - 0x49, 0x64, 0x22, 0x83, 0x02, 0x0a, 0x16, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, + 0x49, 0x64, 0x22, 0xc3, 0x02, 0x0a, 0x16, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x07, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x07, @@ -904,7 +914,11 @@ var file_invoicesrpc_invoices_proto_rawDesc = []byte{ 0x0d, 0x52, 0x0e, 0x65, 0x78, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x63, 0x75, 0x72, 0x72, 0x65, - 0x6e, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x61, 0x0a, 0x17, 0x49, 0x6e, 0x76, 0x6f, + 0x6e, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x3e, 0x0a, 0x1c, 0x65, 0x78, 0x69, 0x74, + 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x6d, 0x73, 0x67, 0x5f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x18, + 0x65, 0x78, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x4d, 0x73, 0x67, 0x43, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x61, 0x0a, 0x17, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, diff --git a/lnrpc/invoicesrpc/invoices.proto b/lnrpc/invoicesrpc/invoices.proto index 251d0d09479..5e5723b7a52 100644 --- a/lnrpc/invoicesrpc/invoices.proto +++ b/lnrpc/invoicesrpc/invoices.proto @@ -229,6 +229,10 @@ message InvoiceAcceptorRequest { // current_height is the current block height. uint32 current_height = 5; + + // exit_htlc_msg_custom_records is the p2p message custom records of the + // exit HTLC. + bytes exit_htlc_msg_custom_records = 6; } // InvoiceAcceptorResponse is a message that the client sends to the server to diff --git a/lnrpc/invoicesrpc/invoices.swagger.json b/lnrpc/invoicesrpc/invoices.swagger.json index cfefaff4300..136381d48b3 100644 --- a/lnrpc/invoicesrpc/invoices.swagger.json +++ b/lnrpc/invoicesrpc/invoices.swagger.json @@ -358,6 +358,11 @@ "type": "integer", "format": "int64", "description": "current_height is the current block height." + }, + "exit_htlc_msg_custom_records": { + "type": "string", + "format": "byte", + "description": "exit_htlc_msg_custom_records is the p2p message custom records of the\nexit HTLC." } }, "description": "InvoiceAcceptorRequest is a message that the server sends to the client to\nrequest the client to accept an invoice."