Skip to content

Commit e403243

Browse files
authored
Merge pull request #9459 from ziggie1984/amp-htlc-invoices
invoices: amp invoices bugfix.
2 parents d2c0279 + 715cafa commit e403243

File tree

7 files changed

+257
-26
lines changed

7 files changed

+257
-26
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)
Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,58 @@
1-
## Bug Fixes
1+
# Release Notes
2+
- [Bug Fixes](#bug-fixes)
3+
- [New Features](#new-features)
4+
- [Functional Enhancements](#functional-enhancements)
5+
- [RPC Additions](#rpc-additions)
6+
- [lncli Additions](#lncli-additions)
7+
- [Improvements](#improvements)
8+
- [Functional Updates](#functional-updates)
9+
- [RPC Updates](#rpc-updates)
10+
- [lncli Updates](#lncli-updates)
11+
- [Breaking Changes](#breaking-changes)
12+
- [Performance Improvements](#performance-improvements)
13+
- [Technical and Architectural Updates](#technical-and-architectural-updates)
14+
- [BOLT Spec Updates](#bolt-spec-updates)
15+
- [Testing](#testing)
16+
- [Database](#database)
17+
- [Code Health](#code-health)
18+
- [Tooling and Documentation](#tooling-and-documentation)
19+
20+
# Bug Fixes
21+
22+
* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9459) where we
23+
would not cancel accepted HTLCs on AMP invoices if the whole invoice was
24+
canceled.
25+
26+
# New Features
27+
28+
## Functional Enhancements
29+
30+
## RPC Additions
31+
32+
## lncli Additions
33+
34+
35+
# Improvements
36+
## Functional Updates
37+
## RPC Updates
38+
39+
## lncli Updates
40+
## Code Health
41+
## Breaking Changes
42+
## Performance Improvements
43+
44+
# Technical and Architectural Updates
45+
## BOLT Spec Updates
46+
47+
## Testing
48+
## Database
49+
## Code Health
250

351
* [Improved user experience](https://github.com/lightningnetwork/lnd/pull/9454)
452
by returning a custom error code when HTLC carries incorrect custom records.
53+
54+
## Tooling and Documentation
555

656
# Contributors (Alphabetical Order)
757

8-
* Ziggie
58+
* Ziggie

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

0 commit comments

Comments
 (0)