Skip to content

Commit 011ff00

Browse files
committed
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.
1 parent 15abb14 commit 011ff00

File tree

2 files changed

+268
-0
lines changed

2 files changed

+268
-0
lines changed

invoices/interface.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,42 @@ type InvoiceUpdater interface {
198198
// Finalize finalizes the update before it is written to the database.
199199
Finalize(updateType UpdateType) error
200200
}
201+
202+
// InterceptClientRequest is the request that is passed to the client via
203+
// callback during an interceptor session. The request contains the invoice that
204+
// is being intercepted and supporting information.
205+
type InterceptClientRequest struct {
206+
// ExitHtlcCircuitKey is the circuit key that identifies the HTLC which
207+
// is involved in the invoice settlement.
208+
ExitHtlcCircuitKey CircuitKey
209+
210+
// ExitHtlcAmt is the amount of the HTLC which is involved in the
211+
// invoice settlement.
212+
ExitHtlcAmt lnwire.MilliSatoshi
213+
214+
// ExitHtlcExpiry is the expiry time of the HTLC which is involved in
215+
// the invoice settlement.
216+
ExitHtlcExpiry uint32
217+
218+
// CurrentHeight is the current block height.
219+
CurrentHeight uint32
220+
221+
// Invoice is the invoice that is being intercepted.
222+
Invoice Invoice
223+
}
224+
225+
// InterceptorClientCallback is a function that is called when an invoice is
226+
// intercepted by the invoice interceptor.
227+
type InterceptorClientCallback func(InterceptClientRequest) error
228+
229+
// SettlementInterceptorInterface is an interface that allows the caller to
230+
// intercept and specify invoice settlement outcomes.
231+
type SettlementInterceptorInterface interface {
232+
// SetClientCallback sets the client callback function that is called
233+
// when an invoice is intercepted.
234+
SetClientCallback(InterceptorClientCallback)
235+
236+
// Resolve is called by the caller to settle an invoice with the
237+
// corresponding resolution.
238+
Resolve(lntypes.Hash, bool) error
239+
}

invoices/settlement_interceptor.go

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

0 commit comments

Comments
 (0)