Skip to content

Commit 118261a

Browse files
committed
invoices+channeldb: Fix AMP invoices behaviour.
We now cancel all HTLCs of an AMP invoice as soon as it expires. Otherwise because we mark the invoice as cancelled we would not allow accepted HTLCs to be resolved via the invoiceEventLoop.
1 parent 32cdbb4 commit 118261a

File tree

6 files changed

+205
-24
lines changed

6 files changed

+205
-24
lines changed

channeldb/invoices.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -650,18 +650,13 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef,
650650
return err
651651
}
652652

653-
// If the set ID hint is non-nil, then we'll use that to filter
654-
// out the HTLCs for AMP invoice so we don't need to read them
655-
// all out to satisfy the invoice callback below. If it's nil,
656-
// then we pass in the zero set ID which means no HTLCs will be
657-
// read out.
658-
var invSetID invpkg.SetID
659-
660-
if setIDHint != nil {
661-
invSetID = *setIDHint
662-
}
653+
// setIDHint can also be nil here, which means all the HTLCs
654+
// for AMP invoices are fetched. If the blank setID is passed
655+
// in, then no HTLCs are fetched for the AMP invoice. If a
656+
// specific setID is passed in, then only the HTLCs for that
657+
// setID are fetched for a particular sub-AMP invoice.
663658
invoice, err := fetchInvoice(
664-
invoiceNum, invoices, []*invpkg.SetID{&invSetID}, false,
659+
invoiceNum, invoices, []*invpkg.SetID{setIDHint}, false,
665660
)
666661
if err != nil {
667662
return err
@@ -691,7 +686,7 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef,
691686
// If this is an AMP update, then limit the returned AMP state
692687
// to only the requested set ID.
693688
if setIDHint != nil {
694-
filterInvoiceAMPState(updatedInvoice, &invSetID)
689+
filterInvoiceAMPState(updatedInvoice, setIDHint)
695690
}
696691

697692
return nil
@@ -848,7 +843,10 @@ func (k *kvInvoiceUpdater) Finalize(updateType invpkg.UpdateType) error {
848843
return k.storeSettleHodlInvoiceUpdate()
849844

850845
case invpkg.CancelInvoiceUpdate:
851-
return k.serializeAndStoreInvoice()
846+
// Persist all changes which where made when cancelling the
847+
// invoice. All HTLCs which were accepted are now canceled, so
848+
// we persist this state.
849+
return k.storeCancelHtlcsUpdate()
852850
}
853851

854852
return fmt.Errorf("unknown update type: %v", updateType)

invoices/interface.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ type InvoiceDB interface {
5656
// passed payment hash. If an invoice matching the passed payment hash
5757
// doesn't exist within the database, then the action will fail with a
5858
// "not found" error.
59+
// The setIDHint is used to signal whether AMP HTLCs should be fetched
60+
// for the invoice. If a blank setID is passed no HTLCs will be fetched
61+
// in case of an AMP invoice. Nil means all HTLCs for all sub AMP
62+
// invoices will be fetched and if a specific setID is supplied only
63+
// HTLCs for that setID will be fetched.
5964
//
6065
// The update is performed inside the same database transaction that
6166
// fetches the invoice and is therefore atomic. The fields to update

invoices/invoiceregistry.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,10 @@ func (i *InvoiceRegistry) cancelSingleHtlc(invoiceRef InvoiceRef,
704704
// Try to mark the specified htlc as canceled in the invoice database.
705705
// Intercept the update descriptor to set the local updated variable. If
706706
// no invoice update is performed, we can return early.
707+
// setID is only set for AMP HTLCs, so it can be nil and it is expected
708+
// to be nil for non-AMP HTLCs.
707709
setID := (*SetID)(invoiceRef.SetID())
710+
708711
var updated bool
709712
invoice, err := i.idb.UpdateInvoice(
710713
context.Background(), invoiceRef, setID,
@@ -1014,6 +1017,9 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
10141017
HtlcResolution, invoiceExpiry, error) {
10151018

10161019
invoiceRef := ctx.invoiceRef()
1020+
1021+
// This setID is only set for AMP HTLCs, so it can be nil and it is
1022+
// also expected to be nil for non-AMP HTLCs.
10171023
setID := (*SetID)(ctx.setID())
10181024

10191025
// We need to look up the current state of the invoice in order to send
@@ -1374,7 +1380,15 @@ func (i *InvoiceRegistry) SettleHodlInvoice(ctx context.Context,
13741380

13751381
hash := preimage.Hash()
13761382
invoiceRef := InvoiceRefByHash(hash)
1377-
invoice, err := i.idb.UpdateInvoice(ctx, invoiceRef, nil, updateInvoice)
1383+
1384+
// AMP hold invoices are not supported so we set the setID to nil.
1385+
// For non-AMP invoices this parameter is ignored during the fetching
1386+
// of the database state.
1387+
setID := (*SetID)(nil)
1388+
1389+
invoice, err := i.idb.UpdateInvoice(
1390+
ctx, invoiceRef, setID, updateInvoice,
1391+
)
13781392
if err != nil {
13791393
log.Errorf("SettleHodlInvoice with preimage %v: %v",
13801394
preimage, err)
@@ -1458,10 +1472,14 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(ctx context.Context,
14581472
}, nil
14591473
}
14601474

1475+
// If it's an AMP invoice we need to fetch all AMP HTLCs here so that
1476+
// we can cancel all of HTLCs which are in the accepted state across
1477+
// different setIDs.
1478+
setID := (*SetID)(nil)
14611479
invoiceRef := InvoiceRefByHash(payHash)
1462-
1463-
// We pass a nil setID which means no HTLCs will be read out.
1464-
invoice, err := i.idb.UpdateInvoice(ctx, invoiceRef, nil, updateInvoice)
1480+
invoice, err := i.idb.UpdateInvoice(
1481+
ctx, invoiceRef, setID, updateInvoice,
1482+
)
14651483

14661484
// Implement idempotency by returning success if the invoice was already
14671485
// canceled.
@@ -1487,8 +1505,8 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(ctx context.Context,
14871505
// that are waiting for resolution. Any htlcs that were already canceled
14881506
// before, will be notified again. This isn't necessary but doesn't hurt
14891507
// either.
1490-
//
1491-
// TODO(ziggie): Also consider AMP HTLCs here.
1508+
// For AMP invoices we fetched all AMP HTLCs for all sub AMP invoices
1509+
// here so we can clean up all of them.
14921510
for key, htlc := range invoice.Htlcs {
14931511
if htlc.State != HtlcStateCanceled {
14941512
continue
@@ -1500,6 +1518,7 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(ctx context.Context,
15001518
),
15011519
)
15021520
}
1521+
15031522
i.notifyClients(payHash, invoice, nil)
15041523

15051524
// Attempt to also delete the invoice if requested through the registry

invoices/invoiceregistry_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ func TestInvoiceRegistry(t *testing.T) {
117117
name: "FailPartialAMPPayment",
118118
test: testFailPartialAMPPayment,
119119
},
120+
{
121+
name: "CancelAMPInvoicePendingHTLCs",
122+
test: testCancelAMPInvoicePendingHTLCs,
123+
},
120124
}
121125

122126
makeKeyValueDB := func(t *testing.T) (invpkg.InvoiceDB,
@@ -2441,3 +2445,130 @@ func testFailPartialAMPPayment(t *testing.T,
24412445
"expected HTLC to be canceled")
24422446
}
24432447
}
2448+
2449+
// testCancelAMPInvoicePendingHTLCs tests the case where an AMP invoice is
2450+
// canceled and the remaining HTLCs are also canceled so that no HTLCs are left
2451+
// in the accepted state.
2452+
func testCancelAMPInvoicePendingHTLCs(t *testing.T,
2453+
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
2454+
2455+
t.Parallel()
2456+
2457+
ctx := newTestContext(t, nil, makeDB)
2458+
ctxb := context.Background()
2459+
2460+
const (
2461+
expiry = uint32(testCurrentHeight + 20)
2462+
numShards = 4
2463+
)
2464+
2465+
var (
2466+
shardAmt = testInvoiceAmount / lnwire.MilliSatoshi(numShards)
2467+
payAddr [32]byte
2468+
)
2469+
_, err := rand.Read(payAddr[:])
2470+
require.NoError(t, err)
2471+
2472+
// Create an AMP invoice we are going to pay via a multi-part payment.
2473+
ampInvoice := newInvoice(t, false, true)
2474+
2475+
// An AMP invoice is referenced by the payment address.
2476+
ampInvoice.Terms.PaymentAddr = payAddr
2477+
2478+
_, err = ctx.registry.AddInvoice(
2479+
ctxb, ampInvoice, testInvoicePaymentHash,
2480+
)
2481+
require.NoError(t, err)
2482+
2483+
htlcPayloadSet1 := &mockPayload{
2484+
mpp: record.NewMPP(testInvoiceAmount, payAddr),
2485+
// We are not interested in settling the AMP HTLC so we don't
2486+
// use valid shares.
2487+
amp: record.NewAMP([32]byte{1}, [32]byte{1}, 1),
2488+
}
2489+
2490+
// Send first HTLC which pays part of the invoice.
2491+
hodlChan1 := make(chan interface{}, 1)
2492+
resolution, err := ctx.registry.NotifyExitHopHtlc(
2493+
lntypes.Hash{1}, shardAmt, expiry, testCurrentHeight,
2494+
getCircuitKey(1), hodlChan1, nil, htlcPayloadSet1,
2495+
)
2496+
require.NoError(t, err)
2497+
require.Nil(t, resolution, "did not expect direct resolution")
2498+
2499+
htlcPayloadSet2 := &mockPayload{
2500+
mpp: record.NewMPP(testInvoiceAmount, payAddr),
2501+
// We are not interested in settling the AMP HTLC so we don't
2502+
// use valid shares.
2503+
amp: record.NewAMP([32]byte{2}, [32]byte{2}, 1),
2504+
}
2505+
2506+
// Send htlc 2 which should be added to the invoice as expected.
2507+
hodlChan2 := make(chan interface{}, 1)
2508+
resolution, err = ctx.registry.NotifyExitHopHtlc(
2509+
lntypes.Hash{2}, shardAmt, expiry, testCurrentHeight,
2510+
getCircuitKey(2), hodlChan2, nil, htlcPayloadSet2,
2511+
)
2512+
require.NoError(t, err)
2513+
require.Nil(t, resolution, "did not expect direct resolution")
2514+
2515+
require.Eventuallyf(t, func() bool {
2516+
inv, err := ctx.registry.LookupInvoice(
2517+
ctxb, testInvoicePaymentHash,
2518+
)
2519+
require.NoError(t, err)
2520+
2521+
return len(inv.Htlcs) == 2
2522+
}, testTimeout, time.Millisecond*100, "HTLCs not added to invoice")
2523+
2524+
// expire the invoice here.
2525+
ctx.clock.SetTime(testTime.Add(65 * time.Minute))
2526+
2527+
// Expect HLTC 1 to be canceled via the MPPTimeout fail resolution.
2528+
select {
2529+
case resolution := <-hodlChan1:
2530+
htlcResolution, _ := resolution.(invpkg.HtlcResolution)
2531+
_, ok := htlcResolution.(*invpkg.HtlcFailResolution)
2532+
require.True(
2533+
t, ok, "expected fail resolution, got: %T", resolution,
2534+
)
2535+
2536+
case <-time.After(testTimeout):
2537+
t.Fatal("timeout waiting for HTLC resolution")
2538+
}
2539+
2540+
// Expect HLTC 2 to be canceled via the MPPTimeout fail resolution.
2541+
select {
2542+
case resolution := <-hodlChan2:
2543+
htlcResolution, _ := resolution.(invpkg.HtlcResolution)
2544+
_, ok := htlcResolution.(*invpkg.HtlcFailResolution)
2545+
require.True(
2546+
t, ok, "expected fail resolution, got: %T", resolution,
2547+
)
2548+
2549+
case <-time.After(testTimeout):
2550+
t.Fatal("timeout waiting for HTLC resolution")
2551+
}
2552+
2553+
require.Eventuallyf(t, func() bool {
2554+
inv, err := ctx.registry.LookupInvoice(
2555+
ctxb, testInvoicePaymentHash,
2556+
)
2557+
require.NoError(t, err)
2558+
2559+
return inv.State == invpkg.ContractCanceled
2560+
}, testTimeout, time.Millisecond*100, "invoice not canceled")
2561+
2562+
// Fetch the invoice again and compare the number of cancelled HTLCs.
2563+
inv, err := ctx.registry.LookupInvoice(
2564+
ctxb, testInvoicePaymentHash,
2565+
)
2566+
require.NoError(t, err)
2567+
2568+
// Make sure all HTLCs are in the cancelled state.
2569+
require.Len(t, inv.Htlcs, 2)
2570+
for _, htlc := range inv.Htlcs {
2571+
require.Equal(t, invpkg.HtlcStateCanceled, htlc.State,
2572+
"expected HTLC to be canceled")
2573+
}
2574+
}

invoices/sql_store.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,14 +1406,26 @@ func (i *SQLStore) UpdateInvoice(ctx context.Context, ref InvoiceRef,
14061406

14071407
txOpt := SQLInvoiceQueriesTxOptions{readOnly: false}
14081408
txErr := i.db.ExecTx(ctx, &txOpt, func(db SQLInvoiceQueries) error {
1409-
if setID != nil {
1410-
// Make sure to use the set ID if this is an AMP update.
1409+
switch {
1410+
// For the default case we fetch all HTLCs.
1411+
case setID == nil:
1412+
ref.refModifier = DefaultModifier
1413+
1414+
// If the setID is the blank but NOT nil, we set the
1415+
// refModifier to HtlcSetBlankModifier to fetch no HTLC for the
1416+
// AMP invoice.
1417+
case *setID == BlankPayAddr:
1418+
ref.refModifier = HtlcSetBlankModifier
1419+
1420+
// A setID is provided, we use the refModifier to fetch only
1421+
// the HTLCs for the given setID and also make sure we add the
1422+
// setID to the ref.
1423+
default:
14111424
var setIDBytes [32]byte
14121425
copy(setIDBytes[:], setID[:])
14131426
ref.setID = &setIDBytes
14141427

1415-
// If we're updating an AMP invoice, we'll also only
1416-
// need to fetch the HTLCs for the given set ID.
1428+
// We only fetch the HTLCs for the given setID.
14171429
ref.refModifier = HtlcSetOnlyModifier
14181430
}
14191431

invoices/update_invoice.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func acceptHtlcsAmp(invoice *Invoice, setID SetID,
3131
}
3232

3333
// cancelHtlcsAmp processes a cancellation of an HTLC that belongs to an AMP
34-
// HTLC set. We'll need to update the meta data in the main invoice, and also
34+
// HTLC set. We'll need to update the meta data in the main invoice, and also
3535
// apply the new update to the update MAP, since all the HTLCs for a given HTLC
3636
// set need to be written in-line with each other.
3737
func cancelHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey,
@@ -552,6 +552,9 @@ func cancelInvoice(invoice *Invoice, hash *lntypes.Hash,
552552
invoice.State = ContractCanceled
553553

554554
for key, htlc := range invoice.Htlcs {
555+
// We might not have a setID here in case we are cancelling
556+
// an AMP invoice however the setID is only important when
557+
// settling an AMP HTLC.
555558
canceled, _, err := getUpdatedHtlcState(
556559
htlc, ContractCanceled, setID,
557560
)
@@ -567,6 +570,19 @@ func cancelInvoice(invoice *Invoice, hash *lntypes.Hash,
567570
if err != nil {
568571
return err
569572
}
573+
574+
// If its an AMP HTLC we need to make sure we persist
575+
// this new state otherwise AMP HTLCs are not updated
576+
// on disk because HTLCs for AMP invoices are stored
577+
// separately.
578+
if htlc.AMP != nil {
579+
err := cancelHtlcsAmp(
580+
invoice, key, htlc, updater,
581+
)
582+
if err != nil {
583+
return err
584+
}
585+
}
570586
}
571587
}
572588

0 commit comments

Comments
 (0)