Merge branch '0-18-5-branch-rc2-9459' into 0-18-5-branch-rc2

This commit is contained in:
Oliver Gugger 2025-01-31 21:22:39 +01:00
commit 184b63f755
No known key found for this signature in database
GPG key ID: 8E4256593F177720
6 changed files with 205 additions and 24 deletions

View file

@ -650,18 +650,13 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef,
return err return err
} }
// If the set ID hint is non-nil, then we'll use that to filter // setIDHint can also be nil here, which means all the HTLCs
// out the HTLCs for AMP invoice so we don't need to read them // for AMP invoices are fetched. If the blank setID is passed
// all out to satisfy the invoice callback below. If it's nil, // in, then no HTLCs are fetched for the AMP invoice. If a
// then we pass in the zero set ID which means no HTLCs will be // specific setID is passed in, then only the HTLCs for that
// read out. // setID are fetched for a particular sub-AMP invoice.
var invSetID invpkg.SetID
if setIDHint != nil {
invSetID = *setIDHint
}
invoice, err := fetchInvoice( invoice, err := fetchInvoice(
invoiceNum, invoices, []*invpkg.SetID{&invSetID}, false, invoiceNum, invoices, []*invpkg.SetID{setIDHint}, false,
) )
if err != nil { if err != nil {
return err return err
@ -691,7 +686,7 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef,
// If this is an AMP update, then limit the returned AMP state // If this is an AMP update, then limit the returned AMP state
// to only the requested set ID. // to only the requested set ID.
if setIDHint != nil { if setIDHint != nil {
filterInvoiceAMPState(updatedInvoice, &invSetID) filterInvoiceAMPState(updatedInvoice, setIDHint)
} }
return nil return nil
@ -848,7 +843,10 @@ func (k *kvInvoiceUpdater) Finalize(updateType invpkg.UpdateType) error {
return k.storeSettleHodlInvoiceUpdate() return k.storeSettleHodlInvoiceUpdate()
case invpkg.CancelInvoiceUpdate: case invpkg.CancelInvoiceUpdate:
return k.serializeAndStoreInvoice() // Persist all changes which where made when cancelling the
// invoice. All HTLCs which were accepted are now canceled, so
// we persist this state.
return k.storeCancelHtlcsUpdate()
} }
return fmt.Errorf("unknown update type: %v", updateType) return fmt.Errorf("unknown update type: %v", updateType)

View file

@ -56,6 +56,11 @@ type InvoiceDB interface {
// passed payment hash. If an invoice matching the passed payment hash // passed payment hash. If an invoice matching the passed payment hash
// doesn't exist within the database, then the action will fail with a // doesn't exist within the database, then the action will fail with a
// "not found" error. // "not found" error.
// The setIDHint is used to signal whether AMP HTLCs should be fetched
// for the invoice. If a blank setID is passed no HTLCs will be fetched
// in case of an AMP invoice. Nil means all HTLCs for all sub AMP
// invoices will be fetched and if a specific setID is supplied only
// HTLCs for that setID will be fetched.
// //
// The update is performed inside the same database transaction that // The update is performed inside the same database transaction that
// fetches the invoice and is therefore atomic. The fields to update // fetches the invoice and is therefore atomic. The fields to update

View file

@ -704,7 +704,10 @@ func (i *InvoiceRegistry) cancelSingleHtlc(invoiceRef InvoiceRef,
// Try to mark the specified htlc as canceled in the invoice database. // Try to mark the specified htlc as canceled in the invoice database.
// Intercept the update descriptor to set the local updated variable. If // Intercept the update descriptor to set the local updated variable. If
// no invoice update is performed, we can return early. // no invoice update is performed, we can return early.
// setID is only set for AMP HTLCs, so it can be nil and it is expected
// to be nil for non-AMP HTLCs.
setID := (*SetID)(invoiceRef.SetID()) setID := (*SetID)(invoiceRef.SetID())
var updated bool var updated bool
invoice, err := i.idb.UpdateInvoice( invoice, err := i.idb.UpdateInvoice(
context.Background(), invoiceRef, setID, context.Background(), invoiceRef, setID,
@ -1014,6 +1017,9 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
HtlcResolution, invoiceExpiry, error) { HtlcResolution, invoiceExpiry, error) {
invoiceRef := ctx.invoiceRef() invoiceRef := ctx.invoiceRef()
// This setID is only set for AMP HTLCs, so it can be nil and it is
// also expected to be nil for non-AMP HTLCs.
setID := (*SetID)(ctx.setID()) setID := (*SetID)(ctx.setID())
// We need to look up the current state of the invoice in order to send // We need to look up the current state of the invoice in order to send
@ -1370,7 +1376,15 @@ func (i *InvoiceRegistry) SettleHodlInvoice(ctx context.Context,
hash := preimage.Hash() hash := preimage.Hash()
invoiceRef := InvoiceRefByHash(hash) invoiceRef := InvoiceRefByHash(hash)
invoice, err := i.idb.UpdateInvoice(ctx, invoiceRef, nil, updateInvoice)
// AMP hold invoices are not supported so we set the setID to nil.
// For non-AMP invoices this parameter is ignored during the fetching
// of the database state.
setID := (*SetID)(nil)
invoice, err := i.idb.UpdateInvoice(
ctx, invoiceRef, setID, updateInvoice,
)
if err != nil { if err != nil {
log.Errorf("SettleHodlInvoice with preimage %v: %v", log.Errorf("SettleHodlInvoice with preimage %v: %v",
preimage, err) preimage, err)
@ -1454,10 +1468,14 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(ctx context.Context,
}, nil }, nil
} }
// If it's an AMP invoice we need to fetch all AMP HTLCs here so that
// we can cancel all of HTLCs which are in the accepted state across
// different setIDs.
setID := (*SetID)(nil)
invoiceRef := InvoiceRefByHash(payHash) invoiceRef := InvoiceRefByHash(payHash)
invoice, err := i.idb.UpdateInvoice(
// We pass a nil setID which means no HTLCs will be read out. ctx, invoiceRef, setID, updateInvoice,
invoice, err := i.idb.UpdateInvoice(ctx, invoiceRef, nil, updateInvoice) )
// Implement idempotency by returning success if the invoice was already // Implement idempotency by returning success if the invoice was already
// canceled. // canceled.
@ -1483,8 +1501,8 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(ctx context.Context,
// that are waiting for resolution. Any htlcs that were already canceled // that are waiting for resolution. Any htlcs that were already canceled
// before, will be notified again. This isn't necessary but doesn't hurt // before, will be notified again. This isn't necessary but doesn't hurt
// either. // either.
// // For AMP invoices we fetched all AMP HTLCs for all sub AMP invoices
// TODO(ziggie): Also consider AMP HTLCs here. // here so we can clean up all of them.
for key, htlc := range invoice.Htlcs { for key, htlc := range invoice.Htlcs {
if htlc.State != HtlcStateCanceled { if htlc.State != HtlcStateCanceled {
continue continue
@ -1496,6 +1514,7 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(ctx context.Context,
), ),
) )
} }
i.notifyClients(payHash, invoice, nil) i.notifyClients(payHash, invoice, nil)
// Attempt to also delete the invoice if requested through the registry // Attempt to also delete the invoice if requested through the registry

View file

@ -117,6 +117,10 @@ func TestInvoiceRegistry(t *testing.T) {
name: "FailPartialAMPPayment", name: "FailPartialAMPPayment",
test: testFailPartialAMPPayment, test: testFailPartialAMPPayment,
}, },
{
name: "CancelAMPInvoicePendingHTLCs",
test: testCancelAMPInvoicePendingHTLCs,
},
} }
makeKeyValueDB := func(t *testing.T) (invpkg.InvoiceDB, makeKeyValueDB := func(t *testing.T) (invpkg.InvoiceDB,
@ -2441,3 +2445,130 @@ func testFailPartialAMPPayment(t *testing.T,
"expected HTLC to be canceled") "expected HTLC to be canceled")
} }
} }
// testCancelAMPInvoicePendingHTLCs tests the case where an AMP invoice is
// canceled and the remaining HTLCs are also canceled so that no HTLCs are left
// in the accepted state.
func testCancelAMPInvoicePendingHTLCs(t *testing.T,
makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) {
t.Parallel()
ctx := newTestContext(t, nil, makeDB)
ctxb := context.Background()
const (
expiry = uint32(testCurrentHeight + 20)
numShards = 4
)
var (
shardAmt = testInvoiceAmount / lnwire.MilliSatoshi(numShards)
payAddr [32]byte
)
_, err := rand.Read(payAddr[:])
require.NoError(t, err)
// Create an AMP invoice we are going to pay via a multi-part payment.
ampInvoice := newInvoice(t, false, true)
// An AMP invoice is referenced by the payment address.
ampInvoice.Terms.PaymentAddr = payAddr
_, err = ctx.registry.AddInvoice(
ctxb, ampInvoice, testInvoicePaymentHash,
)
require.NoError(t, err)
htlcPayloadSet1 := &mockPayload{
mpp: record.NewMPP(testInvoiceAmount, payAddr),
// We are not interested in settling the AMP HTLC so we don't
// use valid shares.
amp: record.NewAMP([32]byte{1}, [32]byte{1}, 1),
}
// Send first HTLC which pays part of the invoice.
hodlChan1 := make(chan interface{}, 1)
resolution, err := ctx.registry.NotifyExitHopHtlc(
lntypes.Hash{1}, shardAmt, expiry, testCurrentHeight,
getCircuitKey(1), hodlChan1, nil, htlcPayloadSet1,
)
require.NoError(t, err)
require.Nil(t, resolution, "did not expect direct resolution")
htlcPayloadSet2 := &mockPayload{
mpp: record.NewMPP(testInvoiceAmount, payAddr),
// We are not interested in settling the AMP HTLC so we don't
// use valid shares.
amp: record.NewAMP([32]byte{2}, [32]byte{2}, 1),
}
// Send htlc 2 which should be added to the invoice as expected.
hodlChan2 := make(chan interface{}, 1)
resolution, err = ctx.registry.NotifyExitHopHtlc(
lntypes.Hash{2}, shardAmt, expiry, testCurrentHeight,
getCircuitKey(2), hodlChan2, nil, htlcPayloadSet2,
)
require.NoError(t, err)
require.Nil(t, resolution, "did not expect direct resolution")
require.Eventuallyf(t, func() bool {
inv, err := ctx.registry.LookupInvoice(
ctxb, testInvoicePaymentHash,
)
require.NoError(t, err)
return len(inv.Htlcs) == 2
}, testTimeout, time.Millisecond*100, "HTLCs not added to invoice")
// expire the invoice here.
ctx.clock.SetTime(testTime.Add(65 * time.Minute))
// Expect HLTC 1 to be canceled via the MPPTimeout fail resolution.
select {
case resolution := <-hodlChan1:
htlcResolution, _ := resolution.(invpkg.HtlcResolution)
_, ok := htlcResolution.(*invpkg.HtlcFailResolution)
require.True(
t, ok, "expected fail resolution, got: %T", resolution,
)
case <-time.After(testTimeout):
t.Fatal("timeout waiting for HTLC resolution")
}
// Expect HLTC 2 to be canceled via the MPPTimeout fail resolution.
select {
case resolution := <-hodlChan2:
htlcResolution, _ := resolution.(invpkg.HtlcResolution)
_, ok := htlcResolution.(*invpkg.HtlcFailResolution)
require.True(
t, ok, "expected fail resolution, got: %T", resolution,
)
case <-time.After(testTimeout):
t.Fatal("timeout waiting for HTLC resolution")
}
require.Eventuallyf(t, func() bool {
inv, err := ctx.registry.LookupInvoice(
ctxb, testInvoicePaymentHash,
)
require.NoError(t, err)
return inv.State == invpkg.ContractCanceled
}, testTimeout, time.Millisecond*100, "invoice not canceled")
// Fetch the invoice again and compare the number of cancelled HTLCs.
inv, err := ctx.registry.LookupInvoice(
ctxb, testInvoicePaymentHash,
)
require.NoError(t, err)
// Make sure all HTLCs are in the cancelled state.
require.Len(t, inv.Htlcs, 2)
for _, htlc := range inv.Htlcs {
require.Equal(t, invpkg.HtlcStateCanceled, htlc.State,
"expected HTLC to be canceled")
}
}

View file

@ -1336,14 +1336,26 @@ func (i *SQLStore) UpdateInvoice(ctx context.Context, ref InvoiceRef,
txOpt := SQLInvoiceQueriesTxOptions{readOnly: false} txOpt := SQLInvoiceQueriesTxOptions{readOnly: false}
txErr := i.db.ExecTx(ctx, &txOpt, func(db SQLInvoiceQueries) error { txErr := i.db.ExecTx(ctx, &txOpt, func(db SQLInvoiceQueries) error {
if setID != nil { switch {
// Make sure to use the set ID if this is an AMP update. // For the default case we fetch all HTLCs.
case setID == nil:
ref.refModifier = DefaultModifier
// If the setID is the blank but NOT nil, we set the
// refModifier to HtlcSetBlankModifier to fetch no HTLC for the
// AMP invoice.
case *setID == BlankPayAddr:
ref.refModifier = HtlcSetBlankModifier
// A setID is provided, we use the refModifier to fetch only
// the HTLCs for the given setID and also make sure we add the
// setID to the ref.
default:
var setIDBytes [32]byte var setIDBytes [32]byte
copy(setIDBytes[:], setID[:]) copy(setIDBytes[:], setID[:])
ref.setID = &setIDBytes ref.setID = &setIDBytes
// If we're updating an AMP invoice, we'll also only // We only fetch the HTLCs for the given setID.
// need to fetch the HTLCs for the given set ID.
ref.refModifier = HtlcSetOnlyModifier ref.refModifier = HtlcSetOnlyModifier
} }

View file

@ -31,7 +31,7 @@ func acceptHtlcsAmp(invoice *Invoice, setID SetID,
} }
// cancelHtlcsAmp processes a cancellation of an HTLC that belongs to an AMP // cancelHtlcsAmp processes a cancellation of an HTLC that belongs to an AMP
// HTLC set. We'll need to update the meta data in the main invoice, and also // HTLC set. We'll need to update the meta data in the main invoice, and also
// apply the new update to the update MAP, since all the HTLCs for a given HTLC // apply the new update to the update MAP, since all the HTLCs for a given HTLC
// set need to be written in-line with each other. // set need to be written in-line with each other.
func cancelHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey, func cancelHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey,
@ -552,6 +552,9 @@ func cancelInvoice(invoice *Invoice, hash *lntypes.Hash,
invoice.State = ContractCanceled invoice.State = ContractCanceled
for key, htlc := range invoice.Htlcs { for key, htlc := range invoice.Htlcs {
// We might not have a setID here in case we are cancelling
// an AMP invoice however the setID is only important when
// settling an AMP HTLC.
canceled, _, err := getUpdatedHtlcState( canceled, _, err := getUpdatedHtlcState(
htlc, ContractCanceled, setID, htlc, ContractCanceled, setID,
) )
@ -567,6 +570,19 @@ func cancelInvoice(invoice *Invoice, hash *lntypes.Hash,
if err != nil { if err != nil {
return err return err
} }
// If its an AMP HTLC we need to make sure we persist
// this new state otherwise AMP HTLCs are not updated
// on disk because HTLCs for AMP invoices are stored
// separately.
if htlc.AMP != nil {
err := cancelHtlcsAmp(
invoice, key, htlc, updater,
)
if err != nil {
return err
}
}
} }
} }