Skip to content

Commit 9ee12ee

Browse files
committed
invoices: treat replayed HTLCs beforehand.
We make sure that HTLCs which have already been decided upon are resolved before before allowing the external interceptor to potentially cancel them back. This makes the implementation for the external HTLC interceptor more streamlined.
1 parent f25e447 commit 9ee12ee

File tree

4 files changed

+124
-37
lines changed

4 files changed

+124
-37
lines changed

invoices/invoiceregistry.go

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/lightningnetwork/lnd/clock"
12+
"github.com/lightningnetwork/lnd/fn/v2"
1213
"github.com/lightningnetwork/lnd/lntypes"
1314
"github.com/lightningnetwork/lnd/lnwire"
1415
"github.com/lightningnetwork/lnd/queue"
@@ -1086,17 +1087,36 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
10861087
updateSubscribers bool
10871088
)
10881089
callback := func(inv *Invoice) (*InvoiceUpdateDesc, error) {
1089-
updateDesc, res, err := updateInvoice(ctx, inv)
1090+
// First check if this is a replayed htlc and resolve it
1091+
// according to its current state. We cannot decide differently
1092+
// once the HTLC has already been processed before.
1093+
isReplayed, res, err := resolveReplayedHtlc(ctx, inv)
10901094
if err != nil {
10911095
return nil, err
10921096
}
1097+
if isReplayed {
1098+
resolution = res
1099+
return nil, nil
1100+
}
10931101

1094-
// Only send an update if the invoice state was changed.
1095-
updateSubscribers = updateDesc != nil &&
1096-
updateDesc.State != nil
1097-
1098-
// Assign resolution to outer scope variable.
1102+
// In case the HTLC interceptor cancels the HTLC set, we do NOT
1103+
// cancel the invoice however we cancel the complete HTLC set.
10991104
if cancelSet {
1105+
// If the invoice is not open, something is wrong, we
1106+
// fail just the HTLC with the specific error.
1107+
if inv.State != ContractOpen {
1108+
log.Errorf("Invoice state (%v) is not OPEN, "+
1109+
"cancelling HTLC set not allowed by "+
1110+
"external source", inv.State)
1111+
1112+
resolution = NewFailResolution(
1113+
ctx.circuitKey, ctx.currentHeight,
1114+
ResultInvoiceNotOpen,
1115+
)
1116+
1117+
return nil, nil
1118+
}
1119+
11001120
// If a cancel signal was set for the htlc set, we set
11011121
// the resolution as a failure with an underpayment
11021122
// indication. Something was wrong with this htlc, so
@@ -1105,10 +1125,43 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
11051125
ctx.circuitKey, ctx.currentHeight,
11061126
ResultAmountTooLow,
11071127
)
1108-
} else {
1109-
resolution = res
1128+
1129+
// We cancel all HTLCs which are in the accepted state.
1130+
//
1131+
// NOTE: The current HTLC is not included because it
1132+
// was never accepted in the first place.
1133+
htlcs := inv.HTLCSet(ctx.setID(), HtlcStateAccepted)
1134+
htlcKeys := fn.KeySet[CircuitKey](htlcs)
1135+
1136+
// The external source did cancel the htlc set, so we
1137+
// cancel all HTLCs in the set. We however keep the
1138+
// invoice in the open state.
1139+
//
1140+
// NOTE: The invoice event loop will still call the
1141+
// `cancelSingleHTLC` method for MPP payments, however
1142+
// because the HTLCs are already cancled back it will be
1143+
// a NOOP.
1144+
update := &InvoiceUpdateDesc{
1145+
UpdateType: CancelHTLCsUpdate,
1146+
CancelHtlcs: htlcKeys,
1147+
SetID: setID,
1148+
}
1149+
1150+
return update, nil
1151+
}
1152+
1153+
updateDesc, res, err := updateInvoice(ctx, inv)
1154+
if err != nil {
1155+
return nil, err
11101156
}
11111157

1158+
// Set resolution in outer scope only after successful update.
1159+
resolution = res
1160+
1161+
// Only send an update if the invoice state was changed.
1162+
updateSubscribers = updateDesc != nil &&
1163+
updateDesc.State != nil
1164+
11121165
return updateDesc, nil
11131166
}
11141167

invoices/invoices.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,9 @@ type InvoiceStateUpdateDesc struct {
765765

766766
// InvoiceUpdateCallback is a callback used in the db transaction to update the
767767
// invoice.
768+
// TODO(ziggie): Add the option of additional return values to the callback
769+
// for example the resolution which is currently assigned via an outer scope
770+
// variable.
768771
type InvoiceUpdateCallback = func(invoice *Invoice) (*InvoiceUpdateDesc, error)
769772

770773
// ValidateInvoice assures the invoice passes the checks for all the relevant

invoices/update.go

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -109,40 +109,51 @@ func (i invoiceUpdateCtx) acceptRes(
109109
return newAcceptResolution(i.circuitKey, outcome)
110110
}
111111

112-
// updateInvoice is a callback for DB.UpdateInvoice that contains the invoice
113-
// settlement logic. It returns a HTLC resolution that indicates what the
114-
// outcome of the update was.
115-
func updateInvoice(ctx *invoiceUpdateCtx, inv *Invoice) (
116-
*InvoiceUpdateDesc, HtlcResolution, error) {
112+
// resolveReplayedHtlc returns the HTLC resolution for a replayed HTLC. The
113+
// returned boolean indicates whether the HTLC was replayed or not.
114+
func resolveReplayedHtlc(ctx *invoiceUpdateCtx, inv *Invoice) (bool,
115+
HtlcResolution, error) {
117116

118117
// Don't update the invoice when this is a replayed htlc.
119-
htlc, ok := inv.Htlcs[ctx.circuitKey]
120-
if ok {
121-
switch htlc.State {
122-
case HtlcStateCanceled:
123-
return nil, ctx.failRes(ResultReplayToCanceled), nil
124-
125-
case HtlcStateAccepted:
126-
return nil, ctx.acceptRes(resultReplayToAccepted), nil
127-
128-
case HtlcStateSettled:
129-
pre := inv.Terms.PaymentPreimage
130-
131-
// Terms.PaymentPreimage will be nil for AMP invoices.
132-
// Set it to the HTLCs AMP Preimage instead.
133-
if pre == nil {
134-
pre = htlc.AMP.Preimage
135-
}
118+
htlc, replayedHTLC := inv.Htlcs[ctx.circuitKey]
119+
if !replayedHTLC {
120+
return false, nil, nil
121+
}
122+
123+
switch htlc.State {
124+
case HtlcStateCanceled:
125+
return true, ctx.failRes(ResultReplayToCanceled), nil
136126

137-
return nil, ctx.settleRes(
138-
*pre,
139-
ResultReplayToSettled,
140-
), nil
127+
case HtlcStateAccepted:
128+
return true, ctx.acceptRes(resultReplayToAccepted), nil
141129

142-
default:
143-
return nil, nil, errors.New("unknown htlc state")
130+
case HtlcStateSettled:
131+
pre := inv.Terms.PaymentPreimage
132+
133+
// Terms.PaymentPreimage will be nil for AMP invoices.
134+
// Set it to the HTLCs AMP Preimage instead.
135+
if pre == nil {
136+
pre = htlc.AMP.Preimage
144137
}
138+
139+
return true, ctx.settleRes(
140+
*pre,
141+
ResultReplayToSettled,
142+
), nil
143+
144+
default:
145+
return true, nil, errors.New("unknown htlc state")
145146
}
147+
}
148+
149+
// updateInvoice is a callback for DB.UpdateInvoice that contains the invoice
150+
// settlement logic. It returns a HTLC resolution that indicates what the
151+
// outcome of the update was.
152+
//
153+
// NOTE: Make sure replayed HTLCs are always considered before calling this
154+
// function.
155+
func updateInvoice(ctx *invoiceUpdateCtx, inv *Invoice) (
156+
*InvoiceUpdateDesc, HtlcResolution, error) {
146157

147158
// If no MPP payload was provided, then we expect this to be a keysend,
148159
// or a payment to an invoice created before we started to require the

itest/lnd_invoice_acceptor_test.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) {
116116
&invoicesrpc.HtlcModifyResponse{
117117
CircuitKey: modifierRequest.ExitHtlcCircuitKey,
118118
AmtPaid: &amtPaid,
119+
CancelSet: tc.cancelSet,
119120
},
120121
)
121122
require.NoError(ht, err, "failed to send request")
@@ -128,7 +129,9 @@ func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) {
128129
require.Fail(ht, "timeout waiting for payment send")
129130
}
130131

131-
ht.Log("Ensure invoice status is settled")
132+
ht.Logf("Ensure invoice status is expected state %v",
133+
tc.finalInvoiceState)
134+
132135
require.Eventually(ht, func() bool {
133136
updatedInvoice := carol.RPC.LookupInvoice(
134137
tc.invoice.RHash,
@@ -141,6 +144,13 @@ func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) {
141144
tc.invoice.RHash,
142145
)
143146

147+
// If the HTLC modifier canceled the incoming HTLC set, we don't
148+
// expect any HTLCs in the invoice.
149+
if tc.cancelSet {
150+
require.Len(ht, updatedInvoice.Htlcs, 0)
151+
return
152+
}
153+
144154
require.Len(ht, updatedInvoice.Htlcs, 1)
145155
require.Equal(
146156
ht, lntest.CustomRecordsWithUnendorsed(
@@ -231,6 +241,10 @@ type acceptorTestCase struct {
231241

232242
// invoice is the invoice that will be paid.
233243
invoice *lnrpc.Invoice
244+
245+
// cancelSet is a boolean which indicates whether the HTLC modifier
246+
// canceled the incoming HTLC set.
247+
cancelSet bool
234248
}
235249

236250
// acceptorTestScenario is a helper struct to hold the test context and provides
@@ -282,6 +296,12 @@ func (c *acceptorTestScenario) prepareTestCases() []*acceptorTestCase {
282296
lnwire.MinCustomRecordsTlvType: {1, 2, 3},
283297
},
284298
},
299+
{
300+
invoiceAmountMsat: 9000,
301+
sendAmountMsat: 1000,
302+
finalInvoiceState: lnrpc.Invoice_OPEN,
303+
cancelSet: true,
304+
},
285305
}
286306

287307
for _, t := range cases {

0 commit comments

Comments
 (0)