Skip to content

Commit 72abc94

Browse files
ffranrguggero
authored andcommitted
invoices: add invoice htlc interceptor service
This commit introduces a new invoice htlc interceptor service that intercepts invoice HTLCs during their settlement phase. It forwards HTLCs to a subscribed client to determine their settlement outcomes. This commit also introduces an interface to facilitate integrating the interceptor with other packages.
1 parent b628483 commit 72abc94

File tree

2 files changed

+286
-0
lines changed

2 files changed

+286
-0
lines changed

invoices/interface.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/lightningnetwork/lnd/channeldb/models"
8+
"github.com/lightningnetwork/lnd/fn"
89
"github.com/lightningnetwork/lnd/lntypes"
910
"github.com/lightningnetwork/lnd/lnwire"
1011
"github.com/lightningnetwork/lnd/record"
@@ -198,3 +199,64 @@ type InvoiceUpdater interface {
198199
// Finalize finalizes the update before it is written to the database.
199200
Finalize(updateType UpdateType) error
200201
}
202+
203+
// HtlcModifyRequest is the request that is passed to the client via callback
204+
// during a HTLC interceptor session. The request contains the invoice that the
205+
// given HTLC is attempting to settle.
206+
type HtlcModifyRequest struct {
207+
// WireCustomRecords are the custom records that were parsed from the
208+
// HTLC wire message. These are the records of the current HTLC to be
209+
// accepted/settled. All previously accepted/settled HTLCs for the same
210+
// invoice are present in the Invoice field below.
211+
WireCustomRecords lnwire.CustomRecords
212+
213+
// ExitHtlcCircuitKey is the circuit key that identifies the HTLC which
214+
// is involved in the invoice settlement.
215+
ExitHtlcCircuitKey CircuitKey
216+
217+
// ExitHtlcAmt is the amount of the HTLC which is involved in the
218+
// invoice settlement.
219+
ExitHtlcAmt lnwire.MilliSatoshi
220+
221+
// ExitHtlcExpiry is the absolute expiry height of the HTLC which is
222+
// involved in the invoice settlement.
223+
ExitHtlcExpiry uint32
224+
225+
// CurrentHeight is the current block height.
226+
CurrentHeight uint32
227+
228+
// Invoice is the invoice that is being intercepted. The HTLCs within
229+
// the invoice are only those previously accepted/settled for the same
230+
// invoice.
231+
Invoice Invoice
232+
}
233+
234+
// HtlcModifyResponse is the response that the client should send back to the
235+
// interceptor after processing the HTLC modify request.
236+
type HtlcModifyResponse struct {
237+
// AmountPaid is the amount that the client has decided the HTLC is
238+
// actually worth. This might be different from the amount that the
239+
// HTLC was originally sent with, in case additional value is carried
240+
// along with it (which might be the case in custom channels).
241+
AmountPaid lnwire.MilliSatoshi
242+
}
243+
244+
// HtlcModifyCallback is a function that is called when an invoice is
245+
// intercepted by the invoice interceptor.
246+
type HtlcModifyCallback func(HtlcModifyRequest) error
247+
248+
// HtlcModifier is an interface that allows the caller to intercept and modify
249+
// aspects of HTLCs that are settling an invoice.
250+
type HtlcModifier interface {
251+
// Intercept generates a new intercept session for the given invoice.
252+
// The session is returned to the caller so that they can block until
253+
// the client resolution is received.
254+
Intercept(HtlcModifyRequest) fn.Option[InterceptSession]
255+
256+
// SetClientCallback sets the client callback function that is called
257+
// when an invoice is intercepted.
258+
SetClientCallback(HtlcModifyCallback)
259+
260+
// Modify changes parts of the HTLC based on the client's response.
261+
Modify(htlc CircuitKey, amountPaid lnwire.MilliSatoshi) error
262+
}

invoices/settlement_interceptor.go

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package invoices
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
7+
"github.com/lightningnetwork/lnd/fn"
8+
"github.com/lightningnetwork/lnd/lnwire"
9+
)
10+
11+
// InterceptSession creates a session that is returned to the caller when an
12+
// invoice is submitted to this service. This session allows the caller to block
13+
// until the invoice is processed.
14+
type InterceptSession struct {
15+
HtlcModifyRequest
16+
17+
// ClientResponseChannel is a channel that is populated with the
18+
// client's interceptor response during an interceptor session.
19+
ClientResponseChannel chan HtlcModifyResponse
20+
21+
// ClientErrChannel is a channel that is populated with any errors that
22+
// occur during the client's interceptor session.
23+
ClientErrChannel chan error
24+
25+
// Quit is a channel that is closed when the session is no longer
26+
// needed.
27+
Quit chan struct{}
28+
}
29+
30+
// HtlcModificationInterceptor is a service that intercepts HTLCs that aim to
31+
// settle an invoice, enabling a subscribed client to modify certain aspects of
32+
// those HTLCs.
33+
type HtlcModificationInterceptor struct {
34+
wg sync.WaitGroup
35+
36+
// mu is a mutex that protects the callback and activeSessions fields.
37+
mu sync.Mutex
38+
39+
// callback is the client callback function that is called when an
40+
// invoice is intercepted. This function gives the client the ability to
41+
// determine how the invoice should be settled.
42+
callback HtlcModifyCallback
43+
44+
// activeSessions is a map of active intercept sessions that are used to
45+
// manage the client query/response for a given invoice payment hash.
46+
activeSessions map[CircuitKey]InterceptSession
47+
}
48+
49+
// NewHtlcModificationInterceptor creates a new HtlcModificationInterceptor.
50+
func NewHtlcModificationInterceptor() *HtlcModificationInterceptor {
51+
return &HtlcModificationInterceptor{
52+
activeSessions: make(map[CircuitKey]InterceptSession),
53+
}
54+
}
55+
56+
// Intercept generates a new intercept session for the given invoice HTLC. The
57+
// session is returned to the caller so that they can block until the client
58+
// resolution is received.
59+
func (s *HtlcModificationInterceptor) Intercept(
60+
clientRequest HtlcModifyRequest) fn.Option[InterceptSession] {
61+
62+
s.mu.Lock()
63+
defer s.mu.Unlock()
64+
65+
// If there is no client callback set we will not handle the invoice
66+
// further.
67+
if s.callback == nil {
68+
return fn.None[InterceptSession]()
69+
}
70+
71+
// Create and store a new intercept session for the invoice. We will use
72+
// the payment hash as the storage/retrieval key for the session.
73+
sessionKey := clientRequest.ExitHtlcCircuitKey
74+
session := InterceptSession{
75+
HtlcModifyRequest: clientRequest,
76+
ClientResponseChannel: make(chan HtlcModifyResponse, 1),
77+
ClientErrChannel: make(chan error, 1),
78+
Quit: make(chan struct{}, 1),
79+
}
80+
s.activeSessions[sessionKey] = session
81+
82+
// The callback function will block at the client's discretion. We will
83+
// therefore execute it in a separate goroutine.
84+
s.wg.Add(1)
85+
go func(cb HtlcModifyCallback) {
86+
defer s.wg.Done()
87+
88+
// By this point, we've already checked that the client callback
89+
// is set. However, if the client callback has been set to nil
90+
// since that check then Exec will return an error.
91+
err := cb(clientRequest)
92+
if err != nil {
93+
err = fmt.Errorf("client callback failed: %w", err)
94+
log.Error(err)
95+
session.ClientErrChannel <- err
96+
}
97+
}(s.callback)
98+
99+
// Return the session to the caller so that they can block until the
100+
// resolution is received.
101+
return fn.Some(session)
102+
}
103+
104+
// Modify changes parts of the HTLC based on the client's response.
105+
func (s *HtlcModificationInterceptor) Modify(htlc CircuitKey,
106+
amountPaid lnwire.MilliSatoshi) error {
107+
108+
s.mu.Lock()
109+
defer s.mu.Unlock()
110+
111+
// Retrieve the intercept session for the invoice payment hash.
112+
session, ok := s.activeSessions[htlc]
113+
if !ok {
114+
return fmt.Errorf("invoice intercept session not found "+
115+
"(circuit_key=%s)", htlc)
116+
}
117+
118+
// Send the resolution to the session resolution channel.
119+
resolution := HtlcModifyResponse{
120+
AmountPaid: amountPaid,
121+
}
122+
sendSuccessful := fn.SendOrQuit(
123+
session.ClientResponseChannel, resolution, session.Quit,
124+
)
125+
if !sendSuccessful {
126+
return fmt.Errorf("failed to send modification to client")
127+
}
128+
129+
return nil
130+
}
131+
132+
// SetClientCallback sets the client callback function that will be called when
133+
// an invoice is intercepted.
134+
func (s *HtlcModificationInterceptor) SetClientCallback(
135+
callback HtlcModifyCallback) {
136+
137+
s.mu.Lock()
138+
defer s.mu.Unlock()
139+
140+
s.callback = callback
141+
}
142+
143+
// QuitSession closes the quit channel for the session associated with the
144+
// given invoice. This signals to the client that the session has ended.
145+
func (s *HtlcModificationInterceptor) QuitSession(
146+
session InterceptSession) error {
147+
148+
s.mu.Lock()
149+
defer s.mu.Unlock()
150+
151+
// Retrieve the intercept session and delete it from the local cache.
152+
sessionKey := session.ExitHtlcCircuitKey
153+
activeSession, ok := s.activeSessions[sessionKey]
154+
if !ok {
155+
// If the session is not found, no further action is necessary.
156+
return nil
157+
}
158+
159+
// Send to the quit channel to signal the client that the session has
160+
// ended.
161+
close(activeSession.Quit)
162+
delete(s.activeSessions, sessionKey)
163+
164+
return nil
165+
}
166+
167+
// QuitActiveSessions quits all active sessions by sending on each session quit
168+
// channel.
169+
func (s *HtlcModificationInterceptor) QuitActiveSessions() error {
170+
s.mu.Lock()
171+
defer s.mu.Unlock()
172+
173+
for key := range s.activeSessions {
174+
session := s.activeSessions[key]
175+
close(session.Quit)
176+
delete(s.activeSessions, key)
177+
}
178+
179+
return nil
180+
}
181+
182+
// Start starts the service.
183+
func (s *HtlcModificationInterceptor) Start() error {
184+
return nil
185+
}
186+
187+
// Stop stops the service.
188+
func (s *HtlcModificationInterceptor) Stop() error {
189+
// If the service is stopping, we will quit all active sessions.
190+
return s.QuitActiveSessions()
191+
}
192+
193+
// Ensure that HtlcModificationInterceptor implements the HtlcAcceptor and
194+
// HtlcModifier interfaces.
195+
var _ HtlcModifier = (*HtlcModificationInterceptor)(nil)
196+
197+
// MockHtlcModifier is a mock implementation of the HtlcModifier interface.
198+
type MockHtlcModifier struct {
199+
}
200+
201+
// Intercept generates a new intercept session for the given invoice. The
202+
// session is returned to the caller so that they can block until the client
203+
// resolution is received.
204+
func (m *MockHtlcModifier) Intercept(
205+
_ HtlcModifyRequest) fn.Option[InterceptSession] {
206+
207+
return fn.None[InterceptSession]()
208+
}
209+
210+
// SetClientCallback sets the client callback function that will be called when
211+
// an invoice is intercepted.
212+
func (m *MockHtlcModifier) SetClientCallback(HtlcModifyCallback) {
213+
}
214+
215+
// Modify changes parts of the HTLC based on the client's response.
216+
func (m *MockHtlcModifier) Modify(CircuitKey,
217+
lnwire.MilliSatoshi) error {
218+
219+
return nil
220+
}
221+
222+
// Ensure that MockHtlcModifier implements the HtlcAcceptor
223+
// interface.
224+
var _ HtlcModifier = (*MockHtlcModifier)(nil)

0 commit comments

Comments
 (0)