lnd/invoices/settlement_interceptor.go
ffranr 72abc94fe8
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.
2024-05-27 20:23:01 +02:00

224 lines
6.6 KiB
Go

package invoices
import (
"fmt"
"sync"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire"
)
// 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 {
HtlcModifyRequest
// ClientResponseChannel is a channel that is populated with the
// client's interceptor response during an interceptor session.
ClientResponseChannel chan HtlcModifyResponse
// ClientErrChannel is a channel that is populated with any errors that
// occur during the client's interceptor session.
ClientErrChannel chan error
// Quit is a channel that is closed when the session is no longer
// needed.
Quit chan struct{}
}
// HtlcModificationInterceptor is a service that intercepts HTLCs that aim to
// settle an invoice, enabling a subscribed client to modify certain aspects of
// those HTLCs.
type HtlcModificationInterceptor struct {
wg sync.WaitGroup
// mu is a mutex that protects the callback and activeSessions fields.
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 HtlcModifyCallback
// activeSessions is a map of active intercept sessions that are used to
// manage the client query/response for a given invoice payment hash.
activeSessions map[CircuitKey]InterceptSession
}
// NewHtlcModificationInterceptor creates a new HtlcModificationInterceptor.
func NewHtlcModificationInterceptor() *HtlcModificationInterceptor {
return &HtlcModificationInterceptor{
activeSessions: make(map[CircuitKey]InterceptSession),
}
}
// Intercept generates a new intercept session for the given invoice HTLC. The
// session is returned to the caller so that they can block until the client
// resolution is received.
func (s *HtlcModificationInterceptor) Intercept(
clientRequest HtlcModifyRequest) fn.Option[InterceptSession] {
s.mu.Lock()
defer s.mu.Unlock()
// If there is no client callback set we will not handle the invoice
// further.
if s.callback == nil {
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.
sessionKey := clientRequest.ExitHtlcCircuitKey
session := InterceptSession{
HtlcModifyRequest: clientRequest,
ClientResponseChannel: make(chan HtlcModifyResponse, 1),
ClientErrChannel: make(chan error, 1),
Quit: make(chan struct{}, 1),
}
s.activeSessions[sessionKey] = 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(cb HtlcModifyCallback) {
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 := cb(clientRequest)
if err != nil {
err = fmt.Errorf("client callback failed: %w", err)
log.Error(err)
session.ClientErrChannel <- err
}
}(s.callback)
// Return the session to the caller so that they can block until the
// resolution is received.
return fn.Some(session)
}
// Modify changes parts of the HTLC based on the client's response.
func (s *HtlcModificationInterceptor) Modify(htlc CircuitKey,
amountPaid lnwire.MilliSatoshi) error {
s.mu.Lock()
defer s.mu.Unlock()
// Retrieve the intercept session for the invoice payment hash.
session, ok := s.activeSessions[htlc]
if !ok {
return fmt.Errorf("invoice intercept session not found "+
"(circuit_key=%s)", htlc)
}
// Send the resolution to the session resolution channel.
resolution := HtlcModifyResponse{
AmountPaid: amountPaid,
}
sendSuccessful := fn.SendOrQuit(
session.ClientResponseChannel, resolution, session.Quit,
)
if !sendSuccessful {
return fmt.Errorf("failed to send modification to client")
}
return nil
}
// SetClientCallback sets the client callback function that will be called when
// an invoice is intercepted.
func (s *HtlcModificationInterceptor) SetClientCallback(
callback HtlcModifyCallback) {
s.mu.Lock()
defer s.mu.Unlock()
s.callback = 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 *HtlcModificationInterceptor) QuitSession(
session InterceptSession) error {
s.mu.Lock()
defer s.mu.Unlock()
// Retrieve the intercept session and delete it from the local cache.
sessionKey := session.ExitHtlcCircuitKey
activeSession, ok := s.activeSessions[sessionKey]
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.
close(activeSession.Quit)
delete(s.activeSessions, sessionKey)
return nil
}
// QuitActiveSessions quits all active sessions by sending on each session quit
// channel.
func (s *HtlcModificationInterceptor) QuitActiveSessions() error {
s.mu.Lock()
defer s.mu.Unlock()
for key := range s.activeSessions {
session := s.activeSessions[key]
close(session.Quit)
delete(s.activeSessions, key)
}
return nil
}
// Start starts the service.
func (s *HtlcModificationInterceptor) Start() error {
return nil
}
// Stop stops the service.
func (s *HtlcModificationInterceptor) Stop() error {
// If the service is stopping, we will quit all active sessions.
return s.QuitActiveSessions()
}
// Ensure that HtlcModificationInterceptor implements the HtlcAcceptor and
// HtlcModifier interfaces.
var _ HtlcModifier = (*HtlcModificationInterceptor)(nil)
// MockHtlcModifier is a mock implementation of the HtlcModifier interface.
type MockHtlcModifier struct {
}
// 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 (m *MockHtlcModifier) Intercept(
_ HtlcModifyRequest) fn.Option[InterceptSession] {
return fn.None[InterceptSession]()
}
// SetClientCallback sets the client callback function that will be called when
// an invoice is intercepted.
func (m *MockHtlcModifier) SetClientCallback(HtlcModifyCallback) {
}
// Modify changes parts of the HTLC based on the client's response.
func (m *MockHtlcModifier) Modify(CircuitKey,
lnwire.MilliSatoshi) error {
return nil
}
// Ensure that MockHtlcModifier implements the HtlcAcceptor
// interface.
var _ HtlcModifier = (*MockHtlcModifier)(nil)