Skip to content

Commit e4e04b6

Browse files
authored
Merge pull request #8771 from lightningnetwork/custom-channels-integration-invoice
[6/5]: invoice+rpc: add exit hop InvoiceAcceptor sub-systems and RPC calls
2 parents be752ac + 197b291 commit e4e04b6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3371
-1752
lines changed

contractcourt/htlc_incoming_contest_resolver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ func (h *htlcIncomingContestResolver) Resolve(
308308

309309
resolution, err := h.Registry.NotifyExitHopHtlc(
310310
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
311-
circuitKey, hodlQueue.ChanIn(), payload,
311+
circuitKey, hodlQueue.ChanIn(), nil, payload,
312312
)
313313
if err != nil {
314314
return nil, err

contractcourt/interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Registry interface {
3030
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
3131
expiry uint32, currentHeight int32,
3232
circuitKey models.CircuitKey, hodlChan chan<- interface{},
33+
wireCustomRecords lnwire.CustomRecords,
3334
payload invoices.Payload) (invoices.HtlcResolution, error)
3435

3536
// HodlUnsubscribeAll unsubscribes from all htlc resolutions.

contractcourt/mock_registry_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type mockRegistry struct {
2626
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
2727
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
2828
circuitKey models.CircuitKey, hodlChan chan<- interface{},
29+
wireCustomRecords lnwire.CustomRecords,
2930
payload invoices.Payload) (invoices.HtlcResolution, error) {
3031

3132
r.notifyChan <- notifyExitHopData{

htlcswitch/interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type InvoiceDatabase interface {
3232
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
3333
expiry uint32, currentHeight int32,
3434
circuitKey models.CircuitKey, hodlChan chan<- interface{},
35+
wireCustomRecords lnwire.CustomRecords,
3536
payload invoices.Payload) (invoices.HtlcResolution, error)
3637

3738
// CancelInvoice attempts to cancel the invoice corresponding to the

htlcswitch/link.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3530,13 +3530,24 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
35303530
return nil
35313531
}
35323532

3533-
// As we're the exit hop, we'll double check the hop-payload included in
3534-
// the HTLC to ensure that it was crafted correctly by the sender and
3535-
// is compatible with the HTLC we were extended.
3536-
if pd.Amount < fwdInfo.AmountToForward {
3533+
// As we're the exit hop, we'll double-check the hop-payload included
3534+
// in the HTLC to ensure that it was crafted correctly by the sender
3535+
// and is compatible with the HTLC we were extended.
3536+
//
3537+
// For a special case, if the fwdInfo doesn't have any blinded path
3538+
// information, and the incoming HTLC had special extra data, then
3539+
// we'll skip this amount check. The invoice acceptor will make sure we
3540+
// reject the HTLC if it's not containing the correct amount after
3541+
// examining the custom data.
3542+
hasBlindedPath := fwdInfo.NextBlinding.IsSome()
3543+
customHTLC := len(pd.CustomRecords) > 0 && !hasBlindedPath
3544+
log.Tracef("Exit hop has_blinded_path=%v custom_htlc_bypass=%v",
3545+
hasBlindedPath, customHTLC)
3546+
3547+
if !customHTLC && pd.Amount < fwdInfo.AmountToForward {
35373548
l.log.Errorf("onion payload of incoming htlc(%x) has "+
3538-
"incompatible value: expected <=%v, got %v", pd.RHash,
3539-
pd.Amount, fwdInfo.AmountToForward)
3549+
"incompatible value: expected >=%v, got %v", pd.RHash,
3550+
fwdInfo.AmountToForward, pd.Amount)
35403551

35413552
failure := NewLinkError(
35423553
lnwire.NewFinalIncorrectHtlcAmount(pd.Amount),
@@ -3573,7 +3584,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
35733584

35743585
event, err := l.cfg.Registry.NotifyExitHopHtlc(
35753586
invoiceHash, pd.Amount, pd.Timeout, int32(heightNow),
3576-
circuitKey, l.hodlQueue.ChanIn(), payload,
3587+
circuitKey, l.hodlQueue.ChanIn(), pd.CustomRecords, payload,
35773588
)
35783589
if err != nil {
35793590
return err

htlcswitch/mock.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
10111011
panic(err)
10121012
}
10131013

1014+
modifierMock := &invoices.MockHtlcModifier{}
10141015
registry := invoices.NewRegistry(
10151016
cdb,
10161017
invoices.NewInvoiceExpiryWatcher(
@@ -1019,6 +1020,7 @@ func newMockRegistry(minDelta uint32) *mockInvoiceRegistry {
10191020
),
10201021
&invoices.RegistryConfig{
10211022
FinalCltvRejectDelta: 5,
1023+
HtlcModifier: modifierMock,
10221024
},
10231025
)
10241026
registry.Start()
@@ -1044,11 +1046,12 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice(
10441046
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
10451047
amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
10461048
circuitKey models.CircuitKey, hodlChan chan<- interface{},
1049+
wireCustomRecords lnwire.CustomRecords,
10471050
payload invoices.Payload) (invoices.HtlcResolution, error) {
10481051

10491052
event, err := i.registry.NotifyExitHopHtlc(
1050-
rhash, amt, expiry, currentHeight, circuitKey, hodlChan,
1051-
payload,
1053+
rhash, amt, expiry, currentHeight, circuitKey,
1054+
hodlChan, wireCustomRecords, payload,
10521055
)
10531056
if err != nil {
10541057
return nil, err

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/invoiceregistry.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ type RegistryConfig struct {
7474
// KeysendHoldTime indicates for how long we want to accept and hold
7575
// spontaneous keysend payments.
7676
KeysendHoldTime time.Duration
77+
78+
// HtlcModifier is a service that intercepts invoice HTLCs during the
79+
// settlement phase, enabling a subscribed client to modify certain
80+
// aspects of those HTLCs.
81+
HtlcModifier HtlcModifier
7782
}
7883

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

892898
// Create the update context containing the relevant details of the
@@ -898,6 +904,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
898904
expiry: expiry,
899905
currentHeight: currentHeight,
900906
finalCltvRejectDelta: i.cfg.FinalCltvRejectDelta,
907+
wireCustomRecords: wireCustomRecords,
901908
customRecords: payload.CustomRecords(),
902909
mpp: payload.MultiPath(),
903910
amp: payload.AMPRecord(),
@@ -998,6 +1005,54 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
9981005
)
9991006

10001007
callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) {
1008+
// Provide the invoice to the settlement interceptor to allow
1009+
// the interceptor's client an opportunity to manipulate the
1010+
// settlement process.
1011+
clientReq := HtlcModifyRequest{
1012+
WireCustomRecords: ctx.wireCustomRecords,
1013+
ExitHtlcCircuitKey: ctx.circuitKey,
1014+
ExitHtlcAmt: ctx.amtPaid,
1015+
ExitHtlcExpiry: ctx.expiry,
1016+
CurrentHeight: uint32(ctx.currentHeight),
1017+
Invoice: *inv,
1018+
}
1019+
interceptSession := i.cfg.HtlcModifier.Intercept(
1020+
clientReq,
1021+
)
1022+
1023+
// If the interceptor service has provided a response, we'll
1024+
// use the interceptor session to wait for the client to respond
1025+
// with a settlement resolution.
1026+
var interceptErr error
1027+
interceptSession.WhenSome(func(session InterceptSession) {
1028+
log.Debug("Waiting for client response from invoice " +
1029+
"HTLC interceptor session")
1030+
1031+
select {
1032+
case resp := <-session.ClientResponseChannel:
1033+
log.Debugf("Received invoice HTLC interceptor "+
1034+
"response: %v", resp)
1035+
1036+
if resp.AmountPaid != 0 {
1037+
ctx.amtPaid = resp.AmountPaid
1038+
}
1039+
1040+
case err := <-session.ClientErrChannel:
1041+
log.Errorf("Error from invoice HTLC "+
1042+
"interceptor session: %v", err)
1043+
1044+
interceptErr = err
1045+
1046+
case <-session.Quit:
1047+
// At this point, the interceptor session has
1048+
// quit.
1049+
}
1050+
})
1051+
if interceptErr != nil {
1052+
return nil, fmt.Errorf("error during invoice HTLC "+
1053+
"interception: %w", interceptErr)
1054+
}
1055+
10011056
updateDesc, res, err := updateInvoice(ctx, inv)
10021057
if err != nil {
10031058
return nil, err
@@ -1051,6 +1106,8 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
10511106

10521107
var invoiceToExpire invoiceExpiry
10531108

1109+
log.Tracef("Settlement resolution: %T %v", resolution, resolution)
1110+
10541111
switch res := resolution.(type) {
10551112
case *HtlcFailResolution:
10561113
// Inspect latest htlc state on the invoice. If it is found,
@@ -1183,7 +1240,7 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
11831240
}
11841241

11851242
// Now that the links have been notified of any state changes to their
1186-
// HTLCs, we'll go ahead and notify any clients wiaiting on the invoice
1243+
// HTLCs, we'll go ahead and notify any clients waiting on the invoice
11871244
// state changes.
11881245
if updateSubscribers {
11891246
// We'll add a setID onto the notification, but only if this is

0 commit comments

Comments
 (0)