Skip to content

Commit

Permalink
Merge pull request #8771 from lightningnetwork/custom-channels-integr…
Browse files Browse the repository at this point in the history
…ation-invoice

[6/5]: invoice+rpc: add exit hop InvoiceAcceptor sub-systems and RPC calls
  • Loading branch information
guggero committed May 28, 2024
2 parents be752ac + 197b291 commit e4e04b6
Show file tree
Hide file tree
Showing 41 changed files with 3,371 additions and 1,752 deletions.
2 changes: 1 addition & 1 deletion contractcourt/htlc_incoming_contest_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ func (h *htlcIncomingContestResolver) Resolve(

resolution, err := h.Registry.NotifyExitHopHtlc(
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
circuitKey, hodlQueue.ChanIn(), payload,
circuitKey, hodlQueue.ChanIn(), nil, payload,
)
if err != nil {
return nil, err
Expand Down
1 change: 1 addition & 0 deletions contractcourt/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Registry interface {
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32,
circuitKey models.CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload invoices.Payload) (invoices.HtlcResolution, error)

// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
Expand Down
1 change: 1 addition & 0 deletions contractcourt/mock_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type mockRegistry struct {
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
circuitKey models.CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload invoices.Payload) (invoices.HtlcResolution, error) {

r.notifyChan <- notifyExitHopData{
Expand Down
1 change: 1 addition & 0 deletions htlcswitch/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type InvoiceDatabase interface {
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32,
circuitKey models.CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload invoices.Payload) (invoices.HtlcResolution, error)

// CancelInvoice attempts to cancel the invoice corresponding to the
Expand Down
25 changes: 18 additions & 7 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -3530,13 +3530,24 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
return nil
}

// As we're the exit hop, we'll double check the hop-payload included in
// the HTLC to ensure that it was crafted correctly by the sender and
// is compatible with the HTLC we were extended.
if pd.Amount < fwdInfo.AmountToForward {
// As we're the exit hop, we'll double-check the hop-payload included
// in the HTLC to ensure that it was crafted correctly by the sender
// and is compatible with the HTLC we were extended.
//
// For a special case, if the fwdInfo doesn't have any blinded path
// information, and the incoming HTLC had special extra data, then
// we'll skip this amount check. The invoice acceptor will make sure we
// reject the HTLC if it's not containing the correct amount after
// examining the custom data.
hasBlindedPath := fwdInfo.NextBlinding.IsSome()
customHTLC := len(pd.CustomRecords) > 0 && !hasBlindedPath
log.Tracef("Exit hop has_blinded_path=%v custom_htlc_bypass=%v",
hasBlindedPath, customHTLC)

if !customHTLC && pd.Amount < fwdInfo.AmountToForward {
l.log.Errorf("onion payload of incoming htlc(%x) has "+
"incompatible value: expected <=%v, got %v", pd.RHash,
pd.Amount, fwdInfo.AmountToForward)
"incompatible value: expected >=%v, got %v", pd.RHash,
fwdInfo.AmountToForward, pd.Amount)

failure := NewLinkError(
lnwire.NewFinalIncorrectHtlcAmount(pd.Amount),
Expand Down Expand Up @@ -3573,7 +3584,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, l.hodlQueue.ChanIn(), pd.CustomRecords, payload,
)
if err != nil {
return err
Expand Down
7 changes: 5 additions & 2 deletions htlcswitch/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
panic(err)
}

modifierMock := &invoices.MockHtlcModifier{}
registry := invoices.NewRegistry(
cdb,
invoices.NewInvoiceExpiryWatcher(
Expand All @@ -1019,6 +1020,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
),
&invoices.RegistryConfig{
FinalCltvRejectDelta: 5,
HtlcModifier: modifierMock,
},
)
registry.Start()
Expand All @@ -1044,11 +1046,12 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice(
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
circuitKey models.CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload invoices.Payload) (invoices.HtlcResolution, error) {

event, err := i.registry.NotifyExitHopHtlc(
rhash, amt, expiry, currentHeight, circuitKey, hodlChan,
payload,
rhash, amt, expiry, currentHeight, circuitKey,
hodlChan, wireCustomRecords, payload,
)
if err != nil {
return nil, err
Expand Down
62 changes: 62 additions & 0 deletions invoices/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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"
Expand Down Expand Up @@ -198,3 +199,64 @@ type InvoiceUpdater interface {
// Finalize finalizes the update before it is written to the database.
Finalize(updateType UpdateType) error
}

// HtlcModifyRequest is the request that is passed to the client via callback
// during a HTLC interceptor session. The request contains the invoice that the
// given HTLC is attempting to settle.
type HtlcModifyRequest struct {
// WireCustomRecords are the custom records that were parsed from the
// HTLC wire message. These are the records of the current HTLC to be
// accepted/settled. All previously accepted/settled HTLCs for the same
// invoice are present in the Invoice field below.
WireCustomRecords lnwire.CustomRecords

// 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 absolute expiry height 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. The HTLCs within
// the invoice are only those previously accepted/settled for the same
// invoice.
Invoice Invoice
}

// HtlcModifyResponse is the response that the client should send back to the
// interceptor after processing the HTLC modify request.
type HtlcModifyResponse struct {
// AmountPaid is the amount that the client has decided the HTLC is
// actually worth. This might be different from the amount that the
// HTLC was originally sent with, in case additional value is carried
// along with it (which might be the case in custom channels).
AmountPaid lnwire.MilliSatoshi
}

// HtlcModifyCallback is a function that is called when an invoice is
// intercepted by the invoice interceptor.
type HtlcModifyCallback func(HtlcModifyRequest) error

// HtlcModifier is an interface that allows the caller to intercept and modify
// aspects of HTLCs that are settling an invoice.
type HtlcModifier interface {
// 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.
Intercept(HtlcModifyRequest) fn.Option[InterceptSession]

// SetClientCallback sets the client callback function that is called
// when an invoice is intercepted.
SetClientCallback(HtlcModifyCallback)

// Modify changes parts of the HTLC based on the client's response.
Modify(htlc CircuitKey, amountPaid lnwire.MilliSatoshi) error
}
59 changes: 58 additions & 1 deletion invoices/invoiceregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ type RegistryConfig struct {
// KeysendHoldTime indicates for how long we want to accept and hold
// spontaneous keysend payments.
KeysendHoldTime time.Duration

// HtlcModifier is a service that intercepts invoice HTLCs during the
// settlement phase, enabling a subscribed client to modify certain
// aspects of those HTLCs.
HtlcModifier HtlcModifier
}

// htlcReleaseEvent describes an htlc auto-release event. It is used to release
Expand Down Expand Up @@ -887,6 +892,7 @@ func (i *InvoiceRegistry) processAMP(ctx invoiceUpdateCtx) error {
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
circuitKey CircuitKey, hodlChan chan<- interface{},
wireCustomRecords lnwire.CustomRecords,
payload Payload) (HtlcResolution, error) {

// Create the update context containing the relevant details of the
Expand All @@ -898,6 +904,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
expiry: expiry,
currentHeight: currentHeight,
finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta,
wireCustomRecords: wireCustomRecords,
customRecords: payload.CustomRecords(),
mpp: payload.MultiPath(),
amp: payload.AMPRecord(),
Expand Down Expand Up @@ -998,6 +1005,54 @@ 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.
clientReq := HtlcModifyRequest{
WireCustomRecords: ctx.wireCustomRecords,
ExitHtlcCircuitKey: ctx.circuitKey,
ExitHtlcAmt: ctx.amtPaid,
ExitHtlcExpiry: ctx.expiry,
CurrentHeight: uint32(ctx.currentHeight),
Invoice: *inv,
}
interceptSession := i.cfg.HtlcModifier.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.
var interceptErr error
interceptSession.WhenSome(func(session InterceptSession) {
log.Debug("Waiting for client response from invoice " +
"HTLC interceptor session")

select {
case resp := <-session.ClientResponseChannel:
log.Debugf("Received invoice HTLC interceptor "+
"response: %v", resp)

if resp.AmountPaid != 0 {
ctx.amtPaid = resp.AmountPaid
}

case err := <-session.ClientErrChannel:
log.Errorf("Error from invoice HTLC "+
"interceptor session: %v", err)

interceptErr = err

case <-session.Quit:
// At this point, the interceptor session has
// quit.
}
})
if interceptErr != nil {
return nil, fmt.Errorf("error during invoice HTLC "+
"interception: %w", interceptErr)
}

updateDesc, res, err := updateInvoice(ctx, inv)
if err != nil {
return nil, err
Expand Down Expand Up @@ -1051,6 +1106,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(

var invoiceToExpire invoiceExpiry

log.Tracef("Settlement resolution: %T %v", resolution, resolution)

switch res := resolution.(type) {
case *HtlcFailResolution:
// Inspect latest htlc state on the invoice. If it is found,
Expand Down Expand Up @@ -1183,7 +1240,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
Expand Down
Loading

0 comments on commit e4e04b6

Please sign in to comment.