mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-04 12:54:07 +02:00
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.
This commit is contained in:
179
invoices/modification_interceptor.go
Normal file
179
invoices/modification_interceptor.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package invoices
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInterceptorClientAlreadyConnected is an error that is returned
|
||||
// when a client tries to connect to the interceptor service while
|
||||
// another client is already connected.
|
||||
ErrInterceptorClientAlreadyConnected = errors.New(
|
||||
"interceptor client already connected",
|
||||
)
|
||||
|
||||
// ErrInterceptorClientDisconnected is an error that is returned when
|
||||
// the client disconnects during an interceptor session.
|
||||
ErrInterceptorClientDisconnected = errors.New(
|
||||
"interceptor client disconnected",
|
||||
)
|
||||
)
|
||||
|
||||
// safeCallback is a wrapper around a callback function that is safe for
|
||||
// concurrent access.
|
||||
type safeCallback struct {
|
||||
// callback is the actual callback function that is called when an
|
||||
// invoice is intercepted. This might be nil if no client is currently
|
||||
// connected.
|
||||
callback atomic.Pointer[HtlcModifyCallback]
|
||||
}
|
||||
|
||||
// Set atomically sets the callback function. If a callback is already set, an
|
||||
// error is returned. The returned function can be used to reset the callback to
|
||||
// nil once the client is done.
|
||||
func (s *safeCallback) Set(callback HtlcModifyCallback) (func(), error) {
|
||||
if !s.callback.CompareAndSwap(nil, &callback) {
|
||||
return nil, ErrInterceptorClientAlreadyConnected
|
||||
}
|
||||
|
||||
return func() {
|
||||
s.callback.Store(nil)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsConnected returns true if a client is currently connected.
|
||||
func (s *safeCallback) IsConnected() bool {
|
||||
return s.callback.Load() != nil
|
||||
}
|
||||
|
||||
// Exec executes the callback function if it is set. If the callback is not set,
|
||||
// an error is returned.
|
||||
func (s *safeCallback) Exec(req HtlcModifyRequest) (*HtlcModifyResponse,
|
||||
error) {
|
||||
|
||||
callback := s.callback.Load()
|
||||
if callback == nil {
|
||||
return nil, ErrInterceptorClientDisconnected
|
||||
}
|
||||
|
||||
return (*callback)(req)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// callback is the wrapped 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 *safeCallback
|
||||
|
||||
// quit is a channel that is closed when the interceptor is stopped.
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
// NewHtlcModificationInterceptor creates a new HtlcModificationInterceptor.
|
||||
func NewHtlcModificationInterceptor() *HtlcModificationInterceptor {
|
||||
return &HtlcModificationInterceptor{
|
||||
callback: &safeCallback{},
|
||||
}
|
||||
}
|
||||
|
||||
// Intercept generates a new intercept session for the given invoice. The call
|
||||
// blocks until the client has responded to the request or an error occurs. The
|
||||
// response callback is only called if a session was created in the first place,
|
||||
// which is only the case if a client is registered.
|
||||
func (s *HtlcModificationInterceptor) Intercept(clientRequest HtlcModifyRequest,
|
||||
responseCallback func(HtlcModifyResponse)) error {
|
||||
|
||||
// If there is no client callback set we will not handle the invoice
|
||||
// further.
|
||||
if !s.callback.IsConnected() {
|
||||
log.Debugf("Not intercepting invoice with circuit key %v, no "+
|
||||
"intercept client connected",
|
||||
clientRequest.ExitHtlcCircuitKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We'll block until the client has responded to the request or an error
|
||||
// occurs.
|
||||
var (
|
||||
responseChan = make(chan *HtlcModifyResponse, 1)
|
||||
errChan = make(chan error, 1)
|
||||
)
|
||||
|
||||
// The callback function will block at the client's discretion. We will
|
||||
// therefore execute it in a separate goroutine. We don't need a wait
|
||||
// group because we wait for the response directly below. The caller
|
||||
// needs to make sure they don't block indefinitely, by selecting on the
|
||||
// quit channel they receive when registering the callback.
|
||||
go func() {
|
||||
log.Debugf("Waiting for client response from invoice HTLC "+
|
||||
"interceptor session with circuit key %v",
|
||||
clientRequest.ExitHtlcCircuitKey)
|
||||
|
||||
// By this point, we've already checked that the client callback
|
||||
// is set. However, if the client disconnected since that check
|
||||
// then Exec will return an error.
|
||||
result, err := s.callback.Exec(clientRequest)
|
||||
if err != nil {
|
||||
_ = fn.SendOrQuit(errChan, err, s.quit)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = fn.SendOrQuit(responseChan, result, s.quit)
|
||||
}()
|
||||
|
||||
// Wait for the client to respond or an error to occur.
|
||||
select {
|
||||
case response := <-responseChan:
|
||||
log.Debugf("Received invoice HTLC interceptor response: %v",
|
||||
response)
|
||||
|
||||
responseCallback(*response)
|
||||
|
||||
return nil
|
||||
|
||||
case err := <-errChan:
|
||||
log.Errorf("Error from invoice HTLC interceptor session: %v",
|
||||
err)
|
||||
|
||||
return err
|
||||
|
||||
case <-s.quit:
|
||||
return ErrInterceptorClientDisconnected
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterInterceptor sets the client callback function that will be called
|
||||
// when an invoice is intercepted. If a callback is already set, an error is
|
||||
// returned. The returned function must be used to reset the callback to nil
|
||||
// once the client is done or disconnects.
|
||||
func (s *HtlcModificationInterceptor) RegisterInterceptor(
|
||||
callback HtlcModifyCallback) (func(), <-chan struct{}, error) {
|
||||
|
||||
done, err := s.callback.Set(callback)
|
||||
return done, s.quit, err
|
||||
}
|
||||
|
||||
// Start starts the service.
|
||||
func (s *HtlcModificationInterceptor) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the service.
|
||||
func (s *HtlcModificationInterceptor) Stop() error {
|
||||
close(s.quit)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that HtlcModificationInterceptor implements the HtlcInterceptor and
|
||||
// HtlcModifier interfaces.
|
||||
var _ HtlcInterceptor = (*HtlcModificationInterceptor)(nil)
|
||||
var _ HtlcModifier = (*HtlcModificationInterceptor)(nil)
|
Reference in New Issue
Block a user