From eb4198b9708e2c3eb696a9dd5876e5f690e25537 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 13 Oct 2023 17:48:47 +0200 Subject: [PATCH 01/15] channeldb: extract store update methods when updaing an invoice This commit is a small refactor to move all actual DB updates after an invoice state is update to separate methods. This is a small preliminary change before we completely decouple DB updates from in-memory invocie update. --- channeldb/invoices.go | 196 +++++++++++++++++++++++++++--------------- 1 file changed, 128 insertions(+), 68 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 9ffc6aa27d..9793b2c858 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1939,8 +1939,8 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, // NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the // InvoiceUpdateDesc. func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice, - update *invpkg.InvoiceUpdateDesc) (*invpkg.Invoice, error) { + invoice *invpkg.Invoice, update *invpkg.InvoiceUpdateDesc) ( + *invpkg.Invoice, error) { timestamp := d.clock.Now() @@ -1971,9 +1971,7 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, // Tally this into the set of HTLCs that need to be updated on // disk, but once again, only if this is an AMP invoice. if invoice.IsAMP() { - cancelHtlcsAmp( - invoice, htlcsAmpUpdate, htlc, key, - ) + cancelHtlcsAmp(invoice, htlcsAmpUpdate, htlc, key) } } @@ -1983,22 +1981,41 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, return nil, errors.New("cancel action on non-existent htlc(s)") } - err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + err := d.cancelHTLCsStoreUpdate( + invoices, invoiceNum, invoice, htlcsAmpUpdate, + ) if err != nil { return nil, err } - // If this is an AMP invoice, then we'll actually store the rest of the - // HTLCs in-line with the invoice, using the invoice ID as a prefix, - // and the AMP key as a suffix: invoiceNum || setID. + return invoice, nil +} + +// cancelHTLCsStoreUpdate is a helper function used to store the invoice and +// AMP state after canceling HTLCs. +func (d *DB) cancelHTLCsStoreUpdate(invoices kvdb.RwBucket, invoiceNum []byte, + invoice *invpkg.Invoice, + htlcsAmpUpdate map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) error { + + err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + if err != nil { + return err + } + + // If this is an AMP invoice, then we'll actually store the rest + // of the HTLCs in-line with the invoice, using the invoice ID + // as a prefix, and the AMP key as a suffix: invoiceNum || + // setID. if invoice.IsAMP() { - err := updateAMPInvoices(invoices, invoiceNum, htlcsAmpUpdate) + err := updateAMPInvoices( + invoices, invoiceNum, htlcsAmpUpdate, + ) if err != nil { - return nil, err + return err } } - return invoice, nil + return nil } // serializeAndStoreInvoice is a helper function used to store invoices. @@ -2039,29 +2056,6 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen return nil, errors.New("nil custom records map") } - if invoiceIsAMP { - if htlcUpdate.AMP == nil { - return nil, fmt.Errorf("unable to add htlc "+ - "without AMP data to AMP invoice(%v)", - invoice.AddIndex) - } - - // Check if this SetID already exist. - htlcSetID := htlcUpdate.AMP.Record.SetID() - setIDInvNum := setIDIndex.Get(htlcSetID[:]) - - if setIDInvNum == nil { - err := setIDIndex.Put(htlcSetID[:], invoiceNum) - if err != nil { - return nil, err - } - } else if !bytes.Equal(setIDInvNum, invoiceNum) { - return nil, invpkg.ErrDuplicateSetID{ - SetID: htlcSetID, - } - } - } - htlc := &invpkg.InvoiceHTLC{ Amt: htlcUpdate.Amt, MppTotalAmt: htlcUpdate.MppTotalAmt, @@ -2070,7 +2064,16 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen AcceptTime: timestamp, State: invpkg.HtlcStateAccepted, CustomRecords: htlcUpdate.CustomRecords, - AMP: htlcUpdate.AMP.Copy(), + } + + if invoiceIsAMP { + if htlcUpdate.AMP == nil { + return nil, fmt.Errorf("unable to add htlc "+ + "without AMP data to AMP invoice(%v)", + invoice.AddIndex) + } + + htlc.AMP = htlcUpdate.AMP.Copy() } invoice.Htlcs[key] = htlc @@ -2099,28 +2102,13 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen } // If this isn't an AMP invoice, then we'll go ahead and update - // the invoice state directly here. For AMP invoices, we - // instead will keep the top-level invoice open, and instead - // update the state of each _htlc set_ instead. However, we'll - // allow the invoice to transition to the cancelled state - // regardless. + // the invoice state directly here. For AMP invoices, we instead + // will keep the top-level invoice open, and update the state of + // each _htlc set_ instead. However, we'll allow the invoice to + // transition to the cancelled state regardless. if !invoiceIsAMP || *newState == invpkg.ContractCanceled { invoice.State = *newState } - - // If this is a non-AMP invoice, then the state can eventually - // go to ContractSettled, so we pass in nil value as part of - // setSettleMetaFields. - isSettled := update.State.NewState == invpkg.ContractSettled - if !invoiceIsAMP && isSettled { - err := setSettleMetaFields( - settleIndex, invoiceNum, invoice, timestamp, - nil, - ) - if err != nil { - return nil, err - } - } } // The set of HTLC pre-images will only be set if we were actually able @@ -2227,6 +2215,55 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen invoice.AmtPaid += amtPaid } + err := d.addHTLCsStoreUpdate( + invoices, settleIndex, setIDIndex, invoiceNum, invoice, + settledSetIDs, htlcsAmpUpdate, timestamp, + ) + if err != nil { + return nil, err + } + + return invoice, nil +} + +// addHTLCsStoreUpdate is a helper function used to store the invoice and +// AMP state after adding HTLCs. +func (d *DB) addHTLCsStoreUpdate(invoices, settleIndex, setIDIndex kvdb.RwBucket, + invoiceNum []byte, invoice *invpkg.Invoice, + settledSetIDs map[invpkg.SetID]struct{}, + htlcsAmpUpdate map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, + timestamp time.Time) error { + + invoiceIsAMP := invoice.IsAMP() + + for htlcSetID := range htlcsAmpUpdate { + // Check if this SetID already exist. + setIDInvNum := setIDIndex.Get(htlcSetID[:]) + + if setIDInvNum == nil { + err := setIDIndex.Put(htlcSetID[:], invoiceNum) + if err != nil { + return err + } + } else if !bytes.Equal(setIDInvNum, invoiceNum) { + return invpkg.ErrDuplicateSetID{ + SetID: htlcSetID, + } + } + } + + // If this is a non-AMP invoice, then the state can eventually go to + // ContractSettled, so we pass in nil value as part of + // setSettleMetaFields. + if !invoiceIsAMP && invoice.State == invpkg.ContractSettled { + err := setSettleMetaFields( + settleIndex, invoiceNum, invoice, timestamp, nil, + ) + if err != nil { + return err + } + } + // As we don't update the settle index above for AMP invoices, we'll do // it here for each sub-AMP invoice that was settled. for settledSetID := range settledSetIDs { @@ -2236,13 +2273,13 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen &settledSetID, ) if err != nil { - return nil, err + return err } } err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) if err != nil { - return nil, err + return err } // If this is an AMP invoice, then we'll actually store the rest of the @@ -2251,11 +2288,11 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen if invoiceIsAMP { err := updateAMPInvoices(invoices, invoiceNum, htlcsAmpUpdate) if err != nil { - return nil, err + return err } } - return invoice, nil + return nil } // settleHodlInvoice marks a hodl invoice as settled. @@ -2299,13 +2336,6 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, invoice.State = invpkg.ContractSettled timestamp := d.clock.Now() - err = setSettleMetaFields( - settleIndex, invoiceNum, invoice, timestamp, nil, - ) - if err != nil { - return nil, err - } - // TODO(positiveblue): this logic can be further simplified. var amtPaid lnwire.MilliSatoshi for _, htlc := range invoice.Htlcs { @@ -2323,7 +2353,9 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, invoice.AmtPaid = amtPaid - err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + err = d.settleHodlInvoiceStoreUpdate( + invoices, settleIndex, invoiceNum, invoice, timestamp, + ) if err != nil { return nil, err } @@ -2331,6 +2363,26 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, return invoice, nil } +// settleHodlInvoiceStoreUpdate is a helper function used to store the settled +// hodl invoice update. +func (d *DB) settleHodlInvoiceStoreUpdate(invoices, settleIndex kvdb.RwBucket, + invoiceNum []byte, invoice *invpkg.Invoice, timestamp time.Time) error { + + err := setSettleMetaFields( + settleIndex, invoiceNum, invoice, timestamp, nil, + ) + if err != nil { + return err + } + + err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + if err != nil { + return err + } + + return nil +} + // cancelInvoice attempts to cancel the given invoice. That includes changing // the invoice state and the state of any relevant HTLC. func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, @@ -2380,7 +2432,7 @@ func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, } } - err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + err = d.cancelInvoiceStoreUpdate(invoices, invoiceNum, invoice) if err != nil { return nil, err } @@ -2388,6 +2440,14 @@ func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, return invoice, nil } +// cancelInvoiceStoreUpdate is a helper function used to store the canceled +// invoice update. +func (d *DB) cancelInvoiceStoreUpdate(invoices kvdb.RwBucket, invoiceNum []byte, + invoice *invpkg.Invoice) error { + + return d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) +} + // updateInvoiceState validates and processes an invoice state update. The new // state to transition to is returned, so the caller is able to select exactly // how the invoice state is updated. From 4bf6b521580e9fbe8c2e0d16b1df953a3752c3d1 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 17 Oct 2023 15:42:02 +0200 Subject: [PATCH 02/15] channeldb: fetch the invoice before calling into updateInvoice --- channeldb/invoices.go | 53 +++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 9793b2c858..5c3891cd77 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -641,10 +641,25 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef, return err } + // If the set ID hint is non-nil, then we'll use that to filter + // out the HTLCs for AMP invoice so we don't need to read them + // all out to satisfy the invoice callback below. If it's nil, + // then we pass in the zero set ID which means no HTLCs will be + // read out. + var invSetID invpkg.SetID + + if setIDHint != nil { + invSetID = *setIDHint + } + invoice, err := fetchInvoice(invoiceNum, invoices, &invSetID) + if err != nil { + return err + } + payHash := ref.PayHash() updatedInvoice, err = d.updateInvoice( - payHash, setIDHint, invoices, settleIndex, setIDIndex, - invoiceNum, callback, + payHash, invoices, settleIndex, setIDIndex, + &invoice, invoiceNum, callback, ) return err @@ -1872,26 +1887,14 @@ func settleHtlcsAmp(invoice *invpkg.Invoice, // updateInvoice fetches the invoice, obtains the update descriptor from the // callback and applies the updates in a single db transaction. -func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, - settleIndex, setIDIndex kvdb.RwBucket, invoiceNum []byte, - callback invpkg.InvoiceUpdateCallback) (*invpkg.Invoice, error) { - - // If the set ID is non-nil, then we'll use that to filter out the - // HTLCs for AMP invoice so we don't need to read them all out to - // satisfy the invoice callback below. If it's nil, then we pass in the - // zero set ID which means no HTLCs will be read out. - var invSetID invpkg.SetID - if refSetID != nil { - invSetID = *refSetID - } - invoice, err := fetchInvoice(invoiceNum, invoices, &invSetID) - if err != nil { - return nil, err - } +func (d *DB) updateInvoice(hash *lntypes.Hash, invoices, + settleIndex, setIDIndex kvdb.RwBucket, invoice *invpkg.Invoice, + invoiceNum []byte, callback invpkg.InvoiceUpdateCallback) ( + *invpkg.Invoice, error) { // Create deep copy to prevent any accidental modification in the // callback. - invoiceCopy, err := invpkg.CopyInvoice(&invoice) + invoiceCopy, err := invpkg.CopyInvoice(invoice) if err != nil { return nil, err } @@ -1899,33 +1902,33 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, refSetID *invpkg.SetID, invoices, // Call the callback and obtain the update descriptor. update, err := callback(invoiceCopy) if err != nil { - return &invoice, err + return invoice, err } // If there is nothing to update, return early. if update == nil { - return &invoice, nil + return invoice, nil } switch update.UpdateType { case invpkg.CancelHTLCsUpdate: - return d.cancelHTLCs(invoices, invoiceNum, &invoice, update) + return d.cancelHTLCs(invoices, invoiceNum, invoice, update) case invpkg.AddHTLCsUpdate: return d.addHTLCs( - invoices, settleIndex, setIDIndex, invoiceNum, &invoice, + invoices, settleIndex, setIDIndex, invoiceNum, invoice, hash, update, ) case invpkg.SettleHodlInvoiceUpdate: return d.settleHodlInvoice( - invoices, settleIndex, invoiceNum, &invoice, hash, + invoices, settleIndex, invoiceNum, invoice, hash, update.State, ) case invpkg.CancelInvoiceUpdate: return d.cancelInvoice( - invoices, invoiceNum, &invoice, hash, update.State, + invoices, invoiceNum, invoice, hash, update.State, ) default: From 7298b2d1902476ef12bec00a6f96b06e0348942b Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 24 Nov 2023 16:47:33 +0100 Subject: [PATCH 03/15] channeldb: extract AMP state updates to updateInvoiceAmpState() --- channeldb/invoices.go | 124 +++++++++++++++++++++++++++++------------- 1 file changed, 87 insertions(+), 37 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 5c3891cd77..d2c60f6147 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1770,30 +1770,20 @@ func updateAMPInvoices(invoiceBucket kvdb.RwBucket, invoiceNum []byte, } // updateHtlcsAmp takes an invoice, and a new HTLC to be added (along with its -// set ID), and update sthe internal AMP state of an invoice, and also tallies +// set ID), and updates the internal AMP state of an invoice, and also tallies // the set of HTLCs to be updated on disk. func updateHtlcsAmp(invoice *invpkg.Invoice, updateMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, htlc *invpkg.InvoiceHTLC, setID invpkg.SetID, - circuitKey models.CircuitKey) { + circuitKey models.CircuitKey) error { - ampState, ok := invoice.AMPState[setID] - if !ok { - // If an entry for this set ID doesn't already exist, then - // we'll need to create it. - ampState = invpkg.InvoiceStateAMP{ - State: invpkg.HtlcStateAccepted, - InvoiceKeys: make(map[models.CircuitKey]struct{}), - } + err := updateInvoiceAmpState( + invoice, setID, circuitKey, invpkg.HtlcStateAccepted, htlc.Amt, + ) + if err != nil { + return err } - ampState.AmtPaid += htlc.Amt - ampState.InvoiceKeys[circuitKey] = struct{}{} - - // Due to the way maps work, we need to read out the value, update it, - // then re-assign it into the map. - invoice.AMPState[setID] = ampState - // Now that we've updated the invoice state, we'll inform the caller of // the _neitre_ HTLC set they need to write for this new set ID. if _, ok := updateMap[setID]; !ok { @@ -1805,6 +1795,8 @@ func updateHtlcsAmp(invoice *invpkg.Invoice, ) } updateMap[setID][circuitKey] = htlc + + return nil } // cancelHtlcsAmp processes a cancellation of an HTLC that belongs to an AMP @@ -1813,20 +1805,17 @@ func updateHtlcsAmp(invoice *invpkg.Invoice, // set need to be written in-line with each other. func cancelHtlcsAmp(invoice *invpkg.Invoice, updateMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - htlc *invpkg.InvoiceHTLC, circuitKey models.CircuitKey) { + htlc *invpkg.InvoiceHTLC, circuitKey models.CircuitKey) error { setID := htlc.AMP.Record.SetID() // First, we'll update the state of the entire HTLC set to cancelled. - ampState := invoice.AMPState[setID] - ampState.State = invpkg.HtlcStateCanceled - - ampState.InvoiceKeys[circuitKey] = struct{}{} - ampState.AmtPaid -= htlc.Amt - - // With the state update,d we'll set the new value so the struct - // changes are propagated. - invoice.AMPState[setID] = ampState + err := updateInvoiceAmpState( + invoice, setID, circuitKey, invpkg.HtlcStateCanceled, htlc.Amt, + ) + if err != nil { + return err + } if _, ok := updateMap[setID]; !ok { // Only HTLCs in the accepted state, can be cancelled, but we @@ -1853,6 +1842,8 @@ func cancelHtlcsAmp(invoice *invpkg.Invoice, if invoice.AmtPaid != 0 { invoice.AmtPaid -= htlc.Amt } + + return nil } // settleHtlcsAmp processes a new settle operation on an HTLC set for an AMP @@ -1861,7 +1852,7 @@ func cancelHtlcsAmp(invoice *invpkg.Invoice, func settleHtlcsAmp(invoice *invpkg.Invoice, settledSetIDs map[invpkg.SetID]struct{}, updateMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - htlc *invpkg.InvoiceHTLC, circuitKey models.CircuitKey) { + htlc *invpkg.InvoiceHTLC, circuitKey models.CircuitKey) error { // First, add the set ID to the set that was settled in this invoice // update. We'll use this later to update the settle index. @@ -1870,12 +1861,12 @@ func settleHtlcsAmp(invoice *invpkg.Invoice, // Next update the main AMP meta-data to indicate that this HTLC set // has been fully settled. - ampState := invoice.AMPState[setID] - ampState.State = invpkg.HtlcStateSettled - - ampState.InvoiceKeys[circuitKey] = struct{}{} - - invoice.AMPState[setID] = ampState + err := updateInvoiceAmpState( + invoice, setID, circuitKey, invpkg.HtlcStateSettled, 0, + ) + if err != nil { + return err + } // Finally, we'll add this to the set of HTLCs that need to be updated. if _, ok := updateMap[setID]; !ok { @@ -1883,6 +1874,8 @@ func settleHtlcsAmp(invoice *invpkg.Invoice, updateMap[setID] = mapEntry } updateMap[setID][circuitKey] = htlc + + return nil } // updateInvoice fetches the invoice, obtains the update descriptor from the @@ -1974,7 +1967,12 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, // Tally this into the set of HTLCs that need to be updated on // disk, but once again, only if this is an AMP invoice. if invoice.IsAMP() { - cancelHtlcsAmp(invoice, htlcsAmpUpdate, htlc, key) + err := cancelHtlcsAmp( + invoice, htlcsAmpUpdate, htlc, key, + ) + if err != nil { + return nil, err + } } } @@ -2084,10 +2082,13 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // Collect the set of new HTLCs so we can write them properly // below, but only if this is an AMP invoice. if invoiceIsAMP { - updateHtlcsAmp( + err := updateHtlcsAmp( invoice, htlcsAmpUpdate, htlc, htlcUpdate.AMP.Record.SetID(), key, ) + if err != nil { + return nil, err + } } } @@ -2170,10 +2171,13 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // is an AMP invoice, then we'll need to update some additional // meta data state. if htlcSettled && invoiceIsAMP { - settleHtlcsAmp( + err = settleHtlcsAmp( invoice, settledSetIDs, htlcsAmpUpdate, htlc, key, ) + if err != nil { + return nil, err + } } accepted := htlc.State == invpkg.HtlcStateAccepted @@ -2541,6 +2545,52 @@ func updateInvoiceState(invoice *invpkg.Invoice, hash *lntypes.Hash, } } +// updateInvoiceAmpState updates the AMP state of an invoice, given the new +// state, and the amount of the HTLC that is being updated. +func updateInvoiceAmpState(invoice *invpkg.Invoice, setID invpkg.SetID, + circuitKey models.CircuitKey, state invpkg.HtlcState, + amt lnwire.MilliSatoshi) error { + + // Retrieve the AMP state for this set ID. + ampState, ok := invoice.AMPState[setID] + + // If the state is accepted then we may need to create a new entry for + // this set ID, otherwise we expect that the entry already exists and + // we can update it. + if !ok && state != invpkg.HtlcStateAccepted { + return fmt.Errorf("unable to update AMP state for setID=%x ", + setID) + } + + switch state { + case invpkg.HtlcStateAccepted: + if !ok { + // If an entry for this set ID doesn't already exist, + // then we'll need to create it. + ampState = invpkg.InvoiceStateAMP{ + State: invpkg.HtlcStateAccepted, + InvoiceKeys: make( + map[models.CircuitKey]struct{}, + ), + } + } + + ampState.AmtPaid += amt + + case invpkg.HtlcStateCanceled: + ampState.State = invpkg.HtlcStateCanceled + ampState.AmtPaid -= amt + + case invpkg.HtlcStateSettled: + ampState.State = invpkg.HtlcStateSettled + } + + ampState.InvoiceKeys[circuitKey] = struct{}{} + invoice.AMPState[setID] = ampState + + return nil +} + // cancelSingleHtlc validates cancellation of a single htlc and update its // state. func cancelSingleHtlc(resolveTime time.Time, htlc *invpkg.InvoiceHTLC, From ef5a31733e4a5135156849cbbbb94e2c6273a553 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 24 Nov 2023 16:50:04 +0100 Subject: [PATCH 04/15] channeldb: do not change the update descriptor when cancelling htlcs --- channeldb/invoices.go | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index d2c60f6147..24ca9c8055 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1944,15 +1944,13 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, htlcsAmpUpdate := make(map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) //nolint:lll // Process cancel actions from update descriptor. - cancelHtlcs := update.CancelHtlcs - for key, htlc := range invoice.Htlcs { - htlc := htlc + for key := range update.CancelHtlcs { + htlc, exists := invoice.Htlcs[key] - // Check whether this htlc needs to be canceled. If it does, - // update the htlc state to Canceled. - _, cancel := cancelHtlcs[key] - if !cancel { - continue + // Verify that we don't get an action for htlcs that are not + // present on the invoice. + if !exists { + return nil, fmt.Errorf("cancel of non-existent htlc") } err := cancelSingleHtlc(timestamp, htlc, invoice.State) @@ -1960,10 +1958,6 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, return nil, err } - // Delete processed cancel action, so that we can check later - // that there are no actions left. - delete(cancelHtlcs, key) - // Tally this into the set of HTLCs that need to be updated on // disk, but once again, only if this is an AMP invoice. if invoice.IsAMP() { @@ -1976,12 +1970,6 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, } } - // Verify that we didn't get an action for htlcs that are not present on - // the invoice. - if len(cancelHtlcs) > 0 { - return nil, errors.New("cancel action on non-existent htlc(s)") - } - err := d.cancelHTLCsStoreUpdate( invoices, invoiceNum, invoice, htlcsAmpUpdate, ) From 342eb4eac2aaa3556ee8f7573b125fca4db4e6dd Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 24 Nov 2023 17:13:05 +0100 Subject: [PATCH 05/15] invoices: refactor updateHtlc to return new state With this refactor updateHtlc is renamed to getUpdatedHtlcState and changed such that it won't change the HTLC's state and resolve time but instead returns whether the change is needed. This change is part of a multi-commit refactor to ensure that all changes to the invoice will be tracked individually. --- channeldb/invoice_test.go | 9 ++++- channeldb/invoices.go | 79 ++++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index d7bbc5b5c5..d6d1eaf7ae 100644 --- a/channeldb/invoice_test.go +++ b/channeldb/invoice_test.go @@ -2997,7 +2997,14 @@ func TestUpdateHTLC(t *testing.T) { func testUpdateHTLC(t *testing.T, test updateHTLCTest) { htlc := test.input.Copy() - _, err := updateHtlc(testNow, htlc, test.invState, test.setID) + stateChanged, state, err := getUpdatedHtlcState( + htlc, test.invState, test.setID, + ) + if stateChanged { + htlc.State = state + htlc.ResolveTime = testNow + } + require.Equal(t, test.expErr, err) require.Equal(t, test.output, *htlc) } diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 24ca9c8055..74b4681d5c 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -2148,13 +2148,22 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen if settleEligibleAMP { htlcContextState = invpkg.ContractSettled } - htlcSettled, err := updateHtlc( - timestamp, htlc, htlcContextState, setID, + + htlcStateChanged, htlcState, err := getUpdatedHtlcState( + htlc, htlcContextState, setID, ) if err != nil { return nil, err } + if htlcStateChanged { + htlc.State = htlcState + htlc.ResolveTime = timestamp + } + + htlcSettled := htlcStateChanged && + htlcState == invpkg.HtlcStateSettled + // If the HTLC has being settled for the first time, and this // is an AMP invoice, then we'll need to update some additional // meta data state. @@ -2334,14 +2343,16 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, // TODO(positiveblue): this logic can be further simplified. var amtPaid lnwire.MilliSatoshi for _, htlc := range invoice.Htlcs { - _, err := updateHtlc( - timestamp, htlc, invpkg.ContractSettled, nil, + settled, _, err := getUpdatedHtlcState( + htlc, invpkg.ContractSettled, nil, ) if err != nil { return nil, err } - if htlc.State == invpkg.HtlcStateSettled { + if settled { + htlc.State = invpkg.HtlcStateSettled + htlc.ResolveTime = timestamp amtPaid += htlc.Amt } } @@ -2419,12 +2430,17 @@ func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, // TODO(positiveblue): this logic can be simplified. for _, htlc := range invoice.Htlcs { - _, err := updateHtlc( - timestamp, htlc, invpkg.ContractCanceled, setID, + canceled, _, err := getUpdatedHtlcState( + htlc, invpkg.ContractCanceled, setID, ) if err != nil { return nil, err } + + if canceled { + htlc.State = invpkg.HtlcStateCanceled + htlc.ResolveTime = timestamp + } } err = d.cancelInvoiceStoreUpdate(invoices, invoiceNum, invoice) @@ -2602,21 +2618,23 @@ func cancelSingleHtlc(resolveTime time.Time, htlc *invpkg.InvoiceHTLC, return nil } -// updateHtlc aligns the state of an htlc with the given invoice state. A -// boolean is returned if the HTLC was settled. -func updateHtlc(resolveTime time.Time, htlc *invpkg.InvoiceHTLC, - invState invpkg.ContractState, setID *[32]byte) (bool, error) { +// getUpdatedHtlcState aligns the state of an htlc with the given invoice state. +// A boolean indicating whether the HTLCs state need to be updated, along with +// the new state (or old state if no change is needed) is returned. +func getUpdatedHtlcState(htlc *invpkg.InvoiceHTLC, + invoiceState invpkg.ContractState, setID *[32]byte) ( + bool, invpkg.HtlcState, error) { - trySettle := func(persist bool) (bool, error) { + trySettle := func(persist bool) (bool, invpkg.HtlcState, error) { if htlc.State != invpkg.HtlcStateAccepted { - return false, nil + return false, htlc.State, nil } // Settle the HTLC if it matches the settled set id. If // there're other HTLCs with distinct setIDs, then we'll leave // them, as they may eventually be settled as we permit // multiple settles to a single pay_addr for AMP. - var htlcState invpkg.HtlcState + settled := false if htlc.IsInHTLCSet(setID) { // Non-AMP HTLCs can be settled immediately since we // already know the preimage is valid due to checks at @@ -2634,29 +2652,31 @@ func updateHtlc(resolveTime time.Time, htlc *invpkg.InvoiceHTLC, // // Fail if an accepted AMP HTLC has no preimage. case htlc.AMP.Preimage == nil: - return false, invpkg.ErrHTLCPreimageMissing + return false, htlc.State, + invpkg.ErrHTLCPreimageMissing // Fail if the accepted AMP HTLC has an invalid // preimage. case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash): - return false, invpkg.ErrHTLCPreimageMismatch + return false, htlc.State, + invpkg.ErrHTLCPreimageMismatch } - htlcState = invpkg.HtlcStateSettled + settled = true } // Only persist the changes if the invoice is moving to the // settled state, and we're actually updating the state to // settled. - if persist && htlcState == invpkg.HtlcStateSettled { - htlc.State = htlcState - htlc.ResolveTime = resolveTime + newState := htlc.State + if settled { + newState = invpkg.HtlcStateSettled } - return persist && htlcState == invpkg.HtlcStateSettled, nil + return persist && settled, newState, nil } - if invState == invpkg.ContractSettled { + if invoiceState == invpkg.ContractSettled { // Check that we can settle the HTLCs. For legacy and MPP HTLCs // this will be a NOP, but for AMP HTLCs this asserts that we // have a valid hash/preimage pair. Passing true permits the @@ -2667,16 +2687,13 @@ func updateHtlc(resolveTime time.Time, htlc *invpkg.InvoiceHTLC, // We should never find a settled HTLC on an invoice that isn't in // ContractSettled. if htlc.State == invpkg.HtlcStateSettled { - return false, invpkg.ErrHTLCAlreadySettled + return false, htlc.State, invpkg.ErrHTLCAlreadySettled } - switch invState { + switch invoiceState { case invpkg.ContractCanceled: - if htlc.State == invpkg.HtlcStateAccepted { - htlc.State = invpkg.HtlcStateCanceled - htlc.ResolveTime = resolveTime - } - return false, nil + htlcAlreadyCanceled := htlc.State == invpkg.HtlcStateCanceled + return !htlcAlreadyCanceled, invpkg.HtlcStateCanceled, nil // TODO(roasbeef): never fully passed thru now? case invpkg.ContractAccepted: @@ -2688,10 +2705,10 @@ func updateHtlc(resolveTime time.Time, htlc *invpkg.InvoiceHTLC, return trySettle(false) case invpkg.ContractOpen: - return false, nil + return false, htlc.State, nil default: - return false, errors.New("unknown state transition") + return false, htlc.State, errors.New("unknown state transition") } } From 87044b815cb61cdc388861de91d85427fc356525 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 24 Nov 2023 17:26:14 +0100 Subject: [PATCH 06/15] invoices: rename updateInvoiceState to getUpdatedInvoiceState This commit turns updateInvoiceState "const" by moving preimage update out of the function while also removing it to getUpdatedInvoiceState. --- channeldb/invoices.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 74b4681d5c..e6c96806b5 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -2086,7 +2086,7 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // change, which depends on having an accurate view of the accepted // HTLCs. if update.State != nil { - newState, err := updateInvoiceState( + newState, err := getUpdatedInvoiceState( invoice, hash, *update.State, ) if err != nil { @@ -2327,7 +2327,9 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, } // TODO(positiveblue): create a invoice.CanSettleHodlInvoice func. - newState, err := updateInvoiceState(invoice, hash, *update) + newState, err := getUpdatedInvoiceState( + invoice, hash, *update, + ) if err != nil { return nil, err } @@ -2338,6 +2340,7 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, } invoice.State = invpkg.ContractSettled + invoice.Terms.PaymentPreimage = update.Preimage timestamp := d.clock.Now() // TODO(positiveblue): this logic can be further simplified. @@ -2414,7 +2417,7 @@ func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, setID = update.SetID } - newState, err := updateInvoiceState(invoice, hash, *update) + newState, err := getUpdatedInvoiceState(invoice, hash, *update) if err != nil { return nil, err } @@ -2459,10 +2462,12 @@ func (d *DB) cancelInvoiceStoreUpdate(invoices kvdb.RwBucket, invoiceNum []byte, return d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) } -// updateInvoiceState validates and processes an invoice state update. The new -// state to transition to is returned, so the caller is able to select exactly -// how the invoice state is updated. -func updateInvoiceState(invoice *invpkg.Invoice, hash *lntypes.Hash, +// getUpdatedInvoiceState validates and processes an invoice state update. The +// new state to transition to is returned, so the caller is able to select +// exactly how the invoice state is updated. Note that for AMP invoices this +// function is only used to validate the state transition if we're cancelling +// the invoice. +func getUpdatedInvoiceState(invoice *invpkg.Invoice, hash *lntypes.Hash, update invpkg.InvoiceStateUpdateDesc) (*invpkg.ContractState, error) { // Returning to open is never allowed from any state. @@ -2505,6 +2510,7 @@ func updateInvoiceState(invoice *invpkg.Invoice, hash *lntypes.Hash, return nil, errors.New("AMP set cannot have " + "preimage") } + return &update.NewState, nil } @@ -2519,7 +2525,6 @@ func updateInvoiceState(invoice *invpkg.Invoice, hash *lntypes.Hash, if update.Preimage.Hash() != *hash { return nil, invpkg.ErrInvoicePreimageMismatch } - invoice.Terms.PaymentPreimage = update.Preimage // Permit non-AMP invoices to be accepted without knowing the // preimage. When trying to settle we'll have to pass through From 08df7f417568d26cc17b1b1baf19df6ad9f9ca49 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 24 Nov 2023 17:31:11 +0100 Subject: [PATCH 07/15] invoices: change cancelSingleHtlc to be purely used for validation This change moves the HTLC state change out of the cancelSingleHtlc function. This is part of the larger refactor of collecting all changes to be later applied by the invoice updater. --- channeldb/invoices.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index e6c96806b5..5e66ac3c25 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -1953,11 +1953,14 @@ func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, return nil, fmt.Errorf("cancel of non-existent htlc") } - err := cancelSingleHtlc(timestamp, htlc, invoice.State) + err := canCancelSingleHtlc(htlc, invoice.State) if err != nil { return nil, err } + htlc.State = invpkg.HtlcStateCanceled + htlc.ResolveTime = timestamp + // Tally this into the set of HTLCs that need to be updated on // disk, but once again, only if this is an AMP invoice. if invoice.IsAMP() { @@ -2600,26 +2603,22 @@ func updateInvoiceAmpState(invoice *invpkg.Invoice, setID invpkg.SetID, return nil } -// cancelSingleHtlc validates cancellation of a single htlc and update its -// state. -func cancelSingleHtlc(resolveTime time.Time, htlc *invpkg.InvoiceHTLC, - invState invpkg.ContractState) error { +// canCancelSingleHtlc validates cancellation of a single HTLC. If nil is +// returned, then the HTLC can be cancelled. +func canCancelSingleHtlc(htlc *invpkg.InvoiceHTLC, + invoiceState invpkg.ContractState) error { // It is only possible to cancel individual htlcs on an open invoice. - if invState != invpkg.ContractOpen { - return fmt.Errorf("htlc canceled on invoice in "+ - "state %v", invState) + if invoiceState != invpkg.ContractOpen { + return fmt.Errorf("htlc canceled on invoice in state %v", + invoiceState) } // It is only possible if the htlc is still pending. if htlc.State != invpkg.HtlcStateAccepted { - return fmt.Errorf("htlc canceled in state %v", - htlc.State) + return fmt.Errorf("htlc canceled in state %v", htlc.State) } - htlc.State = invpkg.HtlcStateCanceled - htlc.ResolveTime = resolveTime - return nil } From 998156930f482e705ecacd3d033af266f7b65c17 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 24 Nov 2023 17:39:07 +0100 Subject: [PATCH 08/15] invoices: refactor updateInvoiceAmpState to return new state With this commit updateInvoiceAmpState becomes getUpdatedInvoiceAmpState which will only return the new AMP state but that needs to be applied at the call site. This is a part of a larger refactor to gather all mutations of an invoice update to be later applied by the invoice updater. --- channeldb/invoices.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index 5e66ac3c25..ff88b46a32 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -558,6 +558,7 @@ func (d *DB) QueryInvoices(_ context.Context, q invpkg.InvoiceQuery) ( // At this point, we've exhausted the offset, so we'll // begin collecting invoices found within the range. resp.Invoices = append(resp.Invoices, invoice) + return true, nil } @@ -1777,13 +1778,15 @@ func updateHtlcsAmp(invoice *invpkg.Invoice, htlc *invpkg.InvoiceHTLC, setID invpkg.SetID, circuitKey models.CircuitKey) error { - err := updateInvoiceAmpState( + newAmpState, err := getUpdatedInvoiceAmpState( invoice, setID, circuitKey, invpkg.HtlcStateAccepted, htlc.Amt, ) if err != nil { return err } + invoice.AMPState[setID] = newAmpState + // Now that we've updated the invoice state, we'll inform the caller of // the _neitre_ HTLC set they need to write for this new set ID. if _, ok := updateMap[setID]; !ok { @@ -1810,13 +1813,15 @@ func cancelHtlcsAmp(invoice *invpkg.Invoice, setID := htlc.AMP.Record.SetID() // First, we'll update the state of the entire HTLC set to cancelled. - err := updateInvoiceAmpState( + newAmpState, err := getUpdatedInvoiceAmpState( invoice, setID, circuitKey, invpkg.HtlcStateCanceled, htlc.Amt, ) if err != nil { return err } + invoice.AMPState[setID] = newAmpState + if _, ok := updateMap[setID]; !ok { // Only HTLCs in the accepted state, can be cancelled, but we // also want to merge that with HTLCs that may be canceled as @@ -1861,13 +1866,15 @@ func settleHtlcsAmp(invoice *invpkg.Invoice, // Next update the main AMP meta-data to indicate that this HTLC set // has been fully settled. - err := updateInvoiceAmpState( + newAmpState, err := getUpdatedInvoiceAmpState( invoice, setID, circuitKey, invpkg.HtlcStateSettled, 0, ) if err != nil { return err } + invoice.AMPState[setID] = newAmpState + // Finally, we'll add this to the set of HTLCs that need to be updated. if _, ok := updateMap[setID]; !ok { mapEntry := make(map[models.CircuitKey]*invpkg.InvoiceHTLC) @@ -2557,11 +2564,12 @@ func getUpdatedInvoiceState(invoice *invpkg.Invoice, hash *lntypes.Hash, } } -// updateInvoiceAmpState updates the AMP state of an invoice, given the new -// state, and the amount of the HTLC that is being updated. -func updateInvoiceAmpState(invoice *invpkg.Invoice, setID invpkg.SetID, +// getUpdatedInvoiceAmpState returns the AMP state of an invoice (without +// applying it), given the new state, and the amount of the HTLC that is +// being updated. +func getUpdatedInvoiceAmpState(invoice *invpkg.Invoice, setID invpkg.SetID, circuitKey models.CircuitKey, state invpkg.HtlcState, - amt lnwire.MilliSatoshi) error { + amt lnwire.MilliSatoshi) (invpkg.InvoiceStateAMP, error) { // Retrieve the AMP state for this set ID. ampState, ok := invoice.AMPState[setID] @@ -2570,8 +2578,9 @@ func updateInvoiceAmpState(invoice *invpkg.Invoice, setID invpkg.SetID, // this set ID, otherwise we expect that the entry already exists and // we can update it. if !ok && state != invpkg.HtlcStateAccepted { - return fmt.Errorf("unable to update AMP state for setID=%x ", - setID) + return invpkg.InvoiceStateAMP{}, + fmt.Errorf("unable to update AMP state for setID=%x ", + setID) } switch state { @@ -2598,9 +2607,8 @@ func updateInvoiceAmpState(invoice *invpkg.Invoice, setID invpkg.SetID, } ampState.InvoiceKeys[circuitKey] = struct{}{} - invoice.AMPState[setID] = ampState - return nil + return ampState, nil } // canCancelSingleHtlc validates cancellation of a single HTLC. If nil is From ecbfc463125c301cba700c15eff6939359efb45c Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 24 Nov 2023 18:51:49 +0100 Subject: [PATCH 09/15] invoices+channeldb: add InvoiceUpdater interface and the KV impl This commit introduces the InvoiceUpdater interface which is meant to abstract and assist the in-memory invoice update procedure with the accompanying database updates. These abstract updater steps will enable further refactoring later while also ensuring that a full SQL implementation of the InvoiceDB interface will be possible. --- channeldb/invoices.go | 881 +++++++++++++++++++++++------------------- invoices/interface.go | 36 ++ 2 files changed, 527 insertions(+), 390 deletions(-) diff --git a/channeldb/invoices.go b/channeldb/invoices.go index ff88b46a32..d134c03b40 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -607,7 +607,9 @@ func (d *DB) QueryInvoices(_ context.Context, q invpkg.InvoiceQuery) ( // // The update is performed inside the same database transaction that fetches the // invoice and is therefore atomic. The fields to update are controlled by the -// supplied callback. +// supplied callback. When updating an invoice, the update itself happens +// in-memory on a copy of the invoice. Once it is written successfully to the +// database, the in-memory copy is returned to the caller. func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef, setIDHint *invpkg.SetID, callback invpkg.InvoiceUpdateCallback) ( *invpkg.Invoice, error) { @@ -657,10 +659,22 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef, return err } + now := d.clock.Now() + updater := &kvInvoiceUpdater{ + db: d, + invoicesBucket: invoices, + settleIndexBucket: settleIndex, + setIDIndexBucket: setIDIndex, + updateTime: now, + invoiceNum: invoiceNum, + invoice: &invoice, + updatedAmpHtlcs: make(ampHTLCsMap), + settledSetIDs: make(map[invpkg.SetID]struct{}), + } + payHash := ref.PayHash() - updatedInvoice, err = d.updateInvoice( - payHash, invoices, settleIndex, setIDIndex, - &invoice, invoiceNum, callback, + updatedInvoice, err = updateInvoice( + payHash, updater.invoice, now, callback, updater, ) return err @@ -671,6 +685,316 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef, return updatedInvoice, err } +// ampHTLCsMap is a map of AMP HTLCs affected by an invoice update. +type ampHTLCsMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC + +// kvInvoiceUpdater is an implementation of the InvoiceUpdater interface that +// is used with the kv implementation of the invoice database. Note that this +// updater is not concurrency safe and synchronizaton is expected to be handled +// on the DB level. +type kvInvoiceUpdater struct { + db *DB + invoicesBucket kvdb.RwBucket + settleIndexBucket kvdb.RwBucket + setIDIndexBucket kvdb.RwBucket + + // updateTime is the timestamp for the update. + updateTime time.Time + + // invoiceNum is a legacy key similar to the add index that is used + // only in the kv implementation. + invoiceNum []byte + + // invoice is the invoice that we're updating. As a side effect of the + // update this invoice will be mutated. + invoice *invpkg.Invoice + + // updatedAmpHtlcs holds the set of AMP HTLCs that were added or + // cancelled as part of this update. + updatedAmpHtlcs ampHTLCsMap + + // settledSetIDs holds the set IDs that are settled with this update. + settledSetIDs map[invpkg.SetID]struct{} +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) AddHtlc(_ models.CircuitKey, + _ *invpkg.InvoiceHTLC) error { + + return nil +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) ResolveHtlc(_ models.CircuitKey, _ invpkg.HtlcState, + _ time.Time) error { + + return nil +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) AddAmpHtlcPreimage(_ [32]byte, _ models.CircuitKey, + _ lntypes.Preimage) error { + + return nil +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) UpdateInvoiceState(_ invpkg.ContractState, + _ *lntypes.Preimage) error { + + return nil +} + +// NOTE: this method does nothing in the k/v implementation of InvoiceUpdater. +func (k *kvInvoiceUpdater) UpdateInvoiceAmtPaid(_ lnwire.MilliSatoshi) error { + return nil +} + +// UpdateAmpState updates the state of the AMP invoice identified by the setID. +func (k *kvInvoiceUpdater) UpdateAmpState(setID [32]byte, + state invpkg.InvoiceStateAMP, circuitKey models.CircuitKey) error { + + if _, ok := k.updatedAmpHtlcs[setID]; !ok { + switch state.State { + case invpkg.HtlcStateAccepted: + // If we're just now creating the HTLCs for this set + // then we'll also pull in the existing HTLCs that are + // part of this set, so we can write them all to disk + // together (same value) + k.updatedAmpHtlcs[setID] = k.invoice.HTLCSet( + &setID, invpkg.HtlcStateAccepted, + ) + + case invpkg.HtlcStateCanceled: + // Only HTLCs in the accepted state, can be cancelled, + // but we also want to merge that with HTLCs that may be + // canceled as well since it can be cancelled one by + // one. + k.updatedAmpHtlcs[setID] = k.invoice.HTLCSet( + &setID, invpkg.HtlcStateAccepted, + ) + + cancelledHtlcs := k.invoice.HTLCSet( + &setID, invpkg.HtlcStateCanceled, + ) + for htlcKey, htlc := range cancelledHtlcs { + k.updatedAmpHtlcs[setID][htlcKey] = htlc + } + + case invpkg.HtlcStateSettled: + k.updatedAmpHtlcs[setID] = make( + map[models.CircuitKey]*invpkg.InvoiceHTLC, + ) + } + } + + if state.State == invpkg.HtlcStateSettled { + // Add the set ID to the set that was settled in this invoice + // update. We'll use this later to update the settle index. + k.settledSetIDs[setID] = struct{}{} + } + + k.updatedAmpHtlcs[setID][circuitKey] = k.invoice.Htlcs[circuitKey] + + return nil +} + +// Finalize finalizes the update before it is written to the database. +func (k *kvInvoiceUpdater) Finalize(updateType invpkg.UpdateType) error { + switch updateType { + case invpkg.AddHTLCsUpdate: + return k.storeAddHtlcsUpdate() + + case invpkg.CancelHTLCsUpdate: + return k.storeCancelHtlcsUpdate() + + case invpkg.SettleHodlInvoiceUpdate: + return k.storeSettleHodlInvoiceUpdate() + + case invpkg.CancelInvoiceUpdate: + return k.serializeAndStoreInvoice() + } + + return fmt.Errorf("unknown update type: %v", updateType) +} + +// storeCancelHtlcsUpdate updates the invoice in the database after cancelling a +// set of HTLCs. +func (k *kvInvoiceUpdater) storeCancelHtlcsUpdate() error { + err := k.serializeAndStoreInvoice() + if err != nil { + return err + } + + // If this is an AMP invoice, then we'll actually store the rest + // of the HTLCs in-line with the invoice, using the invoice ID + // as a prefix, and the AMP key as a suffix: invoiceNum || + // setID. + if k.invoice.IsAMP() { + return k.updateAMPInvoices() + } + + return nil +} + +// storeAddHtlcsUpdate updates the invoice in the database after adding a set of +// HTLCs. +func (k *kvInvoiceUpdater) storeAddHtlcsUpdate() error { + invoiceIsAMP := k.invoice.IsAMP() + + for htlcSetID := range k.updatedAmpHtlcs { + // Check if this SetID already exist. + setIDInvNum := k.setIDIndexBucket.Get(htlcSetID[:]) + + if setIDInvNum == nil { + err := k.setIDIndexBucket.Put( + htlcSetID[:], k.invoiceNum, + ) + if err != nil { + return err + } + } else if !bytes.Equal(setIDInvNum, k.invoiceNum) { + return invpkg.ErrDuplicateSetID{ + SetID: htlcSetID, + } + } + } + + // If this is a non-AMP invoice, then the state can eventually go to + // ContractSettled, so we pass in nil value as part of + // setSettleMetaFields. + if !invoiceIsAMP && k.invoice.State == invpkg.ContractSettled { + err := k.setSettleMetaFields(nil) + if err != nil { + return err + } + } + + // As we don't update the settle index above for AMP invoices, we'll do + // it here for each sub-AMP invoice that was settled. + for settledSetID := range k.settledSetIDs { + settledSetID := settledSetID + err := k.setSettleMetaFields(&settledSetID) + if err != nil { + return err + } + } + + err := k.serializeAndStoreInvoice() + if err != nil { + return err + } + + // If this is an AMP invoice, then we'll actually store the rest of the + // HTLCs in-line with the invoice, using the invoice ID as a prefix, + // and the AMP key as a suffix: invoiceNum || setID. + if invoiceIsAMP { + return k.updateAMPInvoices() + } + + return nil +} + +// storeSettleHodlInvoiceUpdate updates the invoice in the database after +// settling a hodl invoice. +func (k *kvInvoiceUpdater) storeSettleHodlInvoiceUpdate() error { + err := k.setSettleMetaFields(nil) + if err != nil { + return err + } + + return k.serializeAndStoreInvoice() +} + +// setSettleMetaFields updates the metadata associated with settlement of an +// invoice. If a non-nil setID is passed in, then the value will be append to +// the invoice number as well, in order to allow us to detect repeated payments +// to the same AMP invoices "across time". +func (k *kvInvoiceUpdater) setSettleMetaFields(setID *invpkg.SetID) error { + // Now that we know the invoice hasn't already been settled, we'll + // update the settle index so we can place this settle event in the + // proper location within our time series. + nextSettleSeqNo, err := k.settleIndexBucket.NextSequence() + if err != nil { + return err + } + + // Make a new byte array on the stack that can potentially store the 4 + // byte invoice number along w/ the 32 byte set ID. We capture valueLen + // here which is the number of bytes copied so we can only store the 4 + // bytes if this is a non-AMP invoice. + var indexKey [invoiceSetIDKeyLen]byte + valueLen := copy(indexKey[:], k.invoiceNum) + + if setID != nil { + valueLen += copy(indexKey[valueLen:], setID[:]) + } + + var seqNoBytes [8]byte + byteOrder.PutUint64(seqNoBytes[:], nextSettleSeqNo) + err = k.settleIndexBucket.Put(seqNoBytes[:], indexKey[:valueLen]) + if err != nil { + return err + } + + // If the setID is nil, then this means that this is a non-AMP settle, + // so we'll update the invoice settle index directly. + if setID == nil { + k.invoice.SettleDate = k.updateTime + k.invoice.SettleIndex = nextSettleSeqNo + } else { + // If the set ID isn't blank, we'll update the AMP state map + // which tracks when each of the setIDs associated with a given + // AMP invoice are settled. + ampState := k.invoice.AMPState[*setID] + + ampState.SettleDate = k.updateTime + ampState.SettleIndex = nextSettleSeqNo + + k.invoice.AMPState[*setID] = ampState + } + + return nil +} + +// updateAMPInvoices updates the set of AMP invoices in-place. For AMP, rather +// then continually write the invoices to the end of the invoice value, we +// instead write the invoices into a new key preifx that follows the main +// invoice number. This ensures that we don't need to continually decode a +// potentially massive HTLC set, and also allows us to quickly find the HLTCs +// associated with a particular HTLC set. +func (k *kvInvoiceUpdater) updateAMPInvoices() error { + for setID, htlcSet := range k.updatedAmpHtlcs { + // First write out the set of HTLCs including all the relevant + // TLV values. + var b bytes.Buffer + if err := serializeHtlcs(&b, htlcSet); err != nil { + return err + } + + // Next store each HTLC in-line, using a prefix based off the + // invoice number. + invoiceSetIDKey := makeInvoiceSetIDKey(k.invoiceNum, setID[:]) + + err := k.invoicesBucket.Put(invoiceSetIDKey[:], b.Bytes()) + if err != nil { + return err + } + } + + return nil +} + +// serializeAndStoreInvoice is a helper function used to store invoices. +func (k *kvInvoiceUpdater) serializeAndStoreInvoice() error { + var buf bytes.Buffer + if err := serializeInvoice(&buf, k.invoice); err != nil { + return err + } + + return k.invoicesBucket.Put(k.invoiceNum, buf.Bytes()) +} + // InvoicesSettledSince can be used by callers to catch up any settled invoices // they missed within the settled invoice time series. We'll return all known // settled invoice that have a settle index higher than the passed @@ -1740,43 +2064,12 @@ func makeInvoiceSetIDKey(invoiceNum, setID []byte) [invoiceSetIDKeyLen]byte { return invoiceSetIDKey } -// updateAMPInvoices updates the set of AMP invoices in-place. For AMP, rather -// then continually write the invoices to the end of the invoice value, we -// instead write the invoices into a new key preifx that follows the main -// invoice number. This ensures that we don't need to continually decode a -// potentially massive HTLC set, and also allows us to quickly find the HLTCs -// associated with a particular HTLC set. -func updateAMPInvoices(invoiceBucket kvdb.RwBucket, invoiceNum []byte, - htlcsToUpdate map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) error { //nolint:lll - - for setID, htlcSet := range htlcsToUpdate { - // First write out the set of HTLCs including all the relevant - // TLV values. - var b bytes.Buffer - if err := serializeHtlcs(&b, htlcSet); err != nil { - return err - } - - // Next store each HTLC in-line, using a prefix based off the - // invoice number. - invoiceSetIDKey := makeInvoiceSetIDKey(invoiceNum, setID[:]) - - err := invoiceBucket.Put(invoiceSetIDKey[:], b.Bytes()) - if err != nil { - return err - } - } - - return nil -} - // updateHtlcsAmp takes an invoice, and a new HTLC to be added (along with its // set ID), and updates the internal AMP state of an invoice, and also tallies // the set of HTLCs to be updated on disk. -func updateHtlcsAmp(invoice *invpkg.Invoice, - updateMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - htlc *invpkg.InvoiceHTLC, setID invpkg.SetID, - circuitKey models.CircuitKey) error { +func acceptHtlcsAmp(invoice *invpkg.Invoice, setID invpkg.SetID, + circuitKey models.CircuitKey, htlc *invpkg.InvoiceHTLC, + updater invpkg.InvoiceUpdater) error { newAmpState, err := getUpdatedInvoiceAmpState( invoice, setID, circuitKey, invpkg.HtlcStateAccepted, htlc.Amt, @@ -1787,34 +2080,24 @@ func updateHtlcsAmp(invoice *invpkg.Invoice, invoice.AMPState[setID] = newAmpState - // Now that we've updated the invoice state, we'll inform the caller of - // the _neitre_ HTLC set they need to write for this new set ID. - if _, ok := updateMap[setID]; !ok { - // If we're just now creating the HTLCs for this set then we'll - // also pull in the existing HTLCs are part of this set, so we - // can write them all to disk together (same value) - updateMap[setID] = invoice.HTLCSet( - (*[32]byte)(&setID), invpkg.HtlcStateAccepted, - ) - } - updateMap[setID][circuitKey] = htlc - - return nil + // Mark the updates as needing to be written to disk. + return updater.UpdateAmpState(setID, newAmpState, circuitKey) } // 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 // 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. -func cancelHtlcsAmp(invoice *invpkg.Invoice, - updateMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - htlc *invpkg.InvoiceHTLC, circuitKey models.CircuitKey) error { +func cancelHtlcsAmp(invoice *invpkg.Invoice, circuitKey models.CircuitKey, + htlc *invpkg.InvoiceHTLC, updater invpkg.InvoiceUpdater) error { setID := htlc.AMP.Record.SetID() - // First, we'll update the state of the entire HTLC set to cancelled. + // First, we'll update the state of the entire HTLC set + // to cancelled. newAmpState, err := getUpdatedInvoiceAmpState( - invoice, setID, circuitKey, invpkg.HtlcStateCanceled, htlc.Amt, + invoice, setID, circuitKey, invpkg.HtlcStateCanceled, + htlc.Amt, ) if err != nil { return err @@ -1822,30 +2105,18 @@ func cancelHtlcsAmp(invoice *invpkg.Invoice, invoice.AMPState[setID] = newAmpState - if _, ok := updateMap[setID]; !ok { - // Only HTLCs in the accepted state, can be cancelled, but we - // also want to merge that with HTLCs that may be canceled as - // well since it can be cancelled one by one. - updateMap[setID] = invoice.HTLCSet( - &setID, invpkg.HtlcStateAccepted, - ) - - cancelledHtlcs := invoice.HTLCSet( - &setID, invpkg.HtlcStateCanceled, - ) - for htlcKey, htlc := range cancelledHtlcs { - updateMap[setID][htlcKey] = htlc - } + // Mark the updates as needing to be written to disk. + err = updater.UpdateAmpState(setID, newAmpState, circuitKey) + if err != nil { + return err } - // Finally, include the newly cancelled HTLC in the set of HTLCs we - // need to cancel. - updateMap[setID][circuitKey] = htlc - // We'll only decrement the total amount paid if the invoice was // already in the accepted state. if invoice.AmtPaid != 0 { - invoice.AmtPaid -= htlc.Amt + return updateInvoiceAmtPaid( + invoice, invoice.AmtPaid-htlc.Amt, updater, + ) } return nil @@ -1854,15 +2125,10 @@ func cancelHtlcsAmp(invoice *invpkg.Invoice, // settleHtlcsAmp processes a new settle operation on an HTLC set for an AMP // invoice. We'll update some meta data in the main invoice, and also signal // that this HTLC set needs to be re-written back to disk. -func settleHtlcsAmp(invoice *invpkg.Invoice, - settledSetIDs map[invpkg.SetID]struct{}, - updateMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - htlc *invpkg.InvoiceHTLC, circuitKey models.CircuitKey) error { +func settleHtlcsAmp(invoice *invpkg.Invoice, circuitKey models.CircuitKey, + htlc *invpkg.InvoiceHTLC, updater invpkg.InvoiceUpdater) error { - // First, add the set ID to the set that was settled in this invoice - // update. We'll use this later to update the settle index. setID := htlc.AMP.Record.SetID() - settledSetIDs[setID] = struct{}{} // Next update the main AMP meta-data to indicate that this HTLC set // has been fully settled. @@ -1875,22 +2141,15 @@ func settleHtlcsAmp(invoice *invpkg.Invoice, invoice.AMPState[setID] = newAmpState - // Finally, we'll add this to the set of HTLCs that need to be updated. - if _, ok := updateMap[setID]; !ok { - mapEntry := make(map[models.CircuitKey]*invpkg.InvoiceHTLC) - updateMap[setID] = mapEntry - } - updateMap[setID][circuitKey] = htlc - - return nil + // Mark the updates as needing to be written to disk. + return updater.UpdateAmpState(setID, newAmpState, circuitKey) } // updateInvoice fetches the invoice, obtains the update descriptor from the // callback and applies the updates in a single db transaction. -func (d *DB) updateInvoice(hash *lntypes.Hash, invoices, - settleIndex, setIDIndex kvdb.RwBucket, invoice *invpkg.Invoice, - invoiceNum []byte, callback invpkg.InvoiceUpdateCallback) ( - *invpkg.Invoice, error) { +func updateInvoice(hash *lntypes.Hash, invoice *invpkg.Invoice, + updateTime time.Time, callback invpkg.InvoiceUpdateCallback, + updater invpkg.InvoiceUpdater) (*invpkg.Invoice, error) { // Create deep copy to prevent any accidental modification in the // callback. @@ -1912,147 +2171,106 @@ func (d *DB) updateInvoice(hash *lntypes.Hash, invoices, switch update.UpdateType { case invpkg.CancelHTLCsUpdate: - return d.cancelHTLCs(invoices, invoiceNum, invoice, update) + err := cancelHTLCs(invoice, updateTime, update, updater) + if err != nil { + return nil, err + } case invpkg.AddHTLCsUpdate: - return d.addHTLCs( - invoices, settleIndex, setIDIndex, invoiceNum, invoice, - hash, update, - ) + err := addHTLCs(invoice, hash, updateTime, update, updater) + if err != nil { + return nil, err + } case invpkg.SettleHodlInvoiceUpdate: - return d.settleHodlInvoice( - invoices, settleIndex, invoiceNum, invoice, hash, - update.State, + err := settleHodlInvoice( + invoice, hash, updateTime, update.State, updater, ) + if err != nil { + return nil, err + } case invpkg.CancelInvoiceUpdate: - return d.cancelInvoice( - invoices, invoiceNum, invoice, hash, update.State, + err := cancelInvoice( + invoice, hash, updateTime, update.State, updater, ) + if err != nil { + return nil, err + } default: return nil, fmt.Errorf("unknown update type: %s", update.UpdateType) } + + if err := updater.Finalize(update.UpdateType); err != nil { + return nil, err + } + + return invoice, nil } // cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc. // // NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the // InvoiceUpdateDesc. -func (d *DB) cancelHTLCs(invoices kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice, update *invpkg.InvoiceUpdateDesc) ( - *invpkg.Invoice, error) { - - timestamp := d.clock.Now() +func cancelHTLCs(invoice *invpkg.Invoice, updateTime time.Time, + update *invpkg.InvoiceUpdateDesc, updater invpkg.InvoiceUpdater) error { - // Process add actions from update descriptor. - htlcsAmpUpdate := make(map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) //nolint:lll - - // Process cancel actions from update descriptor. for key := range update.CancelHtlcs { htlc, exists := invoice.Htlcs[key] // Verify that we don't get an action for htlcs that are not // present on the invoice. if !exists { - return nil, fmt.Errorf("cancel of non-existent htlc") + return fmt.Errorf("cancel of non-existent htlc") } err := canCancelSingleHtlc(htlc, invoice.State) if err != nil { - return nil, err + return err } - htlc.State = invpkg.HtlcStateCanceled - htlc.ResolveTime = timestamp + err = resolveHtlc( + key, htlc, invpkg.HtlcStateCanceled, updateTime, + updater, + ) + if err != nil { + return err + } // Tally this into the set of HTLCs that need to be updated on // disk, but once again, only if this is an AMP invoice. if invoice.IsAMP() { - err := cancelHtlcsAmp( - invoice, htlcsAmpUpdate, htlc, key, - ) + err := cancelHtlcsAmp(invoice, key, htlc, updater) if err != nil { - return nil, err + return err } } } - err := d.cancelHTLCsStoreUpdate( - invoices, invoiceNum, invoice, htlcsAmpUpdate, - ) - if err != nil { - return nil, err - } - - return invoice, nil -} - -// cancelHTLCsStoreUpdate is a helper function used to store the invoice and -// AMP state after canceling HTLCs. -func (d *DB) cancelHTLCsStoreUpdate(invoices kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice, - htlcsAmpUpdate map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) error { - - err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) - if err != nil { - return err - } - - // If this is an AMP invoice, then we'll actually store the rest - // of the HTLCs in-line with the invoice, using the invoice ID - // as a prefix, and the AMP key as a suffix: invoiceNum || - // setID. - if invoice.IsAMP() { - err := updateAMPInvoices( - invoices, invoiceNum, htlcsAmpUpdate, - ) - if err != nil { - return err - } - } - return nil } -// serializeAndStoreInvoice is a helper function used to store invoices. -func (d *DB) serializeAndStoreInvoice(invoices kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice) error { - - var buf bytes.Buffer - if err := serializeInvoice(&buf, invoice); err != nil { - return err - } - - return invoices.Put(invoiceNum, buf.Bytes()) -} - // addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc. -func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen - setIDIndex kvdb.RwBucket, invoiceNum []byte, invoice *invpkg.Invoice, - hash *lntypes.Hash, update *invpkg.InvoiceUpdateDesc) (*invpkg.Invoice, - error) { +func addHTLCs(invoice *invpkg.Invoice, hash *lntypes.Hash, updateTime time.Time, + update *invpkg.InvoiceUpdateDesc, updater invpkg.InvoiceUpdater) error { var setID *[32]byte invoiceIsAMP := invoice.IsAMP() if invoiceIsAMP && update.State != nil { setID = update.State.SetID } - timestamp := d.clock.Now() - // Process add actions from update descriptor. - htlcsAmpUpdate := make(map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC) //nolint:lll for key, htlcUpdate := range update.AddHtlcs { if _, exists := invoice.Htlcs[key]; exists { - return nil, fmt.Errorf("duplicate add of htlc %v", key) + return fmt.Errorf("duplicate add of htlc %v", key) } // Force caller to supply htlc without custom records in a // consistent way. if htlcUpdate.CustomRecords == nil { - return nil, errors.New("nil custom records map") + return errors.New("nil custom records map") } htlc := &invpkg.InvoiceHTLC{ @@ -2060,14 +2278,14 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen MppTotalAmt: htlcUpdate.MppTotalAmt, Expiry: htlcUpdate.Expiry, AcceptHeight: uint32(htlcUpdate.AcceptHeight), - AcceptTime: timestamp, + AcceptTime: updateTime, State: invpkg.HtlcStateAccepted, CustomRecords: htlcUpdate.CustomRecords, } if invoiceIsAMP { if htlcUpdate.AMP == nil { - return nil, fmt.Errorf("unable to add htlc "+ + return fmt.Errorf("unable to add htlc "+ "without AMP data to AMP invoice(%v)", invoice.AddIndex) } @@ -2075,17 +2293,21 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen htlc.AMP = htlcUpdate.AMP.Copy() } + if err := updater.AddHtlc(key, htlc); err != nil { + return err + } + invoice.Htlcs[key] = htlc // Collect the set of new HTLCs so we can write them properly // below, but only if this is an AMP invoice. if invoiceIsAMP { - err := updateHtlcsAmp( - invoice, htlcsAmpUpdate, htlc, - htlcUpdate.AMP.Record.SetID(), key, + err := acceptHtlcsAmp( + invoice, htlcUpdate.AMP.Record.SetID(), key, + htlc, updater, ) if err != nil { - return nil, err + return err } } } @@ -2100,7 +2322,7 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen invoice, hash, *update.State, ) if err != nil { - return nil, err + return err } // If this isn't an AMP invoice, then we'll go ahead and update @@ -2109,6 +2331,10 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // each _htlc set_ instead. However, we'll allow the invoice to // transition to the cancelled state regardless. if !invoiceIsAMP || *newState == invpkg.ContractCanceled { + err := updater.UpdateInvoiceState(*newState, nil) + if err != nil { + return err + } invoice.State = *newState } } @@ -2123,10 +2349,8 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // With any invoice level state transitions recorded, we'll now // finalize the process by updating the state transitions for // individual HTLCs - var ( - settledSetIDs = make(map[invpkg.SetID]struct{}) - amtPaid lnwire.MilliSatoshi - ) + var amtPaid lnwire.MilliSatoshi + for key, htlc := range invoice.Htlcs { // Set the HTLC preimage for any AMP HTLCs. if setID != nil && update.State != nil { @@ -2135,13 +2359,19 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // If we don't already have a preimage for this HTLC, we // can set it now. case ok && htlc.AMP.Preimage == nil: + err := updater.AddAmpHtlcPreimage( + htlc.AMP.Record.SetID(), key, preimage, + ) + if err != nil { + return err + } htlc.AMP.Preimage = &preimage // Otherwise, prevent over-writing an existing // preimage. Ignore the case where the preimage is // identical. case ok && *htlc.AMP.Preimage != preimage: - return nil, invpkg.ErrHTLCPreimageAlreadyExists + return invpkg.ErrHTLCPreimageAlreadyExists } } @@ -2158,17 +2388,20 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen if settleEligibleAMP { htlcContextState = invpkg.ContractSettled } - htlcStateChanged, htlcState, err := getUpdatedHtlcState( htlc, htlcContextState, setID, ) if err != nil { - return nil, err + return err } if htlcStateChanged { - htlc.State = htlcState - htlc.ResolveTime = timestamp + err = resolveHtlc( + key, htlc, htlcState, updateTime, updater, + ) + if err != nil { + return err + } } htlcSettled := htlcStateChanged && @@ -2178,12 +2411,9 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // is an AMP invoice, then we'll need to update some additional // meta data state. if htlcSettled && invoiceIsAMP { - err = settleHtlcsAmp( - invoice, settledSetIDs, htlcsAmpUpdate, htlc, - key, - ) + err = settleHtlcsAmp(invoice, key, htlc, updater) if err != nil { - return nil, err + return err } } @@ -2223,88 +2453,35 @@ func (d *DB) addHTLCs(invoices, settleIndex, //nolint:funlen // For non-AMP invoices we recalculate the amount paid from scratch // each time, while for AMP invoices, we'll accumulate only based on // newly added HTLCs. - if !invoiceIsAMP { - invoice.AmtPaid = amtPaid - } else { - invoice.AmtPaid += amtPaid - } - - err := d.addHTLCsStoreUpdate( - invoices, settleIndex, setIDIndex, invoiceNum, invoice, - settledSetIDs, htlcsAmpUpdate, timestamp, - ) - if err != nil { - return nil, err + if invoiceIsAMP { + amtPaid += invoice.AmtPaid } - return invoice, nil + return updateInvoiceAmtPaid(invoice, amtPaid, updater) } -// addHTLCsStoreUpdate is a helper function used to store the invoice and -// AMP state after adding HTLCs. -func (d *DB) addHTLCsStoreUpdate(invoices, settleIndex, setIDIndex kvdb.RwBucket, - invoiceNum []byte, invoice *invpkg.Invoice, - settledSetIDs map[invpkg.SetID]struct{}, - htlcsAmpUpdate map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC, - timestamp time.Time) error { - - invoiceIsAMP := invoice.IsAMP() +func resolveHtlc(circuitKey models.CircuitKey, htlc *invpkg.InvoiceHTLC, + state invpkg.HtlcState, resolveTime time.Time, + updater invpkg.InvoiceUpdater) error { - for htlcSetID := range htlcsAmpUpdate { - // Check if this SetID already exist. - setIDInvNum := setIDIndex.Get(htlcSetID[:]) - - if setIDInvNum == nil { - err := setIDIndex.Put(htlcSetID[:], invoiceNum) - if err != nil { - return err - } - } else if !bytes.Equal(setIDInvNum, invoiceNum) { - return invpkg.ErrDuplicateSetID{ - SetID: htlcSetID, - } - } + err := updater.ResolveHtlc(circuitKey, state, resolveTime) + if err != nil { + return err } + htlc.State = state + htlc.ResolveTime = resolveTime - // If this is a non-AMP invoice, then the state can eventually go to - // ContractSettled, so we pass in nil value as part of - // setSettleMetaFields. - if !invoiceIsAMP && invoice.State == invpkg.ContractSettled { - err := setSettleMetaFields( - settleIndex, invoiceNum, invoice, timestamp, nil, - ) - if err != nil { - return err - } - } + return nil +} - // As we don't update the settle index above for AMP invoices, we'll do - // it here for each sub-AMP invoice that was settled. - for settledSetID := range settledSetIDs { - settledSetID := settledSetID - err := setSettleMetaFields( - settleIndex, invoiceNum, invoice, timestamp, - &settledSetID, - ) - if err != nil { - return err - } - } +func updateInvoiceAmtPaid(invoice *invpkg.Invoice, amt lnwire.MilliSatoshi, + updater invpkg.InvoiceUpdater) error { - err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + err := updater.UpdateInvoiceAmtPaid(amt) if err != nil { return err } - - // If this is an AMP invoice, then we'll actually store the rest of the - // HTLCs in-line with the invoice, using the invoice ID as a prefix, - // and the AMP key as a suffix: invoiceNum || setID. - if invoiceIsAMP { - err := updateAMPInvoices(invoices, invoiceNum, htlcsAmpUpdate) - if err != nil { - return err - } - } + invoice.AmtPaid = amt return nil } @@ -2312,12 +2489,13 @@ func (d *DB) addHTLCsStoreUpdate(invoices, settleIndex, setIDIndex kvdb.RwBucket // settleHodlInvoice marks a hodl invoice as settled. // // NOTE: Currently it is not possible to have HODL AMP invoices. -func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, - invoiceNum []byte, invoice *invpkg.Invoice, hash *lntypes.Hash, - update *invpkg.InvoiceStateUpdateDesc) (*invpkg.Invoice, error) { + +func settleHodlInvoice(invoice *invpkg.Invoice, hash *lntypes.Hash, + updateTime time.Time, update *invpkg.InvoiceStateUpdateDesc, + updater invpkg.InvoiceUpdater) error { if !invoice.HodlInvoice { - return nil, fmt.Errorf("unable to settle hodl invoice: %v is "+ + return fmt.Errorf("unable to settle hodl invoice: %v is "+ "not a hodl invoice", invoice.AddIndex) } @@ -2328,92 +2506,74 @@ func (d *DB) settleHodlInvoice(invoices, settleIndex kvdb.RwBucket, fallthrough case update.NewState != invpkg.ContractSettled: - return nil, fmt.Errorf("unable to settle hodl invoice: "+ + return fmt.Errorf("unable to settle hodl invoice: "+ "not valid InvoiceUpdateDesc.State: %v", update) case update.Preimage == nil: - return nil, fmt.Errorf("unable to settle hodl invoice: " + + return fmt.Errorf("unable to settle hodl invoice: " + "preimage is nil") } - // TODO(positiveblue): create a invoice.CanSettleHodlInvoice func. newState, err := getUpdatedInvoiceState( invoice, hash, *update, ) if err != nil { - return nil, err + return err } if newState == nil || *newState != invpkg.ContractSettled { - return nil, fmt.Errorf("unable to settle hodl invoice: "+ + return fmt.Errorf("unable to settle hodl invoice: "+ "new computed state is not settled: %s", newState) } + err = updater.UpdateInvoiceState( + invpkg.ContractSettled, update.Preimage, + ) + if err != nil { + return err + } + invoice.State = invpkg.ContractSettled invoice.Terms.PaymentPreimage = update.Preimage - timestamp := d.clock.Now() // TODO(positiveblue): this logic can be further simplified. var amtPaid lnwire.MilliSatoshi - for _, htlc := range invoice.Htlcs { + for key, htlc := range invoice.Htlcs { settled, _, err := getUpdatedHtlcState( htlc, invpkg.ContractSettled, nil, ) if err != nil { - return nil, err + return err } if settled { - htlc.State = invpkg.HtlcStateSettled - htlc.ResolveTime = timestamp + err = resolveHtlc( + key, htlc, invpkg.HtlcStateSettled, updateTime, + updater, + ) + if err != nil { + return err + } + amtPaid += htlc.Amt } } - invoice.AmtPaid = amtPaid - - err = d.settleHodlInvoiceStoreUpdate( - invoices, settleIndex, invoiceNum, invoice, timestamp, - ) - if err != nil { - return nil, err - } - - return invoice, nil -} - -// settleHodlInvoiceStoreUpdate is a helper function used to store the settled -// hodl invoice update. -func (d *DB) settleHodlInvoiceStoreUpdate(invoices, settleIndex kvdb.RwBucket, - invoiceNum []byte, invoice *invpkg.Invoice, timestamp time.Time) error { - - err := setSettleMetaFields( - settleIndex, invoiceNum, invoice, timestamp, nil, - ) - if err != nil { - return err - } - - err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) - if err != nil { - return err - } - - return nil + return updateInvoiceAmtPaid(invoice, amtPaid, updater) } // cancelInvoice attempts to cancel the given invoice. That includes changing // the invoice state and the state of any relevant HTLC. -func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice, hash *lntypes.Hash, - update *invpkg.InvoiceStateUpdateDesc) (*invpkg.Invoice, error) { +func cancelInvoice(invoice *invpkg.Invoice, hash *lntypes.Hash, + updateTime time.Time, update *invpkg.InvoiceStateUpdateDesc, + updater invpkg.InvoiceUpdater) error { switch { case update == nil: fallthrough case update.NewState != invpkg.ContractCanceled: - return nil, fmt.Errorf("unable to cancel invoice: "+ + return fmt.Errorf("unable to cancel invoice: "+ "InvoiceUpdateDesc.State not valid: %v", update) } @@ -2429,47 +2589,41 @@ func (d *DB) cancelInvoice(invoices kvdb.RwBucket, invoiceNum []byte, newState, err := getUpdatedInvoiceState(invoice, hash, *update) if err != nil { - return nil, err + return err } if newState == nil || *newState != invpkg.ContractCanceled { - return nil, fmt.Errorf("unable to cancel invoice(%v): new "+ + return fmt.Errorf("unable to cancel invoice(%v): new "+ "computed state is not canceled: %s", invoice.AddIndex, newState) } + err = updater.UpdateInvoiceState(invpkg.ContractCanceled, nil) + if err != nil { + return err + } invoice.State = invpkg.ContractCanceled - timestamp := d.clock.Now() - // TODO(positiveblue): this logic can be simplified. - for _, htlc := range invoice.Htlcs { + for key, htlc := range invoice.Htlcs { canceled, _, err := getUpdatedHtlcState( htlc, invpkg.ContractCanceled, setID, ) if err != nil { - return nil, err + return err } if canceled { - htlc.State = invpkg.HtlcStateCanceled - htlc.ResolveTime = timestamp + err = resolveHtlc( + key, htlc, invpkg.HtlcStateCanceled, updateTime, + updater, + ) + if err != nil { + return err + } } } - err = d.cancelInvoiceStoreUpdate(invoices, invoiceNum, invoice) - if err != nil { - return nil, err - } - - return invoice, nil -} - -// cancelInvoiceStoreUpdate is a helper function used to store the canceled -// invoice update. -func (d *DB) cancelInvoiceStoreUpdate(invoices kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice) error { - - return d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) + return nil } // getUpdatedInvoiceState validates and processes an invoice state update. The @@ -2724,59 +2878,6 @@ func getUpdatedHtlcState(htlc *invpkg.InvoiceHTLC, } } -// setSettleMetaFields updates the metadata associated with settlement of an -// invoice. If a non-nil setID is passed in, then the value will be append to -// the invoice number as well, in order to allow us to detect repeated payments -// to the same AMP invoices "across time". -func setSettleMetaFields(settleIndex kvdb.RwBucket, invoiceNum []byte, - invoice *invpkg.Invoice, now time.Time, setID *invpkg.SetID) error { - - // Now that we know the invoice hasn't already been settled, we'll - // update the settle index so we can place this settle event in the - // proper location within our time series. - nextSettleSeqNo, err := settleIndex.NextSequence() - if err != nil { - return err - } - - // Make a new byte array on the stack that can potentially store the 4 - // byte invoice number along w/ the 32 byte set ID. We capture valueLen - // here which is the number of bytes copied so we can only store the 4 - // bytes if this is a non-AMP invoice. - var indexKey [invoiceSetIDKeyLen]byte - valueLen := copy(indexKey[:], invoiceNum) - - if setID != nil { - valueLen += copy(indexKey[valueLen:], setID[:]) - } - - var seqNoBytes [8]byte - byteOrder.PutUint64(seqNoBytes[:], nextSettleSeqNo) - err = settleIndex.Put(seqNoBytes[:], indexKey[:valueLen]) - if err != nil { - return err - } - - // If the setID is nil, then this means that this is a non-AMP settle, - // so we'll update the invoice settle index directly. - if setID == nil { - invoice.SettleDate = now - invoice.SettleIndex = nextSettleSeqNo - } else { - // If the set ID isn't blank, we'll update the AMP state map - // which tracks when each of the setIDs associated with a given - // AMP invoice are settled. - ampState := invoice.AMPState[*setID] - - ampState.SettleDate = now - ampState.SettleIndex = nextSettleSeqNo - - invoice.AMPState[*setID] = ampState - } - - return nil -} - // delAMPInvoices attempts to delete all the "sub" invoices associated with a // greater AMP invoices. We do this by deleting the set of keys that share the // invoice number as a prefix. diff --git a/invoices/interface.go b/invoices/interface.go index 73274dfb5f..490db1be56 100644 --- a/invoices/interface.go +++ b/invoices/interface.go @@ -2,9 +2,11 @@ package invoices import ( "context" + "time" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" ) @@ -162,3 +164,37 @@ type InvoiceSlice struct { // CircuitKey is a tuple of channel ID and HTLC ID, used to uniquely identify // HTLCs in a circuit. type CircuitKey = models.CircuitKey + +// InvoiceUpdater is an interface to abstract away the details of updating an +// invoice in the database. The methods of this interface are called during the +// in-memory update of an invoice when the database needs to be updated or the +// updated state needs to be marked as needing to be written to the database. +type InvoiceUpdater interface { + // AddHtlc adds a new htlc to the invoice. + AddHtlc(circuitKey CircuitKey, newHtlc *InvoiceHTLC) error + + // ResolveHtlc marks an htlc as resolved with the given state. + ResolveHtlc(circuitKey CircuitKey, state HtlcState, + resolveTime time.Time) error + + // AddAmpHtlcPreimage adds a preimage of an AMP htlc to the AMP invoice + // identified by the setID. + AddAmpHtlcPreimage(setID [32]byte, circuitKey CircuitKey, + preimage lntypes.Preimage) error + + // UpdateInvoiceState updates the invoice state to the new state. + UpdateInvoiceState(newState ContractState, + preimage *lntypes.Preimage) error + + // UpdateInvoiceAmtPaid updates the invoice amount paid to the new + // amount. + UpdateInvoiceAmtPaid(amtPaid lnwire.MilliSatoshi) error + + // UpdateAmpState updates the state of the AMP invoice identified by + // the setID. + UpdateAmpState(setID [32]byte, newState InvoiceStateAMP, + circuitKey models.CircuitKey) error + + // Finalize finalizes the update before it is written to the database. + Finalize(updateType UpdateType) error +} From 6b0931af82e7f3539bf892b0a2a37f4e64fb4e93 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 19 Feb 2024 20:22:54 +0100 Subject: [PATCH 10/15] invoices: move UpdateInvoice implementation to the invoices package With the introducation of the `InvoiceUpdater` interface we are now able to move the non-kv parts of `UpdateInvoice` completely under the invoices package. This is a preprequisite for being able to use the same code-base for the sql InvoiceDB implementation of UpdateInvoice. --- channeldb/invoice_test.go | 676 -------------------------- channeldb/invoices.go | 816 +------------------------------ invoices/update_invoice.go | 824 ++++++++++++++++++++++++++++++++ invoices/update_invoice_test.go | 687 ++++++++++++++++++++++++++ 4 files changed, 1512 insertions(+), 1491 deletions(-) create mode 100644 invoices/update_invoice.go create mode 100644 invoices/update_invoice_test.go diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index d6d1eaf7ae..d0d0518a06 100644 --- a/channeldb/invoice_test.go +++ b/channeldb/invoice_test.go @@ -2333,682 +2333,6 @@ func testUpdateHTLCPreimages(t *testing.T, test updateHTLCPreimageTestCase) { require.Equal(t, test.expError, err) } -type updateHTLCTest struct { - name string - input invpkg.InvoiceHTLC - invState invpkg.ContractState - setID *[32]byte - output invpkg.InvoiceHTLC - expErr error -} - -// TestUpdateHTLC asserts the behavior of the updateHTLC method in various -// scenarios for MPP and AMP. -func TestUpdateHTLC(t *testing.T) { - t.Parallel() - - setID := [32]byte{0x01} - ampRecord := record.NewAMP([32]byte{0x02}, setID, 3) - preimage := lntypes.Preimage{0x04} - hash := preimage.Hash() - - diffSetID := [32]byte{0x05} - fakePreimage := lntypes.Preimage{0x06} - testAlreadyNow := time.Now() - - tests := []updateHTLCTest{ - { - name: "MPP accept", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: nil, - }, - invState: invpkg.ContractAccepted, - setID: nil, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: nil, - }, - expErr: nil, - }, - { - name: "MPP settle", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: nil, - }, - invState: invpkg.ContractSettled, - setID: nil, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testNow, - Expiry: 40, - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - AMP: nil, - }, - expErr: nil, - }, - { - name: "MPP cancel", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: nil, - }, - invState: invpkg.ContractCanceled, - setID: nil, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testNow, - Expiry: 40, - State: invpkg.HtlcStateCanceled, - CustomRecords: make(record.CustomSet), - AMP: nil, - }, - expErr: nil, - }, - { - name: "AMP accept missing preimage", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: nil, - }, - }, - invState: invpkg.ContractAccepted, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: nil, - }, - }, - expErr: invpkg.ErrHTLCPreimageMissing, - }, - { - name: "AMP accept invalid preimage", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &fakePreimage, - }, - }, - invState: invpkg.ContractAccepted, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &fakePreimage, - }, - }, - expErr: invpkg.ErrHTLCPreimageMismatch, - }, - { - name: "AMP accept valid preimage", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractAccepted, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: nil, - }, - { - name: "AMP accept valid preimage different htlc set", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractAccepted, - setID: &diffSetID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: nil, - }, - { - name: "AMP settle missing preimage", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: nil, - }, - }, - invState: invpkg.ContractSettled, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: nil, - }, - }, - expErr: invpkg.ErrHTLCPreimageMissing, - }, - { - name: "AMP settle invalid preimage", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &fakePreimage, - }, - }, - invState: invpkg.ContractSettled, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &fakePreimage, - }, - }, - expErr: invpkg.ErrHTLCPreimageMismatch, - }, - { - name: "AMP settle valid preimage", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractSettled, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testNow, - Expiry: 40, - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: nil, - }, - { - // With the newer AMP logic, this is now valid, as we - // want to be able to accept multiple settle attempts - // to a given pay_addr. In this case, the HTLC should - // remain in the accepted state. - name: "AMP settle valid preimage different htlc set", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractSettled, - setID: &diffSetID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: nil, - }, - { - name: "accept invoice htlc already settled", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractAccepted, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: invpkg.ErrHTLCAlreadySettled, - }, - { - name: "cancel invoice htlc already settled", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractCanceled, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: invpkg.ErrHTLCAlreadySettled, - }, - { - name: "settle invoice htlc already settled", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractSettled, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: nil, - }, - { - name: "cancel invoice", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: time.Time{}, - Expiry: 40, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractCanceled, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testNow, - Expiry: 40, - State: invpkg.HtlcStateCanceled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: nil, - }, - { - name: "accept invoice htlc already canceled", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateCanceled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractAccepted, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateCanceled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: nil, - }, - { - name: "cancel invoice htlc already canceled", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateCanceled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractCanceled, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateCanceled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: nil, - }, - { - name: "settle invoice htlc already canceled", - input: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateCanceled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - invState: invpkg.ContractSettled, - setID: &setID, - output: invpkg.InvoiceHTLC{ - Amt: 5000, - MppTotalAmt: 5000, - AcceptHeight: 100, - AcceptTime: testNow, - ResolveTime: testAlreadyNow, - Expiry: 40, - State: invpkg.HtlcStateCanceled, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *ampRecord, - Hash: hash, - Preimage: &preimage, - }, - }, - expErr: nil, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - testUpdateHTLC(t, test) - }) - } -} - -func testUpdateHTLC(t *testing.T, test updateHTLCTest) { - htlc := test.input.Copy() - stateChanged, state, err := getUpdatedHtlcState( - htlc, test.invState, test.setID, - ) - if stateChanged { - htlc.State = state - htlc.ResolveTime = testNow - } - - require.Equal(t, test.expErr, err) - require.Equal(t, test.output, *htlc) -} - // TestDeleteInvoices tests that deleting a list of invoices will succeed // if all delete references are valid, or will fail otherwise. func TestDeleteInvoices(t *testing.T) { diff --git a/channeldb/invoices.go b/channeldb/invoices.go index d134c03b40..a7663cabf6 100644 --- a/channeldb/invoices.go +++ b/channeldb/invoices.go @@ -673,7 +673,7 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef, } payHash := ref.PayHash() - updatedInvoice, err = updateInvoice( + updatedInvoice, err = invpkg.UpdateInvoice( payHash, updater.invoice, now, callback, updater, ) @@ -2064,820 +2064,6 @@ func makeInvoiceSetIDKey(invoiceNum, setID []byte) [invoiceSetIDKeyLen]byte { return invoiceSetIDKey } -// updateHtlcsAmp takes an invoice, and a new HTLC to be added (along with its -// set ID), and updates the internal AMP state of an invoice, and also tallies -// the set of HTLCs to be updated on disk. -func acceptHtlcsAmp(invoice *invpkg.Invoice, setID invpkg.SetID, - circuitKey models.CircuitKey, htlc *invpkg.InvoiceHTLC, - updater invpkg.InvoiceUpdater) error { - - newAmpState, err := getUpdatedInvoiceAmpState( - invoice, setID, circuitKey, invpkg.HtlcStateAccepted, htlc.Amt, - ) - if err != nil { - return err - } - - invoice.AMPState[setID] = newAmpState - - // Mark the updates as needing to be written to disk. - return updater.UpdateAmpState(setID, newAmpState, circuitKey) -} - -// 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 -// 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. -func cancelHtlcsAmp(invoice *invpkg.Invoice, circuitKey models.CircuitKey, - htlc *invpkg.InvoiceHTLC, updater invpkg.InvoiceUpdater) error { - - setID := htlc.AMP.Record.SetID() - - // First, we'll update the state of the entire HTLC set - // to cancelled. - newAmpState, err := getUpdatedInvoiceAmpState( - invoice, setID, circuitKey, invpkg.HtlcStateCanceled, - htlc.Amt, - ) - if err != nil { - return err - } - - invoice.AMPState[setID] = newAmpState - - // Mark the updates as needing to be written to disk. - err = updater.UpdateAmpState(setID, newAmpState, circuitKey) - if err != nil { - return err - } - - // We'll only decrement the total amount paid if the invoice was - // already in the accepted state. - if invoice.AmtPaid != 0 { - return updateInvoiceAmtPaid( - invoice, invoice.AmtPaid-htlc.Amt, updater, - ) - } - - return nil -} - -// settleHtlcsAmp processes a new settle operation on an HTLC set for an AMP -// invoice. We'll update some meta data in the main invoice, and also signal -// that this HTLC set needs to be re-written back to disk. -func settleHtlcsAmp(invoice *invpkg.Invoice, circuitKey models.CircuitKey, - htlc *invpkg.InvoiceHTLC, updater invpkg.InvoiceUpdater) error { - - setID := htlc.AMP.Record.SetID() - - // Next update the main AMP meta-data to indicate that this HTLC set - // has been fully settled. - newAmpState, err := getUpdatedInvoiceAmpState( - invoice, setID, circuitKey, invpkg.HtlcStateSettled, 0, - ) - if err != nil { - return err - } - - invoice.AMPState[setID] = newAmpState - - // Mark the updates as needing to be written to disk. - return updater.UpdateAmpState(setID, newAmpState, circuitKey) -} - -// updateInvoice fetches the invoice, obtains the update descriptor from the -// callback and applies the updates in a single db transaction. -func updateInvoice(hash *lntypes.Hash, invoice *invpkg.Invoice, - updateTime time.Time, callback invpkg.InvoiceUpdateCallback, - updater invpkg.InvoiceUpdater) (*invpkg.Invoice, error) { - - // Create deep copy to prevent any accidental modification in the - // callback. - invoiceCopy, err := invpkg.CopyInvoice(invoice) - if err != nil { - return nil, err - } - - // Call the callback and obtain the update descriptor. - update, err := callback(invoiceCopy) - if err != nil { - return invoice, err - } - - // If there is nothing to update, return early. - if update == nil { - return invoice, nil - } - - switch update.UpdateType { - case invpkg.CancelHTLCsUpdate: - err := cancelHTLCs(invoice, updateTime, update, updater) - if err != nil { - return nil, err - } - - case invpkg.AddHTLCsUpdate: - err := addHTLCs(invoice, hash, updateTime, update, updater) - if err != nil { - return nil, err - } - - case invpkg.SettleHodlInvoiceUpdate: - err := settleHodlInvoice( - invoice, hash, updateTime, update.State, updater, - ) - if err != nil { - return nil, err - } - - case invpkg.CancelInvoiceUpdate: - err := cancelInvoice( - invoice, hash, updateTime, update.State, updater, - ) - if err != nil { - return nil, err - } - - default: - return nil, fmt.Errorf("unknown update type: %s", - update.UpdateType) - } - - if err := updater.Finalize(update.UpdateType); err != nil { - return nil, err - } - - return invoice, nil -} - -// cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc. -// -// NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the -// InvoiceUpdateDesc. -func cancelHTLCs(invoice *invpkg.Invoice, updateTime time.Time, - update *invpkg.InvoiceUpdateDesc, updater invpkg.InvoiceUpdater) error { - - for key := range update.CancelHtlcs { - htlc, exists := invoice.Htlcs[key] - - // Verify that we don't get an action for htlcs that are not - // present on the invoice. - if !exists { - return fmt.Errorf("cancel of non-existent htlc") - } - - err := canCancelSingleHtlc(htlc, invoice.State) - if err != nil { - return err - } - - err = resolveHtlc( - key, htlc, invpkg.HtlcStateCanceled, updateTime, - updater, - ) - if err != nil { - return err - } - - // Tally this into the set of HTLCs that need to be updated on - // disk, but once again, only if this is an AMP invoice. - if invoice.IsAMP() { - err := cancelHtlcsAmp(invoice, key, htlc, updater) - if err != nil { - return err - } - } - } - - return nil -} - -// addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc. -func addHTLCs(invoice *invpkg.Invoice, hash *lntypes.Hash, updateTime time.Time, - update *invpkg.InvoiceUpdateDesc, updater invpkg.InvoiceUpdater) error { - - var setID *[32]byte - invoiceIsAMP := invoice.IsAMP() - if invoiceIsAMP && update.State != nil { - setID = update.State.SetID - } - - for key, htlcUpdate := range update.AddHtlcs { - if _, exists := invoice.Htlcs[key]; exists { - return fmt.Errorf("duplicate add of htlc %v", key) - } - - // Force caller to supply htlc without custom records in a - // consistent way. - if htlcUpdate.CustomRecords == nil { - return errors.New("nil custom records map") - } - - htlc := &invpkg.InvoiceHTLC{ - Amt: htlcUpdate.Amt, - MppTotalAmt: htlcUpdate.MppTotalAmt, - Expiry: htlcUpdate.Expiry, - AcceptHeight: uint32(htlcUpdate.AcceptHeight), - AcceptTime: updateTime, - State: invpkg.HtlcStateAccepted, - CustomRecords: htlcUpdate.CustomRecords, - } - - if invoiceIsAMP { - if htlcUpdate.AMP == nil { - return fmt.Errorf("unable to add htlc "+ - "without AMP data to AMP invoice(%v)", - invoice.AddIndex) - } - - htlc.AMP = htlcUpdate.AMP.Copy() - } - - if err := updater.AddHtlc(key, htlc); err != nil { - return err - } - - invoice.Htlcs[key] = htlc - - // Collect the set of new HTLCs so we can write them properly - // below, but only if this is an AMP invoice. - if invoiceIsAMP { - err := acceptHtlcsAmp( - invoice, htlcUpdate.AMP.Record.SetID(), key, - htlc, updater, - ) - if err != nil { - return err - } - } - } - - // At this point, the set of accepted HTLCs should be fully - // populated with added HTLCs or removed of canceled ones. Update - // invoice state if the update descriptor indicates an invoice state - // change, which depends on having an accurate view of the accepted - // HTLCs. - if update.State != nil { - newState, err := getUpdatedInvoiceState( - invoice, hash, *update.State, - ) - if err != nil { - return err - } - - // If this isn't an AMP invoice, then we'll go ahead and update - // the invoice state directly here. For AMP invoices, we instead - // will keep the top-level invoice open, and update the state of - // each _htlc set_ instead. However, we'll allow the invoice to - // transition to the cancelled state regardless. - if !invoiceIsAMP || *newState == invpkg.ContractCanceled { - err := updater.UpdateInvoiceState(*newState, nil) - if err != nil { - return err - } - invoice.State = *newState - } - } - - // The set of HTLC pre-images will only be set if we were actually able - // to reconstruct all the AMP pre-images. - var settleEligibleAMP bool - if update.State != nil { - settleEligibleAMP = len(update.State.HTLCPreimages) != 0 - } - - // With any invoice level state transitions recorded, we'll now - // finalize the process by updating the state transitions for - // individual HTLCs - var amtPaid lnwire.MilliSatoshi - - for key, htlc := range invoice.Htlcs { - // Set the HTLC preimage for any AMP HTLCs. - if setID != nil && update.State != nil { - preimage, ok := update.State.HTLCPreimages[key] - switch { - // If we don't already have a preimage for this HTLC, we - // can set it now. - case ok && htlc.AMP.Preimage == nil: - err := updater.AddAmpHtlcPreimage( - htlc.AMP.Record.SetID(), key, preimage, - ) - if err != nil { - return err - } - htlc.AMP.Preimage = &preimage - - // Otherwise, prevent over-writing an existing - // preimage. Ignore the case where the preimage is - // identical. - case ok && *htlc.AMP.Preimage != preimage: - return invpkg.ErrHTLCPreimageAlreadyExists - } - } - - // The invoice state may have changed and this could have - // implications for the states of the individual htlcs. Align - // the htlc state with the current invoice state. - // - // If we have all the pre-images for an AMP invoice, then we'll - // act as if we're able to settle the entire invoice. We need - // to do this since it's possible for us to settle AMP invoices - // while the contract state (on disk) is still in the accept - // state. - htlcContextState := invoice.State - if settleEligibleAMP { - htlcContextState = invpkg.ContractSettled - } - htlcStateChanged, htlcState, err := getUpdatedHtlcState( - htlc, htlcContextState, setID, - ) - if err != nil { - return err - } - - if htlcStateChanged { - err = resolveHtlc( - key, htlc, htlcState, updateTime, updater, - ) - if err != nil { - return err - } - } - - htlcSettled := htlcStateChanged && - htlcState == invpkg.HtlcStateSettled - - // If the HTLC has being settled for the first time, and this - // is an AMP invoice, then we'll need to update some additional - // meta data state. - if htlcSettled && invoiceIsAMP { - err = settleHtlcsAmp(invoice, key, htlc, updater) - if err != nil { - return err - } - } - - accepted := htlc.State == invpkg.HtlcStateAccepted - settled := htlc.State == invpkg.HtlcStateSettled - invoiceStateReady := accepted || settled - - if !invoiceIsAMP { - // Update the running amount paid to this invoice. We - // don't include accepted htlcs when the invoice is - // still open. - if invoice.State != invpkg.ContractOpen && - invoiceStateReady { - - amtPaid += htlc.Amt - } - } else { - // For AMP invoices, since we won't always be reading - // out the total invoice set each time, we'll instead - // accumulate newly added invoices to the total amount - // paid. - if _, ok := update.AddHtlcs[key]; !ok { - continue - } - - // Update the running amount paid to this invoice. AMP - // invoices never go to the settled state, so if it's - // open, then we tally the HTLC. - if invoice.State == invpkg.ContractOpen && - invoiceStateReady { - - amtPaid += htlc.Amt - } - } - } - - // For non-AMP invoices we recalculate the amount paid from scratch - // each time, while for AMP invoices, we'll accumulate only based on - // newly added HTLCs. - if invoiceIsAMP { - amtPaid += invoice.AmtPaid - } - - return updateInvoiceAmtPaid(invoice, amtPaid, updater) -} - -func resolveHtlc(circuitKey models.CircuitKey, htlc *invpkg.InvoiceHTLC, - state invpkg.HtlcState, resolveTime time.Time, - updater invpkg.InvoiceUpdater) error { - - err := updater.ResolveHtlc(circuitKey, state, resolveTime) - if err != nil { - return err - } - htlc.State = state - htlc.ResolveTime = resolveTime - - return nil -} - -func updateInvoiceAmtPaid(invoice *invpkg.Invoice, amt lnwire.MilliSatoshi, - updater invpkg.InvoiceUpdater) error { - - err := updater.UpdateInvoiceAmtPaid(amt) - if err != nil { - return err - } - invoice.AmtPaid = amt - - return nil -} - -// settleHodlInvoice marks a hodl invoice as settled. -// -// NOTE: Currently it is not possible to have HODL AMP invoices. - -func settleHodlInvoice(invoice *invpkg.Invoice, hash *lntypes.Hash, - updateTime time.Time, update *invpkg.InvoiceStateUpdateDesc, - updater invpkg.InvoiceUpdater) error { - - if !invoice.HodlInvoice { - return fmt.Errorf("unable to settle hodl invoice: %v is "+ - "not a hodl invoice", invoice.AddIndex) - } - - // TODO(positiveblue): because NewState can only be ContractSettled we - // can remove it from the API and set it here directly. - switch { - case update == nil: - fallthrough - - case update.NewState != invpkg.ContractSettled: - return fmt.Errorf("unable to settle hodl invoice: "+ - "not valid InvoiceUpdateDesc.State: %v", update) - - case update.Preimage == nil: - return fmt.Errorf("unable to settle hodl invoice: " + - "preimage is nil") - } - - newState, err := getUpdatedInvoiceState( - invoice, hash, *update, - ) - if err != nil { - return err - } - - if newState == nil || *newState != invpkg.ContractSettled { - return fmt.Errorf("unable to settle hodl invoice: "+ - "new computed state is not settled: %s", newState) - } - - err = updater.UpdateInvoiceState( - invpkg.ContractSettled, update.Preimage, - ) - if err != nil { - return err - } - - invoice.State = invpkg.ContractSettled - invoice.Terms.PaymentPreimage = update.Preimage - - // TODO(positiveblue): this logic can be further simplified. - var amtPaid lnwire.MilliSatoshi - for key, htlc := range invoice.Htlcs { - settled, _, err := getUpdatedHtlcState( - htlc, invpkg.ContractSettled, nil, - ) - if err != nil { - return err - } - - if settled { - err = resolveHtlc( - key, htlc, invpkg.HtlcStateSettled, updateTime, - updater, - ) - if err != nil { - return err - } - - amtPaid += htlc.Amt - } - } - - return updateInvoiceAmtPaid(invoice, amtPaid, updater) -} - -// cancelInvoice attempts to cancel the given invoice. That includes changing -// the invoice state and the state of any relevant HTLC. -func cancelInvoice(invoice *invpkg.Invoice, hash *lntypes.Hash, - updateTime time.Time, update *invpkg.InvoiceStateUpdateDesc, - updater invpkg.InvoiceUpdater) error { - - switch { - case update == nil: - fallthrough - - case update.NewState != invpkg.ContractCanceled: - return fmt.Errorf("unable to cancel invoice: "+ - "InvoiceUpdateDesc.State not valid: %v", update) - } - - var ( - setID *[32]byte - invoiceIsAMP bool - ) - - invoiceIsAMP = invoice.IsAMP() - if invoiceIsAMP { - setID = update.SetID - } - - newState, err := getUpdatedInvoiceState(invoice, hash, *update) - if err != nil { - return err - } - - if newState == nil || *newState != invpkg.ContractCanceled { - return fmt.Errorf("unable to cancel invoice(%v): new "+ - "computed state is not canceled: %s", invoice.AddIndex, - newState) - } - - err = updater.UpdateInvoiceState(invpkg.ContractCanceled, nil) - if err != nil { - return err - } - invoice.State = invpkg.ContractCanceled - - for key, htlc := range invoice.Htlcs { - canceled, _, err := getUpdatedHtlcState( - htlc, invpkg.ContractCanceled, setID, - ) - if err != nil { - return err - } - - if canceled { - err = resolveHtlc( - key, htlc, invpkg.HtlcStateCanceled, updateTime, - updater, - ) - if err != nil { - return err - } - } - } - - return nil -} - -// getUpdatedInvoiceState validates and processes an invoice state update. The -// new state to transition to is returned, so the caller is able to select -// exactly how the invoice state is updated. Note that for AMP invoices this -// function is only used to validate the state transition if we're cancelling -// the invoice. -func getUpdatedInvoiceState(invoice *invpkg.Invoice, hash *lntypes.Hash, - update invpkg.InvoiceStateUpdateDesc) (*invpkg.ContractState, error) { - - // Returning to open is never allowed from any state. - if update.NewState == invpkg.ContractOpen { - return nil, invpkg.ErrInvoiceCannotOpen - } - - switch invoice.State { - // Once a contract is accepted, we can only transition to settled or - // canceled. Forbid transitioning back into this state. Otherwise this - // state is identical to ContractOpen, so we fallthrough to apply the - // same checks that we apply to open invoices. - case invpkg.ContractAccepted: - if update.NewState == invpkg.ContractAccepted { - return nil, invpkg.ErrInvoiceCannotAccept - } - - fallthrough - - // If a contract is open, permit a state transition to accepted, settled - // or canceled. The only restriction is on transitioning to settled - // where we ensure the preimage is valid. - case invpkg.ContractOpen: - if update.NewState == invpkg.ContractCanceled { - return &update.NewState, nil - } - - // Sanity check that the user isn't trying to settle or accept a - // non-existent HTLC set. - set := invoice.HTLCSet(update.SetID, invpkg.HtlcStateAccepted) - if len(set) == 0 { - return nil, invpkg.ErrEmptyHTLCSet - } - - // For AMP invoices, there are no invoice-level preimage checks. - // However, we still sanity check that we aren't trying to - // settle an AMP invoice with a preimage. - if update.SetID != nil { - if update.Preimage != nil { - return nil, errors.New("AMP set cannot have " + - "preimage") - } - - return &update.NewState, nil - } - - switch { - // If an invoice-level preimage was supplied, but the InvoiceRef - // doesn't specify a hash (e.g. AMP invoices) we fail. - case update.Preimage != nil && hash == nil: - return nil, invpkg.ErrUnexpectedInvoicePreimage - - // Validate the supplied preimage for non-AMP invoices. - case update.Preimage != nil: - if update.Preimage.Hash() != *hash { - return nil, invpkg.ErrInvoicePreimageMismatch - } - - // Permit non-AMP invoices to be accepted without knowing the - // preimage. When trying to settle we'll have to pass through - // the above check in order to not hit the one below. - case update.NewState == invpkg.ContractAccepted: - - // Fail if we still don't have a preimage when transitioning to - // settle the non-AMP invoice. - case update.NewState == invpkg.ContractSettled && - invoice.Terms.PaymentPreimage == nil: - - return nil, errors.New("unknown preimage") - } - - return &update.NewState, nil - - // Once settled, we are in a terminal state. - case invpkg.ContractSettled: - return nil, invpkg.ErrInvoiceAlreadySettled - - // Once canceled, we are in a terminal state. - case invpkg.ContractCanceled: - return nil, invpkg.ErrInvoiceAlreadyCanceled - - default: - return nil, errors.New("unknown state transition") - } -} - -// getUpdatedInvoiceAmpState returns the AMP state of an invoice (without -// applying it), given the new state, and the amount of the HTLC that is -// being updated. -func getUpdatedInvoiceAmpState(invoice *invpkg.Invoice, setID invpkg.SetID, - circuitKey models.CircuitKey, state invpkg.HtlcState, - amt lnwire.MilliSatoshi) (invpkg.InvoiceStateAMP, error) { - - // Retrieve the AMP state for this set ID. - ampState, ok := invoice.AMPState[setID] - - // If the state is accepted then we may need to create a new entry for - // this set ID, otherwise we expect that the entry already exists and - // we can update it. - if !ok && state != invpkg.HtlcStateAccepted { - return invpkg.InvoiceStateAMP{}, - fmt.Errorf("unable to update AMP state for setID=%x ", - setID) - } - - switch state { - case invpkg.HtlcStateAccepted: - if !ok { - // If an entry for this set ID doesn't already exist, - // then we'll need to create it. - ampState = invpkg.InvoiceStateAMP{ - State: invpkg.HtlcStateAccepted, - InvoiceKeys: make( - map[models.CircuitKey]struct{}, - ), - } - } - - ampState.AmtPaid += amt - - case invpkg.HtlcStateCanceled: - ampState.State = invpkg.HtlcStateCanceled - ampState.AmtPaid -= amt - - case invpkg.HtlcStateSettled: - ampState.State = invpkg.HtlcStateSettled - } - - ampState.InvoiceKeys[circuitKey] = struct{}{} - - return ampState, nil -} - -// canCancelSingleHtlc validates cancellation of a single HTLC. If nil is -// returned, then the HTLC can be cancelled. -func canCancelSingleHtlc(htlc *invpkg.InvoiceHTLC, - invoiceState invpkg.ContractState) error { - - // It is only possible to cancel individual htlcs on an open invoice. - if invoiceState != invpkg.ContractOpen { - return fmt.Errorf("htlc canceled on invoice in state %v", - invoiceState) - } - - // It is only possible if the htlc is still pending. - if htlc.State != invpkg.HtlcStateAccepted { - return fmt.Errorf("htlc canceled in state %v", htlc.State) - } - - return nil -} - -// getUpdatedHtlcState aligns the state of an htlc with the given invoice state. -// A boolean indicating whether the HTLCs state need to be updated, along with -// the new state (or old state if no change is needed) is returned. -func getUpdatedHtlcState(htlc *invpkg.InvoiceHTLC, - invoiceState invpkg.ContractState, setID *[32]byte) ( - bool, invpkg.HtlcState, error) { - - trySettle := func(persist bool) (bool, invpkg.HtlcState, error) { - if htlc.State != invpkg.HtlcStateAccepted { - return false, htlc.State, nil - } - - // Settle the HTLC if it matches the settled set id. If - // there're other HTLCs with distinct setIDs, then we'll leave - // them, as they may eventually be settled as we permit - // multiple settles to a single pay_addr for AMP. - settled := false - if htlc.IsInHTLCSet(setID) { - // Non-AMP HTLCs can be settled immediately since we - // already know the preimage is valid due to checks at - // the invoice level. For AMP HTLCs, verify that the - // per-HTLC preimage-hash pair is valid. - switch { - // Non-AMP HTLCs can be settle immediately since we - // already know the preimage is valid due to checks at - // the invoice level. - case setID == nil: - - // At this point, the setID is non-nil, meaning this is - // an AMP HTLC. We know that htlc.AMP cannot be nil, - // otherwise IsInHTLCSet would have returned false. - // - // Fail if an accepted AMP HTLC has no preimage. - case htlc.AMP.Preimage == nil: - return false, htlc.State, - invpkg.ErrHTLCPreimageMissing - - // Fail if the accepted AMP HTLC has an invalid - // preimage. - case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash): - return false, htlc.State, - invpkg.ErrHTLCPreimageMismatch - } - - settled = true - } - - // Only persist the changes if the invoice is moving to the - // settled state, and we're actually updating the state to - // settled. - newState := htlc.State - if settled { - newState = invpkg.HtlcStateSettled - } - - return persist && settled, newState, nil - } - - if invoiceState == invpkg.ContractSettled { - // Check that we can settle the HTLCs. For legacy and MPP HTLCs - // this will be a NOP, but for AMP HTLCs this asserts that we - // have a valid hash/preimage pair. Passing true permits the - // method to update the HTLC to HtlcStateSettled. - return trySettle(true) - } - - // We should never find a settled HTLC on an invoice that isn't in - // ContractSettled. - if htlc.State == invpkg.HtlcStateSettled { - return false, htlc.State, invpkg.ErrHTLCAlreadySettled - } - - switch invoiceState { - case invpkg.ContractCanceled: - htlcAlreadyCanceled := htlc.State == invpkg.HtlcStateCanceled - return !htlcAlreadyCanceled, invpkg.HtlcStateCanceled, nil - - // TODO(roasbeef): never fully passed thru now? - case invpkg.ContractAccepted: - // Check that we can settle the HTLCs. For legacy and MPP HTLCs - // this will be a NOP, but for AMP HTLCs this asserts that we - // have a valid hash/preimage pair. Passing false prevents the - // method from putting the HTLC in HtlcStateSettled, leaving it - // in HtlcStateAccepted. - return trySettle(false) - - case invpkg.ContractOpen: - return false, htlc.State, nil - - default: - return false, htlc.State, errors.New("unknown state transition") - } -} - // delAMPInvoices attempts to delete all the "sub" invoices associated with a // greater AMP invoices. We do this by deleting the set of keys that share the // invoice number as a prefix. diff --git a/invoices/update_invoice.go b/invoices/update_invoice.go new file mode 100644 index 0000000000..dc81d9db0f --- /dev/null +++ b/invoices/update_invoice.go @@ -0,0 +1,824 @@ +package invoices + +import ( + "errors" + "fmt" + "time" + + "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" +) + +// updateHtlcsAmp takes an invoice, and a new HTLC to be added (along with its +// set ID), and updates the internal AMP state of an invoice, and also tallies +// the set of HTLCs to be updated on disk. +func acceptHtlcsAmp(invoice *Invoice, setID SetID, + circuitKey models.CircuitKey, htlc *InvoiceHTLC, + updater InvoiceUpdater) error { + + newAmpState, err := getUpdatedInvoiceAmpState( + invoice, setID, circuitKey, HtlcStateAccepted, htlc.Amt, + ) + if err != nil { + return err + } + + invoice.AMPState[setID] = newAmpState + + // Mark the updates as needing to be written to disk. + return updater.UpdateAmpState(setID, newAmpState, circuitKey) +} + +// 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 +// 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. +func cancelHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey, + htlc *InvoiceHTLC, updater InvoiceUpdater) error { + + setID := htlc.AMP.Record.SetID() + + // First, we'll update the state of the entire HTLC set + // to cancelled. + newAmpState, err := getUpdatedInvoiceAmpState( + invoice, setID, circuitKey, HtlcStateCanceled, + htlc.Amt, + ) + if err != nil { + return err + } + + invoice.AMPState[setID] = newAmpState + + // Mark the updates as needing to be written to disk. + err = updater.UpdateAmpState(setID, newAmpState, circuitKey) + if err != nil { + return err + } + + // We'll only decrement the total amount paid if the invoice was + // already in the accepted state. + if invoice.AmtPaid != 0 { + return updateInvoiceAmtPaid( + invoice, invoice.AmtPaid-htlc.Amt, updater, + ) + } + + return nil +} + +// settleHtlcsAmp processes a new settle operation on an HTLC set for an AMP +// invoice. We'll update some meta data in the main invoice, and also signal +// that this HTLC set needs to be re-written back to disk. +func settleHtlcsAmp(invoice *Invoice, circuitKey models.CircuitKey, + htlc *InvoiceHTLC, updater InvoiceUpdater) error { + + setID := htlc.AMP.Record.SetID() + + // Next update the main AMP meta-data to indicate that this HTLC set + // has been fully settled. + newAmpState, err := getUpdatedInvoiceAmpState( + invoice, setID, circuitKey, HtlcStateSettled, 0, + ) + if err != nil { + return err + } + + invoice.AMPState[setID] = newAmpState + + // Mark the updates as needing to be written to disk. + return updater.UpdateAmpState(setID, newAmpState, circuitKey) +} + +// UpdateInvoice fetches the invoice, obtains the update descriptor from the +// callback and applies the updates in a single db transaction. +func UpdateInvoice(hash *lntypes.Hash, invoice *Invoice, + updateTime time.Time, callback InvoiceUpdateCallback, + updater InvoiceUpdater) (*Invoice, error) { + + // Create deep copy to prevent any accidental modification in the + // callback. + invoiceCopy, err := CopyInvoice(invoice) + if err != nil { + return nil, err + } + + // Call the callback and obtain the update descriptor. + update, err := callback(invoiceCopy) + if err != nil { + return invoice, err + } + + // If there is nothing to update, return early. + if update == nil { + return invoice, nil + } + + switch update.UpdateType { + case CancelHTLCsUpdate: + err := cancelHTLCs(invoice, updateTime, update, updater) + if err != nil { + return nil, err + } + + case AddHTLCsUpdate: + err := addHTLCs(invoice, hash, updateTime, update, updater) + if err != nil { + return nil, err + } + + case SettleHodlInvoiceUpdate: + err := settleHodlInvoice( + invoice, hash, updateTime, update.State, updater, + ) + if err != nil { + return nil, err + } + + case CancelInvoiceUpdate: + err := cancelInvoice( + invoice, hash, updateTime, update.State, updater, + ) + if err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unknown update type: %s", + update.UpdateType) + } + + if err := updater.Finalize(update.UpdateType); err != nil { + return nil, err + } + + return invoice, nil +} + +// cancelHTLCs tries to cancel the htlcs in the given InvoiceUpdateDesc. +// +// NOTE: cancelHTLCs updates will only use the `CancelHtlcs` field in the +// InvoiceUpdateDesc. +func cancelHTLCs(invoice *Invoice, updateTime time.Time, + update *InvoiceUpdateDesc, updater InvoiceUpdater) error { + + for key := range update.CancelHtlcs { + htlc, exists := invoice.Htlcs[key] + + // Verify that we don't get an action for htlcs that are not + // present on the invoice. + if !exists { + return fmt.Errorf("cancel of non-existent htlc") + } + + err := canCancelSingleHtlc(htlc, invoice.State) + if err != nil { + return err + } + + err = resolveHtlc( + key, htlc, HtlcStateCanceled, updateTime, + updater, + ) + if err != nil { + return err + } + + // Tally this into the set of HTLCs that need to be updated on + // disk, but once again, only if this is an AMP invoice. + if invoice.IsAMP() { + err := cancelHtlcsAmp(invoice, key, htlc, updater) + if err != nil { + return err + } + } + } + + return nil +} + +// addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc. +func addHTLCs(invoice *Invoice, hash *lntypes.Hash, updateTime time.Time, + update *InvoiceUpdateDesc, updater InvoiceUpdater) error { + + var setID *[32]byte + invoiceIsAMP := invoice.IsAMP() + if invoiceIsAMP && update.State != nil { + setID = update.State.SetID + } + + for key, htlcUpdate := range update.AddHtlcs { + if _, exists := invoice.Htlcs[key]; exists { + return fmt.Errorf("duplicate add of htlc %v", key) + } + + // Force caller to supply htlc without custom records in a + // consistent way. + if htlcUpdate.CustomRecords == nil { + return errors.New("nil custom records map") + } + + htlc := &InvoiceHTLC{ + Amt: htlcUpdate.Amt, + MppTotalAmt: htlcUpdate.MppTotalAmt, + Expiry: htlcUpdate.Expiry, + AcceptHeight: uint32(htlcUpdate.AcceptHeight), + AcceptTime: updateTime, + State: HtlcStateAccepted, + CustomRecords: htlcUpdate.CustomRecords, + } + + if invoiceIsAMP { + if htlcUpdate.AMP == nil { + return fmt.Errorf("unable to add htlc "+ + "without AMP data to AMP invoice(%v)", + invoice.AddIndex) + } + + htlc.AMP = htlcUpdate.AMP.Copy() + } + + if err := updater.AddHtlc(key, htlc); err != nil { + return err + } + + invoice.Htlcs[key] = htlc + + // Collect the set of new HTLCs so we can write them properly + // below, but only if this is an AMP invoice. + if invoiceIsAMP { + err := acceptHtlcsAmp( + invoice, htlcUpdate.AMP.Record.SetID(), key, + htlc, updater, + ) + if err != nil { + return err + } + } + } + + // At this point, the set of accepted HTLCs should be fully + // populated with added HTLCs or removed of canceled ones. Update + // invoice state if the update descriptor indicates an invoice state + // change, which depends on having an accurate view of the accepted + // HTLCs. + if update.State != nil { + newState, err := getUpdatedInvoiceState( + invoice, hash, *update.State, + ) + if err != nil { + return err + } + + // If this isn't an AMP invoice, then we'll go ahead and update + // the invoice state directly here. For AMP invoices, we instead + // will keep the top-level invoice open, and update the state of + // each _htlc set_ instead. However, we'll allow the invoice to + // transition to the cancelled state regardless. + if !invoiceIsAMP || *newState == ContractCanceled { + err := updater.UpdateInvoiceState(*newState, nil) + if err != nil { + return err + } + invoice.State = *newState + } + } + + // The set of HTLC pre-images will only be set if we were actually able + // to reconstruct all the AMP pre-images. + var settleEligibleAMP bool + if update.State != nil { + settleEligibleAMP = len(update.State.HTLCPreimages) != 0 + } + + // With any invoice level state transitions recorded, we'll now + // finalize the process by updating the state transitions for + // individual HTLCs + var amtPaid lnwire.MilliSatoshi + + for key, htlc := range invoice.Htlcs { + // Set the HTLC preimage for any AMP HTLCs. + if setID != nil && update.State != nil { + preimage, ok := update.State.HTLCPreimages[key] + switch { + // If we don't already have a preimage for this HTLC, we + // can set it now. + case ok && htlc.AMP.Preimage == nil: + err := updater.AddAmpHtlcPreimage( + htlc.AMP.Record.SetID(), key, preimage, + ) + if err != nil { + return err + } + htlc.AMP.Preimage = &preimage + + // Otherwise, prevent over-writing an existing + // preimage. Ignore the case where the preimage is + // identical. + case ok && *htlc.AMP.Preimage != preimage: + return ErrHTLCPreimageAlreadyExists + } + } + + // The invoice state may have changed and this could have + // implications for the states of the individual htlcs. Align + // the htlc state with the current invoice state. + // + // If we have all the pre-images for an AMP invoice, then we'll + // act as if we're able to settle the entire invoice. We need + // to do this since it's possible for us to settle AMP invoices + // while the contract state (on disk) is still in the accept + // state. + htlcContextState := invoice.State + if settleEligibleAMP { + htlcContextState = ContractSettled + } + htlcStateChanged, htlcState, err := getUpdatedHtlcState( + htlc, htlcContextState, setID, + ) + if err != nil { + return err + } + + if htlcStateChanged { + err = resolveHtlc( + key, htlc, htlcState, updateTime, updater, + ) + if err != nil { + return err + } + } + + htlcSettled := htlcStateChanged && + htlcState == HtlcStateSettled + + // If the HTLC has being settled for the first time, and this + // is an AMP invoice, then we'll need to update some additional + // meta data state. + if htlcSettled && invoiceIsAMP { + err = settleHtlcsAmp(invoice, key, htlc, updater) + if err != nil { + return err + } + } + + accepted := htlc.State == HtlcStateAccepted + settled := htlc.State == HtlcStateSettled + invoiceStateReady := accepted || settled + + if !invoiceIsAMP { + // Update the running amount paid to this invoice. We + // don't include accepted htlcs when the invoice is + // still open. + if invoice.State != ContractOpen && + invoiceStateReady { + + amtPaid += htlc.Amt + } + } else { + // For AMP invoices, since we won't always be reading + // out the total invoice set each time, we'll instead + // accumulate newly added invoices to the total amount + // paid. + if _, ok := update.AddHtlcs[key]; !ok { + continue + } + + // Update the running amount paid to this invoice. AMP + // invoices never go to the settled state, so if it's + // open, then we tally the HTLC. + if invoice.State == ContractOpen && + invoiceStateReady { + + amtPaid += htlc.Amt + } + } + } + + // For non-AMP invoices we recalculate the amount paid from scratch + // each time, while for AMP invoices, we'll accumulate only based on + // newly added HTLCs. + if invoiceIsAMP { + amtPaid += invoice.AmtPaid + } + + return updateInvoiceAmtPaid(invoice, amtPaid, updater) +} + +func resolveHtlc(circuitKey models.CircuitKey, htlc *InvoiceHTLC, + state HtlcState, resolveTime time.Time, + updater InvoiceUpdater) error { + + err := updater.ResolveHtlc(circuitKey, state, resolveTime) + if err != nil { + return err + } + htlc.State = state + htlc.ResolveTime = resolveTime + + return nil +} + +func updateInvoiceAmtPaid(invoice *Invoice, amt lnwire.MilliSatoshi, + updater InvoiceUpdater) error { + + err := updater.UpdateInvoiceAmtPaid(amt) + if err != nil { + return err + } + invoice.AmtPaid = amt + + return nil +} + +// settleHodlInvoice marks a hodl invoice as settled. +// +// NOTE: Currently it is not possible to have HODL AMP invoices. +func settleHodlInvoice(invoice *Invoice, hash *lntypes.Hash, + updateTime time.Time, update *InvoiceStateUpdateDesc, + updater InvoiceUpdater) error { + + if !invoice.HodlInvoice { + return fmt.Errorf("unable to settle hodl invoice: %v is "+ + "not a hodl invoice", invoice.AddIndex) + } + + // TODO(positiveblue): because NewState can only be ContractSettled we + // can remove it from the API and set it here directly. + switch { + case update == nil: + fallthrough + + case update.NewState != ContractSettled: + return fmt.Errorf("unable to settle hodl invoice: "+ + "not valid InvoiceUpdateDesc.State: %v", update) + + case update.Preimage == nil: + return fmt.Errorf("unable to settle hodl invoice: " + + "preimage is nil") + } + + newState, err := getUpdatedInvoiceState( + invoice, hash, *update, + ) + if err != nil { + return err + } + + if newState == nil || *newState != ContractSettled { + return fmt.Errorf("unable to settle hodl invoice: "+ + "new computed state is not settled: %s", newState) + } + + err = updater.UpdateInvoiceState( + ContractSettled, update.Preimage, + ) + if err != nil { + return err + } + + invoice.State = ContractSettled + invoice.Terms.PaymentPreimage = update.Preimage + + // TODO(positiveblue): this logic can be further simplified. + var amtPaid lnwire.MilliSatoshi + for key, htlc := range invoice.Htlcs { + settled, _, err := getUpdatedHtlcState( + htlc, ContractSettled, nil, + ) + if err != nil { + return err + } + + if settled { + err = resolveHtlc( + key, htlc, HtlcStateSettled, updateTime, + updater, + ) + if err != nil { + return err + } + + amtPaid += htlc.Amt + } + } + + return updateInvoiceAmtPaid(invoice, amtPaid, updater) +} + +// cancelInvoice attempts to cancel the given invoice. That includes changing +// the invoice state and the state of any relevant HTLC. +func cancelInvoice(invoice *Invoice, hash *lntypes.Hash, + updateTime time.Time, update *InvoiceStateUpdateDesc, + updater InvoiceUpdater) error { + + switch { + case update == nil: + fallthrough + + case update.NewState != ContractCanceled: + return fmt.Errorf("unable to cancel invoice: "+ + "InvoiceUpdateDesc.State not valid: %v", update) + } + + var ( + setID *[32]byte + invoiceIsAMP bool + ) + + invoiceIsAMP = invoice.IsAMP() + if invoiceIsAMP { + setID = update.SetID + } + + newState, err := getUpdatedInvoiceState(invoice, hash, *update) + if err != nil { + return err + } + + if newState == nil || *newState != ContractCanceled { + return fmt.Errorf("unable to cancel invoice(%v): new "+ + "computed state is not canceled: %s", invoice.AddIndex, + newState) + } + + err = updater.UpdateInvoiceState(ContractCanceled, nil) + if err != nil { + return err + } + invoice.State = ContractCanceled + + for key, htlc := range invoice.Htlcs { + canceled, _, err := getUpdatedHtlcState( + htlc, ContractCanceled, setID, + ) + if err != nil { + return err + } + + if canceled { + err = resolveHtlc( + key, htlc, HtlcStateCanceled, updateTime, + updater, + ) + if err != nil { + return err + } + } + } + + return nil +} + +// getUpdatedInvoiceState validates and processes an invoice state update. The +// new state to transition to is returned, so the caller is able to select +// exactly how the invoice state is updated. Note that for AMP invoices this +// function is only used to validate the state transition if we're cancelling +// the invoice. +func getUpdatedInvoiceState(invoice *Invoice, hash *lntypes.Hash, + update InvoiceStateUpdateDesc) (*ContractState, error) { + + // Returning to open is never allowed from any state. + if update.NewState == ContractOpen { + return nil, ErrInvoiceCannotOpen + } + + switch invoice.State { + // Once a contract is accepted, we can only transition to settled or + // canceled. Forbid transitioning back into this state. Otherwise this + // state is identical to ContractOpen, so we fallthrough to apply the + // same checks that we apply to open invoices. + case ContractAccepted: + if update.NewState == ContractAccepted { + return nil, ErrInvoiceCannotAccept + } + + fallthrough + + // If a contract is open, permit a state transition to accepted, settled + // or canceled. The only restriction is on transitioning to settled + // where we ensure the preimage is valid. + case ContractOpen: + if update.NewState == ContractCanceled { + return &update.NewState, nil + } + + // Sanity check that the user isn't trying to settle or accept a + // non-existent HTLC set. + set := invoice.HTLCSet(update.SetID, HtlcStateAccepted) + if len(set) == 0 { + return nil, ErrEmptyHTLCSet + } + + // For AMP invoices, there are no invoice-level preimage checks. + // However, we still sanity check that we aren't trying to + // settle an AMP invoice with a preimage. + if update.SetID != nil { + if update.Preimage != nil { + return nil, errors.New("AMP set cannot have " + + "preimage") + } + + return &update.NewState, nil + } + + switch { + // If an invoice-level preimage was supplied, but the InvoiceRef + // doesn't specify a hash (e.g. AMP invoices) we fail. + case update.Preimage != nil && hash == nil: + return nil, ErrUnexpectedInvoicePreimage + + // Validate the supplied preimage for non-AMP invoices. + case update.Preimage != nil: + if update.Preimage.Hash() != *hash { + return nil, ErrInvoicePreimageMismatch + } + + // Permit non-AMP invoices to be accepted without knowing the + // preimage. When trying to settle we'll have to pass through + // the above check in order to not hit the one below. + case update.NewState == ContractAccepted: + + // Fail if we still don't have a preimage when transitioning to + // settle the non-AMP invoice. + case update.NewState == ContractSettled && + invoice.Terms.PaymentPreimage == nil: + + return nil, errors.New("unknown preimage") + } + + return &update.NewState, nil + + // Once settled, we are in a terminal state. + case ContractSettled: + return nil, ErrInvoiceAlreadySettled + + // Once canceled, we are in a terminal state. + case ContractCanceled: + return nil, ErrInvoiceAlreadyCanceled + + default: + return nil, errors.New("unknown state transition") + } +} + +// getUpdatedInvoiceAmpState returns the AMP state of an invoice (without +// applying it), given the new state, and the amount of the HTLC that is +// being updated. +func getUpdatedInvoiceAmpState(invoice *Invoice, setID SetID, + circuitKey models.CircuitKey, state HtlcState, + amt lnwire.MilliSatoshi) (InvoiceStateAMP, error) { + + // Retrieve the AMP state for this set ID. + ampState, ok := invoice.AMPState[setID] + + // If the state is accepted then we may need to create a new entry for + // this set ID, otherwise we expect that the entry already exists and + // we can update it. + if !ok && state != HtlcStateAccepted { + return InvoiceStateAMP{}, + fmt.Errorf("unable to update AMP state for setID=%x ", + setID) + } + + switch state { + case HtlcStateAccepted: + if !ok { + // If an entry for this set ID doesn't already exist, + // then we'll need to create it. + ampState = InvoiceStateAMP{ + State: HtlcStateAccepted, + InvoiceKeys: make( + map[models.CircuitKey]struct{}, + ), + } + } + + ampState.AmtPaid += amt + + case HtlcStateCanceled: + ampState.State = HtlcStateCanceled + ampState.AmtPaid -= amt + + case HtlcStateSettled: + ampState.State = HtlcStateSettled + } + + ampState.InvoiceKeys[circuitKey] = struct{}{} + + return ampState, nil +} + +// canCancelSingleHtlc validates cancellation of a single HTLC. If nil is +// returned, then the HTLC can be cancelled. +func canCancelSingleHtlc(htlc *InvoiceHTLC, + invoiceState ContractState) error { + + // It is only possible to cancel individual htlcs on an open invoice. + if invoiceState != ContractOpen { + return fmt.Errorf("htlc canceled on invoice in state %v", + invoiceState) + } + + // It is only possible if the htlc is still pending. + if htlc.State != HtlcStateAccepted { + return fmt.Errorf("htlc canceled in state %v", htlc.State) + } + + return nil +} + +// getUpdatedHtlcState aligns the state of an htlc with the given invoice state. +// A boolean indicating whether the HTLCs state need to be updated, along with +// the new state (or old state if no change is needed) is returned. +func getUpdatedHtlcState(htlc *InvoiceHTLC, + invoiceState ContractState, setID *[32]byte) ( + bool, HtlcState, error) { + + trySettle := func(persist bool) (bool, HtlcState, error) { + if htlc.State != HtlcStateAccepted { + return false, htlc.State, nil + } + + // Settle the HTLC if it matches the settled set id. If + // there're other HTLCs with distinct setIDs, then we'll leave + // them, as they may eventually be settled as we permit + // multiple settles to a single pay_addr for AMP. + settled := false + if htlc.IsInHTLCSet(setID) { + // Non-AMP HTLCs can be settled immediately since we + // already know the preimage is valid due to checks at + // the invoice level. For AMP HTLCs, verify that the + // per-HTLC preimage-hash pair is valid. + switch { + // Non-AMP HTLCs can be settle immediately since we + // already know the preimage is valid due to checks at + // the invoice level. + case setID == nil: + + // At this point, the setID is non-nil, meaning this is + // an AMP HTLC. We know that htlc.AMP cannot be nil, + // otherwise IsInHTLCSet would have returned false. + // + // Fail if an accepted AMP HTLC has no preimage. + case htlc.AMP.Preimage == nil: + return false, htlc.State, + ErrHTLCPreimageMissing + + // Fail if the accepted AMP HTLC has an invalid + // preimage. + case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash): + return false, htlc.State, + ErrHTLCPreimageMismatch + } + + settled = true + } + + // Only persist the changes if the invoice is moving to the + // settled state, and we're actually updating the state to + // settled. + newState := htlc.State + if settled { + newState = HtlcStateSettled + } + + return persist && settled, newState, nil + } + + if invoiceState == ContractSettled { + // Check that we can settle the HTLCs. For legacy and MPP HTLCs + // this will be a NOP, but for AMP HTLCs this asserts that we + // have a valid hash/preimage pair. Passing true permits the + // method to update the HTLC to HtlcStateSettled. + return trySettle(true) + } + + // We should never find a settled HTLC on an invoice that isn't in + // ContractSettled. + if htlc.State == HtlcStateSettled { + return false, htlc.State, ErrHTLCAlreadySettled + } + + switch invoiceState { + case ContractCanceled: + htlcAlreadyCanceled := htlc.State == HtlcStateCanceled + return !htlcAlreadyCanceled, HtlcStateCanceled, nil + + // TODO(roasbeef): never fully passed thru now? + case ContractAccepted: + // Check that we can settle the HTLCs. For legacy and MPP HTLCs + // this will be a NOP, but for AMP HTLCs this asserts that we + // have a valid hash/preimage pair. Passing false prevents the + // method from putting the HTLC in HtlcStateSettled, leaving it + // in HtlcStateAccepted. + return trySettle(false) + + case ContractOpen: + return false, htlc.State, nil + + default: + return false, htlc.State, errors.New("unknown state transition") + } +} diff --git a/invoices/update_invoice_test.go b/invoices/update_invoice_test.go new file mode 100644 index 0000000000..42d370971d --- /dev/null +++ b/invoices/update_invoice_test.go @@ -0,0 +1,687 @@ +package invoices + +import ( + "testing" + "time" + + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/record" + "github.com/stretchr/testify/require" +) + +type updateHTLCTest struct { + name string + input InvoiceHTLC + invState ContractState + setID *[32]byte + output InvoiceHTLC + expErr error +} + +// TestUpdateHTLC asserts the behavior of the updateHTLC method in various +// scenarios for MPP and AMP. +func TestUpdateHTLC(t *testing.T) { + t.Parallel() + + testNow := time.Now() + setID := [32]byte{0x01} + ampRecord := record.NewAMP([32]byte{0x02}, setID, 3) + preimage := lntypes.Preimage{0x04} + hash := preimage.Hash() + + diffSetID := [32]byte{0x05} + fakePreimage := lntypes.Preimage{0x06} + testAlreadyNow := time.Now() + + tests := []updateHTLCTest{ + { + name: "MPP accept", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + invState: ContractAccepted, + setID: nil, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + expErr: nil, + }, + { + name: "MPP settle", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + invState: ContractSettled, + setID: nil, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + expErr: nil, + }, + { + name: "MPP cancel", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + invState: ContractCanceled, + setID: nil, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: nil, + }, + expErr: nil, + }, + { + name: "AMP accept missing preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: nil, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: nil, + }, + }, + expErr: ErrHTLCPreimageMissing, + }, + { + name: "AMP accept invalid preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &fakePreimage, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &fakePreimage, + }, + }, + expErr: ErrHTLCPreimageMismatch, + }, + { + name: "AMP accept valid preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "AMP accept valid preimage different htlc set", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractAccepted, + setID: &diffSetID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "AMP settle missing preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: nil, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: nil, + }, + }, + expErr: ErrHTLCPreimageMissing, + }, + { + name: "AMP settle invalid preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &fakePreimage, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &fakePreimage, + }, + }, + expErr: ErrHTLCPreimageMismatch, + }, + { + name: "AMP settle valid preimage", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + // With the newer AMP logic, this is now valid, as we + // want to be able to accept multiple settle attempts + // to a given pay_addr. In this case, the HTLC should + // remain in the accepted state. + name: "AMP settle valid preimage different htlc set", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractSettled, + setID: &diffSetID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "accept invoice htlc already settled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: ErrHTLCAlreadySettled, + }, + { + name: "cancel invoice htlc already settled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractCanceled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: ErrHTLCAlreadySettled, + }, + { + name: "settle invoice htlc already settled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateSettled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "cancel invoice", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: time.Time{}, + Expiry: 40, + State: HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractCanceled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "accept invoice htlc already canceled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractAccepted, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "cancel invoice htlc already canceled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractCanceled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + { + name: "settle invoice htlc already canceled", + input: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + invState: ContractSettled, + setID: &setID, + output: InvoiceHTLC{ + Amt: 5000, + MppTotalAmt: 5000, + AcceptHeight: 100, + AcceptTime: testNow, + ResolveTime: testAlreadyNow, + Expiry: 40, + State: HtlcStateCanceled, + CustomRecords: make(record.CustomSet), + AMP: &InvoiceHtlcAMPData{ + Record: *ampRecord, + Hash: hash, + Preimage: &preimage, + }, + }, + expErr: nil, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + testUpdateHTLC(t, test, testNow) + }) + } +} + +func testUpdateHTLC(t *testing.T, test updateHTLCTest, now time.Time) { + htlc := test.input.Copy() + stateChanged, state, err := getUpdatedHtlcState( + htlc, test.invState, test.setID, + ) + if stateChanged { + htlc.State = state + htlc.ResolveTime = now + } + + require.Equal(t, test.expErr, err) + require.Equal(t, test.output, *htlc) +} From 5e746b4d2c99210db16e6ce2b40e708812750c20 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Wed, 18 Oct 2023 15:42:17 +0200 Subject: [PATCH 11/15] invoices: move InvoiceDB tests unrelated to kvdb to invoices package --- channeldb/invoice_test.go | 2490 +----------------------------------- invoices/invoices_test.go | 2506 +++++++++++++++++++++++++++++++++++++ invoices/setup_test.go | 11 + 3 files changed, 2518 insertions(+), 2489 deletions(-) create mode 100644 invoices/invoices_test.go create mode 100644 invoices/setup_test.go diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index d0d0518a06..3fe6b668e8 100644 --- a/channeldb/invoice_test.go +++ b/channeldb/invoice_test.go @@ -2,2505 +2,17 @@ package channeldb import ( "bytes" - "context" - "crypto/rand" - "fmt" - "math" "testing" "time" "github.com/lightningnetwork/lnd/channeldb/models" - "github.com/lightningnetwork/lnd/clock" - "github.com/lightningnetwork/lnd/feature" invpkg "github.com/lightningnetwork/lnd/invoices" - "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" ) -var ( - emptyFeatures = lnwire.NewFeatureVector(nil, lnwire.Features) - ampFeatures = lnwire.NewFeatureVector( - lnwire.NewRawFeatureVector( - lnwire.TLVOnionPayloadRequired, - lnwire.PaymentAddrOptional, - lnwire.AMPRequired, - ), - lnwire.Features, - ) - testNow = time.Unix(1, 0) -) - -func randInvoice(value lnwire.MilliSatoshi) (*invpkg.Invoice, error) { - var ( - pre lntypes.Preimage - payAddr [32]byte - ) - if _, err := rand.Read(pre[:]); err != nil { - return nil, err - } - if _, err := rand.Read(payAddr[:]); err != nil { - return nil, err - } - - i := &invpkg.Invoice{ - CreationDate: testNow, - Terms: invpkg.ContractTerm{ - Expiry: 4000, - PaymentPreimage: &pre, - PaymentAddr: payAddr, - Value: value, - Features: emptyFeatures, - }, - Htlcs: map[models.CircuitKey]*invpkg.InvoiceHTLC{}, - AMPState: map[invpkg.SetID]invpkg.InvoiceStateAMP{}, - } - i.Memo = []byte("memo") - - // Create a random byte slice of MaxPaymentRequestSize bytes to be used - // as a dummy paymentrequest, and determine if it should be set based - // on one of the random bytes. - var r [invpkg.MaxPaymentRequestSize]byte - if _, err := rand.Read(r[:]); err != nil { - return nil, err - } - if r[0]&1 == 0 { - i.PaymentRequest = r[:] - } else { - i.PaymentRequest = []byte("") - } - - return i, nil -} - -// settleTestInvoice settles a test invoice. -func settleTestInvoice(invoice *invpkg.Invoice, settleIndex uint64) { - invoice.SettleDate = testNow - invoice.AmtPaid = invoice.Terms.Value - invoice.State = invpkg.ContractSettled - invoice.Htlcs[models.CircuitKey{}] = &invpkg.InvoiceHTLC{ - Amt: invoice.Terms.Value, - AcceptTime: testNow, - ResolveTime: testNow, - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - } - invoice.SettleIndex = settleIndex -} - -// Tests that pending invoices are those which are either in ContractOpen or -// in ContractAccepted state. -func TestInvoiceIsPending(t *testing.T) { - contractStates := []invpkg.ContractState{ - invpkg.ContractOpen, invpkg.ContractSettled, - invpkg.ContractCanceled, invpkg.ContractAccepted, - } - - for _, state := range contractStates { - invoice := invpkg.Invoice{ - State: state, - } - - // We expect that an invoice is pending if it's either in - // ContractOpen or ContractAccepted state. - open := invpkg.ContractOpen - accepted := invpkg.ContractAccepted - pending := (state == open || state == accepted) - - require.Equal(t, pending, invoice.IsPending()) - } -} - -type invWorkflowTest struct { - name string - queryPayHash bool - queryPayAddr bool -} - -var invWorkflowTests = []invWorkflowTest{ - { - name: "unknown", - queryPayHash: false, - queryPayAddr: false, - }, - { - name: "only payhash known", - queryPayHash: true, - queryPayAddr: false, - }, - { - name: "payaddr and payhash known", - queryPayHash: true, - queryPayAddr: true, - }, -} - -// TestInvoiceWorkflow asserts the basic process of inserting, fetching, and -// updating an invoice. We assert that the flow is successful using when -// querying with various combinations of payment hash and payment address. -func TestInvoiceWorkflow(t *testing.T) { - t.Parallel() - - for _, test := range invWorkflowTests { - test := test - t.Run(test.name, func(t *testing.T) { - testInvoiceWorkflow(t, test) - }) - } -} - -func testInvoiceWorkflow(t *testing.T, test invWorkflowTest) { - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") - - // Create a fake invoice which we'll use several times in the tests - // below. - fakeInvoice, err := randInvoice(10000) - require.NoError(t, err, "unable to create invoice") - invPayHash := fakeInvoice.Terms.PaymentPreimage.Hash() - - // Select the payment hash and payment address we will use to lookup or - // update the invoice for the remainder of the test. - var ( - payHash lntypes.Hash - payAddr *[32]byte - ref invpkg.InvoiceRef - ) - switch { - case test.queryPayHash && test.queryPayAddr: - payHash = invPayHash - payAddr = &fakeInvoice.Terms.PaymentAddr - ref = invpkg.InvoiceRefByHashAndAddr(payHash, *payAddr) - case test.queryPayHash: - payHash = invPayHash - ref = invpkg.InvoiceRefByHash(payHash) - } - - ctxb := context.Background() - // Add the invoice to the database, this should succeed as there aren't - // any existing invoices within the database with the same payment - // hash. - if _, err := db.AddInvoice(ctxb, fakeInvoice, invPayHash); err != nil { - t.Fatalf("unable to find invoice: %v", err) - } - - // Attempt to retrieve the invoice which was just added to the - // database. It should be found, and the invoice returned should be - // identical to the one created above. - dbInvoice, err := db.LookupInvoice(ctxb, ref) - if !test.queryPayAddr && !test.queryPayHash { - require.ErrorIs(t, err, invpkg.ErrInvoiceNotFound) - return - } - - require.Equal(t, - *fakeInvoice, dbInvoice, - "invoice fetched from db doesn't match original", - ) - - // The add index of the invoice retrieved from the database should now - // be fully populated. As this is the first index written to the DB, - // the addIndex should be 1. - if dbInvoice.AddIndex != 1 { - t.Fatalf("wrong add index: expected %v, got %v", 1, - dbInvoice.AddIndex) - } - - // Settle the invoice, the version retrieved from the database should - // now have the settled bit toggle to true and a non-default - // SettledDate - payAmt := fakeInvoice.Terms.Value * 2 - _, err = db.UpdateInvoice(ctxb, ref, nil, getUpdateInvoice(payAmt)) - require.NoError(t, err, "unable to settle invoice") - dbInvoice2, err := db.LookupInvoice(ctxb, ref) - require.NoError(t, err, "unable to fetch invoice") - if dbInvoice2.State != invpkg.ContractSettled { - t.Fatalf("invoice should now be settled but isn't") - } - if dbInvoice2.SettleDate.IsZero() { - t.Fatalf("invoice should have non-zero SettledDate but isn't") - } - - // Our 2x payment should be reflected, and also the settle index of 1 - // should also have been committed for this index. - if dbInvoice2.AmtPaid != payAmt { - t.Fatalf("wrong amt paid: expected %v, got %v", payAmt, - dbInvoice2.AmtPaid) - } - if dbInvoice2.SettleIndex != 1 { - t.Fatalf("wrong settle index: expected %v, got %v", 1, - dbInvoice2.SettleIndex) - } - - // Attempt to insert generated above again, this should fail as - // duplicates are rejected by the processing logic. - _, err = db.AddInvoice(ctxb, fakeInvoice, payHash) - require.ErrorIs(t, err, invpkg.ErrDuplicateInvoice) - - // Attempt to look up a non-existent invoice, this should also fail but - // with a "not found" error. - var fakeHash [32]byte - fakeRef := invpkg.InvoiceRefByHash(fakeHash) - _, err = db.LookupInvoice(ctxb, fakeRef) - require.ErrorIs(t, err, invpkg.ErrInvoiceNotFound) - - // Add 10 random invoices. - const numInvoices = 10 - amt := lnwire.NewMSatFromSatoshis(1000) - invoices := make([]*invpkg.Invoice, numInvoices+1) - invoices[0] = &dbInvoice2 - for i := 1; i < len(invoices); i++ { - invoice, err := randInvoice(amt) - if err != nil { - t.Fatalf("unable to create invoice: %v", err) - } - - hash := invoice.Terms.PaymentPreimage.Hash() - if _, err := db.AddInvoice(ctxb, invoice, hash); err != nil { - t.Fatalf("unable to add invoice %v", err) - } - - invoices[i] = invoice - } - - // Perform a scan to collect all the active invoices. - query := invpkg.InvoiceQuery{ - IndexOffset: 0, - NumMaxInvoices: math.MaxUint64, - PendingOnly: false, - } - - response, err := db.QueryInvoices(ctxb, query) - require.NoError(t, err, "invoice query failed") - - // The retrieve list of invoices should be identical as since we're - // using big endian, the invoices should be retrieved in ascending - // order (and the primary key should be incremented with each - // insertion). - for i := 0; i < len(invoices); i++ { - require.Equal(t, - *invoices[i], response.Invoices[i], - "retrieved invoice doesn't match", - ) - } -} - -// TestAddDuplicatePayAddr asserts that the payment addresses of inserted -// invoices are unique. -func TestAddDuplicatePayAddr(t *testing.T) { - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err) - - // Create two invoices with the same payment addr. - invoice1, err := randInvoice(1000) - require.NoError(t, err) - - invoice2, err := randInvoice(20000) - require.NoError(t, err) - invoice2.Terms.PaymentAddr = invoice1.Terms.PaymentAddr - - ctxb := context.Background() - - // First insert should succeed. - inv1Hash := invoice1.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice1, inv1Hash) - require.NoError(t, err) - - // Second insert should fail with duplicate payment addr. - inv2Hash := invoice2.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice2, inv2Hash) - require.Error(t, err, invpkg.ErrDuplicatePayAddr) -} - -// TestAddDuplicateKeysendPayAddr asserts that we permit duplicate payment -// addresses to be inserted if they are blank to support JIT legacy keysend -// invoices. -func TestAddDuplicateKeysendPayAddr(t *testing.T) { - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err) - - // Create two invoices with the same _blank_ payment addr. - invoice1, err := randInvoice(1000) - require.NoError(t, err) - invoice1.Terms.PaymentAddr = invpkg.BlankPayAddr - - invoice2, err := randInvoice(20000) - require.NoError(t, err) - invoice2.Terms.PaymentAddr = invpkg.BlankPayAddr - - ctxb := context.Background() - - // Inserting both should succeed without a duplicate payment address - // failure. - inv1Hash := invoice1.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice1, inv1Hash) - require.NoError(t, err) - - inv2Hash := invoice2.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice2, inv2Hash) - require.NoError(t, err) - - // Querying for each should succeed. Here we use hash+addr refs since - // the lookup will fail if the hash and addr point to different - // invoices, so if both succeed we can be assured they aren't included - // in the payment address index. - ref1 := invpkg.InvoiceRefByHashAndAddr(inv1Hash, invpkg.BlankPayAddr) - dbInv1, err := db.LookupInvoice(ctxb, ref1) - require.NoError(t, err) - require.Equal(t, invoice1, &dbInv1) - - ref2 := invpkg.InvoiceRefByHashAndAddr(inv2Hash, invpkg.BlankPayAddr) - dbInv2, err := db.LookupInvoice(ctxb, ref2) - require.NoError(t, err) - require.Equal(t, invoice2, &dbInv2) -} - -// TestFailInvoiceLookupMPPPayAddrOnly asserts that looking up a MPP invoice -// that matches _only_ by payment address fails with ErrInvoiceNotFound. This -// ensures that the HTLC's payment hash always matches the payment hash in the -// returned invoice. -func TestFailInvoiceLookupMPPPayAddrOnly(t *testing.T) { - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err) - - // Create and insert a random invoice. - invoice, err := randInvoice(1000) - require.NoError(t, err) - - payHash := invoice.Terms.PaymentPreimage.Hash() - payAddr := invoice.Terms.PaymentAddr - - ctxb := context.Background() - _, err = db.AddInvoice(ctxb, invoice, payHash) - require.NoError(t, err) - - // Modify the queried payment hash to be invalid. - payHash[0] ^= 0x01 - - // Lookup the invoice by (invalid) payment hash and payment address. The - // lookup should fail since we require the payment hash to match for - // legacy/MPP invoices, as this guarantees that the preimage is valid - // for the given HTLC. - ref := invpkg.InvoiceRefByHashAndAddr(payHash, payAddr) - _, err = db.LookupInvoice(ctxb, ref) - require.Equal(t, invpkg.ErrInvoiceNotFound, err) -} - -// TestInvRefEquivocation asserts that retrieving or updating an invoice using -// an equivocating InvoiceRef results in ErrInvRefEquivocation. -func TestInvRefEquivocation(t *testing.T) { - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err) - - // Add two random invoices. - invoice1, err := randInvoice(1000) - require.NoError(t, err) - - ctxb := context.Background() - inv1Hash := invoice1.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice1, inv1Hash) - require.NoError(t, err) - - invoice2, err := randInvoice(2000) - require.NoError(t, err) - - inv2Hash := invoice2.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice2, inv2Hash) - require.NoError(t, err) - - // Now, query using invoice 1's payment address, but invoice 2's payment - // hash. We expect an error since the invref points to multiple - // invoices. - ref := invpkg.InvoiceRefByHashAndAddr( - inv2Hash, invoice1.Terms.PaymentAddr, - ) - _, err = db.LookupInvoice(ctxb, ref) - require.Error(t, err, invpkg.ErrInvRefEquivocation) - - // The same error should be returned when updating an equivocating - // reference. - nop := func(_ *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { - return nil, nil - } - _, err = db.UpdateInvoice(ctxb, ref, nil, nop) - require.Error(t, err, invpkg.ErrInvRefEquivocation) -} - -// TestInvoiceCancelSingleHtlc tests that a single htlc can be canceled on the -// invoice. -func TestInvoiceCancelSingleHtlc(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") - - preimage := lntypes.Preimage{1} - paymentHash := preimage.Hash() - - testInvoice := &invpkg.Invoice{ - Htlcs: map[models.CircuitKey]*invpkg.InvoiceHTLC{}, - Terms: invpkg.ContractTerm{ - Value: lnwire.NewMSatFromSatoshis(10000), - Features: emptyFeatures, - PaymentPreimage: &preimage, - }, - } - - ctxb := context.Background() - if _, err := db.AddInvoice(ctxb, testInvoice, paymentHash); err != nil { - t.Fatalf("unable to find invoice: %v", err) - } - - // Accept an htlc on this invoice. - key := models.CircuitKey{ - ChanID: lnwire.NewShortChanIDFromInt(1), - HtlcID: 4, - } - htlc := invpkg.HtlcAcceptDesc{ - Amt: 500, - CustomRecords: make(record.CustomSet), - } - - callback := func( - invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { - - htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ - key: &htlc, - } - - return &invpkg.InvoiceUpdateDesc{ - UpdateType: invpkg.AddHTLCsUpdate, - AddHtlcs: htlcs, - }, nil - } - - ref := invpkg.InvoiceRefByHash(paymentHash) - invoice, err := db.UpdateInvoice(ctxb, ref, nil, callback) - require.NoError(t, err, "unable to add invoice htlc") - if len(invoice.Htlcs) != 1 { - t.Fatalf("expected the htlc to be added") - } - if invoice.Htlcs[key].State != invpkg.HtlcStateAccepted { - t.Fatalf("expected htlc in state accepted") - } - - callback = func( - invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { - - return &invpkg.InvoiceUpdateDesc{ - UpdateType: invpkg.CancelHTLCsUpdate, - CancelHtlcs: map[models.CircuitKey]struct{}{ - key: {}, - }, - }, nil - } - - // Cancel the htlc again. - invoice, err = db.UpdateInvoice(ctxb, ref, nil, callback) - require.NoError(t, err, "unable to cancel htlc") - if len(invoice.Htlcs) != 1 { - t.Fatalf("expected the htlc to be present") - } - if invoice.Htlcs[key].State != invpkg.HtlcStateCanceled { - t.Fatalf("expected htlc in state canceled") - } -} - -// TestInvoiceCancelSingleHtlcAMP tests that it's possible to cancel a single -// invoice of an AMP HTLC across multiple set IDs, and also have that update -// the amount paid and other related fields as well. -func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t, OptionClock(testClock)) - require.NoError(t, err, "unable to make test db: %v", err) - - // We'll start out by creating an invoice and writing it to the DB. - amt := lnwire.NewMSatFromSatoshis(1000) - invoice, err := randInvoice(amt) - require.Nil(t, err) - - // Set AMP-specific features so that we can settle with HTLC-level - // preimages. - invoice.Terms.Features = ampFeatures - - ctxb := context.Background() - preimage := *invoice.Terms.PaymentPreimage - payHash := preimage.Hash() - _, err = db.AddInvoice(ctxb, invoice, payHash) - require.Nil(t, err) - - // Add two HTLC sets, one with one HTLC and the other with two. - setID1 := &[32]byte{1} - setID2 := &[32]byte{2} - - ref := invpkg.InvoiceRefByHashAndAddr( - payHash, invoice.Terms.PaymentAddr, - ) - - // The first set ID with a single HTLC added. - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID1), - updateAcceptAMPHtlc(0, amt, setID1, true), - ) - require.Nil(t, err) - - // The second set ID with two HTLCs added. - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID2), - updateAcceptAMPHtlc(1, amt, setID2, true), - ) - require.Nil(t, err) - dbInvoice, err := db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID2), - updateAcceptAMPHtlc(2, amt, setID2, true), - ) - require.Nil(t, err) - - // At this point, we should detect that 3k satoshis total has been - // paid. - require.Equal(t, dbInvoice.AmtPaid, amt*3) - - callback := func( - invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { - - return &invpkg.InvoiceUpdateDesc{ - UpdateType: invpkg.CancelHTLCsUpdate, - CancelHtlcs: map[models.CircuitKey]struct{}{ - {HtlcID: 0}: {}, - }, - SetID: (*invpkg.SetID)(setID1), - }, nil - } - - // Now we'll cancel a single invoice, and assert that the amount paid - // is decremented, and the state for that HTLC set reflects that is - // been cancelled. - _, err = db.UpdateInvoice(ctxb, ref, (*invpkg.SetID)(setID1), callback) - require.NoError(t, err, "unable to cancel htlc") - - freshInvoice, err := db.LookupInvoice(ctxb, ref) - require.Nil(t, err) - dbInvoice = &freshInvoice - - // The amount paid should reflect that an invoice was cancelled. - require.Equal(t, dbInvoice.AmtPaid, amt*2) - - // The HTLC and AMP state should also show that only one HTLC set is - // left. - invoice.State = invpkg.ContractOpen - invoice.AmtPaid = 2 * amt - invoice.SettleDate = dbInvoice.SettleDate - - htlc0 := models.CircuitKey{HtlcID: 0} - htlc1 := models.CircuitKey{HtlcID: 1} - htlc2 := models.CircuitKey{HtlcID: 2} - - invoice.Htlcs = map[models.CircuitKey]*invpkg.InvoiceHTLC{ - htlc0: makeAMPInvoiceHTLC(amt, *setID1, payHash, &preimage), - htlc1: makeAMPInvoiceHTLC(amt, *setID2, payHash, &preimage), - htlc2: makeAMPInvoiceHTLC(amt, *setID2, payHash, &preimage), - } - invoice.AMPState[*setID1] = invpkg.InvoiceStateAMP{ - State: invpkg.HtlcStateCanceled, - InvoiceKeys: map[models.CircuitKey]struct{}{ - {HtlcID: 0}: {}, - }, - } - invoice.AMPState[*setID2] = invpkg.InvoiceStateAMP{ - State: invpkg.HtlcStateAccepted, - AmtPaid: amt * 2, - InvoiceKeys: map[models.CircuitKey]struct{}{ - {HtlcID: 1}: {}, - {HtlcID: 2}: {}, - }, - } - - invoice.Htlcs[htlc0].State = invpkg.HtlcStateCanceled - invoice.Htlcs[htlc0].ResolveTime = time.Unix(1, 0) - - require.Equal(t, invoice, dbInvoice) - - // Next, we'll cancel the _other_ HTLCs active, but we'll do them one - // by one. - _, err = db.UpdateInvoice(ctxb, ref, (*invpkg.SetID)(setID2), - func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, - error) { - - return &invpkg.InvoiceUpdateDesc{ - UpdateType: invpkg.CancelHTLCsUpdate, - CancelHtlcs: map[models.CircuitKey]struct{}{ - {HtlcID: 1}: {}, - }, - SetID: (*invpkg.SetID)(setID2), - }, nil - }) - require.NoError(t, err, "unable to cancel htlc") - - freshInvoice, err = db.LookupInvoice(ctxb, ref) - require.Nil(t, err) - dbInvoice = &freshInvoice - - invoice.Htlcs[htlc1].State = invpkg.HtlcStateCanceled - invoice.Htlcs[htlc1].ResolveTime = time.Unix(1, 0) - invoice.AmtPaid = amt - - ampState := invoice.AMPState[*setID2] - ampState.State = invpkg.HtlcStateCanceled - ampState.AmtPaid = amt - invoice.AMPState[*setID2] = ampState - - require.Equal(t, invoice, dbInvoice) - - callback = func( - invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { - - return &invpkg.InvoiceUpdateDesc{ - UpdateType: invpkg.CancelHTLCsUpdate, - CancelHtlcs: map[models.CircuitKey]struct{}{ - {HtlcID: 2}: {}, - }, - SetID: (*invpkg.SetID)(setID2), - }, nil - } - - // Now we'll cancel the final HTLC, which should cause all the active - // HTLCs to transition to the cancelled state. - _, err = db.UpdateInvoice(ctxb, ref, (*invpkg.SetID)(setID2), callback) - require.NoError(t, err, "unable to cancel htlc") - - freshInvoice, err = db.LookupInvoice(ctxb, ref) - require.Nil(t, err) - dbInvoice = &freshInvoice - - ampState = invoice.AMPState[*setID2] - ampState.AmtPaid = 0 - invoice.AMPState[*setID2] = ampState - - invoice.Htlcs[htlc2].State = invpkg.HtlcStateCanceled - invoice.Htlcs[htlc2].ResolveTime = time.Unix(1, 0) - invoice.AmtPaid = 0 - - require.Equal(t, invoice, dbInvoice) -} - -// TestInvoiceTimeSeries tests that newly added invoices invoices, as well as -// settled invoices are added to the database are properly placed in the add -// add or settle index which serves as an event time series. -func TestInvoiceAddTimeSeries(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t, OptionClock(testClock)) - require.NoError(t, err, "unable to make test db") - - ctxb := context.Background() - _, err = db.InvoicesAddedSince(ctxb, 0) - require.NoError(t, err) - - // We'll start off by creating 20 random invoices, and inserting them - // into the database. - const numInvoices = 20 - amt := lnwire.NewMSatFromSatoshis(1000) - invoices := make([]invpkg.Invoice, numInvoices) - for i := 0; i < len(invoices); i++ { - invoice, err := randInvoice(amt) - if err != nil { - t.Fatalf("unable to create invoice: %v", err) - } - - paymentHash := invoice.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice, paymentHash) - if err != nil { - t.Fatalf("unable to add invoice %v", err) - } - - invoices[i] = *invoice - } - - // With the invoices constructed, we'll now create a series of queries - // that we'll use to assert expected return values of - // InvoicesAddedSince. - addQueries := []struct { - sinceAddIndex uint64 - - resp []invpkg.Invoice - }{ - // If we specify a value of zero, we shouldn't get any invoices - // back. - { - sinceAddIndex: 0, - }, - - // If we specify a value well beyond the number of inserted - // invoices, we shouldn't get any invoices back. - { - sinceAddIndex: 99999999, - }, - - // Using an index of 1 should result in all values, but the - // first one being returned. - { - sinceAddIndex: 1, - resp: invoices[1:], - }, - - // If we use an index of 10, then we should retrieve the - // reaming 10 invoices. - { - sinceAddIndex: 10, - resp: invoices[10:], - }, - } - - for i, query := range addQueries { - resp, err := db.InvoicesAddedSince(ctxb, query.sinceAddIndex) - if err != nil { - t.Fatalf("unable to query: %v", err) - } - - require.Equal(t, len(query.resp), len(resp)) - - for j := 0; j < len(query.resp); j++ { - require.Equal(t, - query.resp[j], resp[j], - fmt.Sprintf("test: #%v, item: #%v", i, j), - ) - } - } - - _, err = db.InvoicesSettledSince(ctxb, 0) - require.NoError(t, err) - - var settledInvoices []invpkg.Invoice - var settleIndex uint64 = 1 - // We'll now only settle the latter half of each of those invoices. - for i := 10; i < len(invoices); i++ { - invoice := &invoices[i] - - paymentHash := invoice.Terms.PaymentPreimage.Hash() - - ref := invpkg.InvoiceRefByHash(paymentHash) - _, err := db.UpdateInvoice( - ctxb, ref, nil, getUpdateInvoice(invoice.Terms.Value), - ) - if err != nil { - t.Fatalf("unable to settle invoice: %v", err) - } - - // Create the settled invoice for the expectation set. - settleTestInvoice(invoice, settleIndex) - settleIndex++ - - settledInvoices = append(settledInvoices, *invoice) - } - - // We'll now prepare an additional set of queries to ensure the settle - // time series has properly been maintained in the database. - settleQueries := []struct { - sinceSettleIndex uint64 - - resp []invpkg.Invoice - }{ - // If we specify a value of zero, we shouldn't get any settled - // invoices back. - { - sinceSettleIndex: 0, - }, - - // If we specify a value well beyond the number of settled - // invoices, we shouldn't get any invoices back. - { - sinceSettleIndex: 99999999, - }, - - // Using an index of 1 should result in the final 10 invoices - // being returned, as we only settled those. - { - sinceSettleIndex: 1, - resp: settledInvoices[1:], - }, - } - - for i, query := range settleQueries { - resp, err := db.InvoicesSettledSince( - ctxb, query.sinceSettleIndex, - ) - if err != nil { - t.Fatalf("unable to query: %v", err) - } - - require.Equal(t, len(query.resp), len(resp)) - - for j := 0; j < len(query.resp); j++ { - require.Equal(t, - query.resp[j], resp[j], - fmt.Sprintf("test: #%v, item: #%v", i, j), - ) - } - } -} - -// TestSettleIndexAmpPayments tests that repeated settles of the same invoice -// end up properly adding entries to the settle index, and the -// InvoicesSettledSince will emit a "projected" version of the invoice w/ -// _just_ that HTLC information. -func TestSettleIndexAmpPayments(t *testing.T) { - t.Parallel() - - testClock := clock.NewTestClock(testNow) - db, err := MakeTestInvoiceDB(t, OptionClock(testClock)) - require.Nil(t, err) - - // First, we'll make a sample invoice that'll be paid to several times - // below. - amt := lnwire.NewMSatFromSatoshis(1000) - testInvoice, err := randInvoice(amt) - require.Nil(t, err) - testInvoice.Terms.Features = ampFeatures - - // Add the invoice to the DB, we use a dummy payment hash here but the - // invoice will have a valid payment address set. - ctxb := context.Background() - preimage := *testInvoice.Terms.PaymentPreimage - payHash := preimage.Hash() - _, err = db.AddInvoice(ctxb, testInvoice, payHash) - require.Nil(t, err) - - // Now that we have the invoice, we'll simulate 3 different HTLC sets - // being attached to the invoice. These represent 3 different - // concurrent payments. - setID1 := &[32]byte{1} - setID2 := &[32]byte{2} - setID3 := &[32]byte{3} - - ref := invpkg.InvoiceRefByHashAndAddr( - payHash, testInvoice.Terms.PaymentAddr, - ) - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID1), - updateAcceptAMPHtlc(1, amt, setID1, true), - ) - require.Nil(t, err) - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID2), - updateAcceptAMPHtlc(2, amt, setID2, true), - ) - require.Nil(t, err) - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID3), - updateAcceptAMPHtlc(3, amt, setID3, true), - ) - require.Nil(t, err) - - // Now that the invoices have been accepted, we'll exercise the - // behavior of the LookupInvoice call that allows us to modify exactly - // how we query for invoices. - // - // First, we'll query for the invoice with just the payment addr, but - // specify no HTLcs are to be included. - refNoHtlcs := invpkg.InvoiceRefByAddrBlankHtlc( - testInvoice.Terms.PaymentAddr, - ) - invoiceNoHTLCs, err := db.LookupInvoice(ctxb, refNoHtlcs) - require.Nil(t, err) - - require.Equal(t, 0, len(invoiceNoHTLCs.Htlcs)) - - // We'll now look up the HTLCs based on the individual setIDs added - // above. - for i, setID := range []*[32]byte{setID1, setID2, setID3} { - refFiltered := invpkg.InvoiceRefBySetIDFiltered(*setID) - invoiceFiltered, err := db.LookupInvoice(ctxb, refFiltered) - require.Nil(t, err) - - // Only a single HTLC should be present. - require.Equal(t, 1, len(invoiceFiltered.Htlcs)) - - // The set ID for the HTLC should match the queried set ID. - key := models.CircuitKey{HtlcID: uint64(i + 1)} - htlc := invoiceFiltered.Htlcs[key] - require.Equal(t, *setID, htlc.AMP.Record.SetID()) - - // The HTLC should show that it's in the accepted state. - require.Equal(t, htlc.State, invpkg.HtlcStateAccepted) - } - - // Now that we know the invoices are in the proper state, we'll settle - // them on by one in distinct updates. - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID1), - getUpdateInvoiceAMPSettle( - setID1, preimage, models.CircuitKey{HtlcID: 1}, - ), - ) - require.Nil(t, err) - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID2), - getUpdateInvoiceAMPSettle( - setID2, preimage, models.CircuitKey{HtlcID: 2}, - ), - ) - require.Nil(t, err) - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID3), - getUpdateInvoiceAMPSettle( - setID3, preimage, models.CircuitKey{HtlcID: 3}, - ), - ) - require.Nil(t, err) - - // Now that all the invoices have been settled, we'll ensure that the - // settle index was updated properly by obtaining all the currently - // settled invoices in the time series. We use a value of 1 here to - // ensure we get _all_ the invoices back. - settledInvoices, err := db.InvoicesSettledSince(ctxb, 1) - require.Nil(t, err) - - // To get around the settle index quirk, we'll fetch the very first - // invoice in the HTLC filtered mode and append it to the set of - // invoices. - firstInvoice, err := db.LookupInvoice( - ctxb, invpkg.InvoiceRefBySetIDFiltered(*setID1), - ) - require.Nil(t, err) - settledInvoices = append( - []invpkg.Invoice{firstInvoice}, settledInvoices..., - ) - - // There should be 3 invoices settled, as we created 3 "sub-invoices" - // above. - numInvoices := 3 - require.Equal(t, numInvoices, len(settledInvoices)) - - // Each invoice should match the set of invoices we settled above, and - // the AMPState should be set accordingly. - for i, settledInvoice := range settledInvoices { - // Only one HTLC should be projected for this settled index. - require.Equal(t, 1, len(settledInvoice.Htlcs)) - - // The invoice should show up as settled, and match the settle - // index increment. - invSetID := &[32]byte{byte(i + 1)} - subInvoiceState, ok := settledInvoice.AMPState[*invSetID] - require.True(t, ok) - - require.Equal(t, subInvoiceState.State, invpkg.HtlcStateSettled) - require.Equal(t, int(subInvoiceState.SettleIndex), i+1) - - invoiceKey := models.CircuitKey{HtlcID: uint64(i + 1)} - _, keyFound := subInvoiceState.InvoiceKeys[invoiceKey] - require.True(t, keyFound) - } - - // If we attempt to look up the invoice by the payment addr, with all - // the HTLCs, the main invoice should have 3 HTLCs present. - refWithHtlcs := invpkg.InvoiceRefByAddr(testInvoice.Terms.PaymentAddr) - invoiceWithHTLCs, err := db.LookupInvoice(ctxb, refWithHtlcs) - require.Nil(t, err) - require.Equal(t, numInvoices, len(invoiceWithHTLCs.Htlcs)) - - // Finally, delete the invoice. If we query again, then nothing should - // be found. - err = db.DeleteInvoice(ctxb, []invpkg.InvoiceDeleteRef{ - { - PayHash: payHash, - PayAddr: &testInvoice.Terms.PaymentAddr, - AddIndex: testInvoice.AddIndex, - }, - }) - require.Nil(t, err) -} - -// TestFetchPendingInvoices tests that we can fetch all pending invoices from -// the database using the FetchPendingInvoices method. -func TestFetchPendingInvoices(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t, OptionClock(testClock)) - require.NoError(t, err, "unable to make test db") - - ctxb := context.Background() - - // Make sure that fetching pending invoices from an empty database - // returns an empty result and no errors. - pending, err := db.FetchPendingInvoices(ctxb) - require.NoError(t, err) - require.Empty(t, pending) - - const numInvoices = 20 - var settleIndex uint64 = 1 - pendingInvoices := make(map[lntypes.Hash]invpkg.Invoice) - - for i := 1; i <= numInvoices; i++ { - amt := lnwire.MilliSatoshi(i * 1000) - invoice, err := randInvoice(amt) - require.NoError(t, err) - - invoice.CreationDate = invoice.CreationDate.Add( - time.Duration(i-1) * time.Second, - ) - - paymentHash := invoice.Terms.PaymentPreimage.Hash() - - _, err = db.AddInvoice(ctxb, invoice, paymentHash) - require.NoError(t, err) - - // Settle every second invoice. - if i%2 == 0 { - pendingInvoices[paymentHash] = *invoice - continue - } - - ref := invpkg.InvoiceRefByHash(paymentHash) - _, err = db.UpdateInvoice(ctxb, ref, nil, getUpdateInvoice(amt)) - require.NoError(t, err) - - settleTestInvoice(invoice, settleIndex) - settleIndex++ - } - - // Fetch all pending invoices. - pending, err = db.FetchPendingInvoices(ctxb) - require.NoError(t, err) - require.Equal(t, pendingInvoices, pending) -} - -// TestDuplicateSettleInvoice tests that if we add a new invoice and settle it -// twice, then the second time we also receive the invoice that we settled as a -// return argument. -func TestDuplicateSettleInvoice(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t, OptionClock(testClock)) - require.NoError(t, err, "unable to make test db") - - // We'll start out by creating an invoice and writing it to the DB. - amt := lnwire.NewMSatFromSatoshis(1000) - invoice, err := randInvoice(amt) - require.NoError(t, err, "unable to create invoice") - - payHash := invoice.Terms.PaymentPreimage.Hash() - - ctxb := context.Background() - if _, err := db.AddInvoice(ctxb, invoice, payHash); err != nil { - t.Fatalf("unable to add invoice %v", err) - } - - // With the invoice in the DB, we'll now attempt to settle the invoice. - ref := invpkg.InvoiceRefByHash(payHash) - dbInvoice, err := db.UpdateInvoice( - ctxb, ref, nil, getUpdateInvoice(amt), - ) - require.NoError(t, err, "unable to settle invoice") - - // We'll update what we expect the settle invoice to be so that our - // comparison below has the correct assumption. - invoice.SettleIndex = 1 - invoice.State = invpkg.ContractSettled - invoice.AmtPaid = amt - invoice.SettleDate = dbInvoice.SettleDate - invoice.Htlcs = map[models.CircuitKey]*invpkg.InvoiceHTLC{ - {}: { - Amt: amt, - AcceptTime: time.Unix(1, 0), - ResolveTime: time.Unix(1, 0), - State: invpkg.HtlcStateSettled, - CustomRecords: make(record.CustomSet), - }, - } - - // We should get back the exact same invoice that we just inserted. - require.Equal(t, invoice, dbInvoice, "wrong invoice after settle") - - // If we try to settle the invoice again, then we should get the very - // same invoice back, but with an error this time. - dbInvoice, err = db.UpdateInvoice(ctxb, ref, nil, getUpdateInvoice(amt)) - require.ErrorIs(t, err, invpkg.ErrInvoiceAlreadySettled) - - if dbInvoice == nil { - t.Fatalf("invoice from db is nil after settle!") - } - - invoice.SettleDate = dbInvoice.SettleDate - require.Equal( - t, invoice, dbInvoice, "wrong invoice after second settle", - ) -} - -// TestQueryInvoices ensures that we can properly query the invoice database for -// invoices using different types of queries. -func TestQueryInvoices(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t, OptionClock(testClock)) - require.NoError(t, err, "unable to make test db") - - // To begin the test, we'll add 50 invoices to the database. We'll - // assume that the index of the invoice within the database is the same - // as the amount of the invoice itself. - const numInvoices = 50 - var settleIndex uint64 = 1 - var invoices []invpkg.Invoice - var pendingInvoices []invpkg.Invoice - - ctxb := context.Background() - for i := 1; i <= numInvoices; i++ { - amt := lnwire.MilliSatoshi(i) - invoice, err := randInvoice(amt) - invoice.CreationDate = invoice.CreationDate.Add( - time.Duration(i-1) * time.Second, - ) - if err != nil { - t.Fatalf("unable to create invoice: %v", err) - } - - paymentHash := invoice.Terms.PaymentPreimage.Hash() - - if _, err := db.AddInvoice( - ctxb, invoice, paymentHash, - ); err != nil { - t.Fatalf("unable to add invoice: %v", err) - } - - // We'll only settle half of all invoices created. - if i%2 == 0 { - ref := invpkg.InvoiceRefByHash(paymentHash) - _, err := db.UpdateInvoice( - ctxb, ref, nil, getUpdateInvoice(amt), - ) - if err != nil { - t.Fatalf("unable to settle invoice: %v", err) - } - - // Create the settled invoice for the expectation set. - settleTestInvoice(invoice, settleIndex) - settleIndex++ - } else { - pendingInvoices = append(pendingInvoices, *invoice) - } - - invoices = append(invoices, *invoice) - } - - // The test will consist of several queries along with their respective - // expected response. Each query response should match its expected one. - testCases := []struct { - query invpkg.InvoiceQuery - expected []invpkg.Invoice - }{ - // Fetch all invoices with a single query. - { - query: invpkg.InvoiceQuery{ - NumMaxInvoices: numInvoices, - }, - expected: invoices, - }, - // Fetch all invoices with a single query, reversed. - { - query: invpkg.InvoiceQuery{ - Reversed: true, - NumMaxInvoices: numInvoices, - }, - expected: invoices, - }, - // Fetch the first 25 invoices. - { - query: invpkg.InvoiceQuery{ - NumMaxInvoices: numInvoices / 2, - }, - expected: invoices[:numInvoices/2], - }, - // Fetch the first 10 invoices, but this time iterating - // backwards. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 11, - Reversed: true, - NumMaxInvoices: numInvoices, - }, - expected: invoices[:10], - }, - // Fetch the last 40 invoices. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 10, - NumMaxInvoices: numInvoices, - }, - expected: invoices[10:], - }, - // Fetch all but the first invoice. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 1, - NumMaxInvoices: numInvoices, - }, - expected: invoices[1:], - }, - // Fetch one invoice, reversed, with index offset 3. This - // should give us the second invoice in the array. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 3, - Reversed: true, - NumMaxInvoices: 1, - }, - expected: invoices[1:2], - }, - // Same as above, at index 2. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 2, - Reversed: true, - NumMaxInvoices: 1, - }, - expected: invoices[0:1], - }, - // Fetch one invoice, at index 1, reversed. Since invoice#1 is - // the very first, there won't be any left in a reverse search, - // so we expect no invoices to be returned. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 1, - Reversed: true, - NumMaxInvoices: 1, - }, - expected: nil, - }, - // Same as above, but don't restrict the number of invoices to - // 1. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 1, - Reversed: true, - NumMaxInvoices: numInvoices, - }, - expected: nil, - }, - // Fetch one invoice, reversed, with no offset set. We expect - // the last invoice in the response. - { - query: invpkg.InvoiceQuery{ - Reversed: true, - NumMaxInvoices: 1, - }, - expected: invoices[numInvoices-1:], - }, - // Fetch one invoice, reversed, the offset set at numInvoices+1. - // We expect this to return the last invoice. - { - query: invpkg.InvoiceQuery{ - IndexOffset: numInvoices + 1, - Reversed: true, - NumMaxInvoices: 1, - }, - expected: invoices[numInvoices-1:], - }, - // Same as above, at offset numInvoices. - { - query: invpkg.InvoiceQuery{ - IndexOffset: numInvoices, - Reversed: true, - NumMaxInvoices: 1, - }, - expected: invoices[numInvoices-2 : numInvoices-1], - }, - // Fetch one invoice, at no offset (same as offset 0). We - // expect the first invoice only in the response. - { - query: invpkg.InvoiceQuery{ - NumMaxInvoices: 1, - }, - expected: invoices[:1], - }, - // Same as above, at offset 1. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 1, - NumMaxInvoices: 1, - }, - expected: invoices[1:2], - }, - // Same as above, at offset 2. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 2, - NumMaxInvoices: 1, - }, - expected: invoices[2:3], - }, - // Same as above, at offset numInvoices-1. Expect the last - // invoice to be returned. - { - query: invpkg.InvoiceQuery{ - IndexOffset: numInvoices - 1, - NumMaxInvoices: 1, - }, - expected: invoices[numInvoices-1:], - }, - // Same as above, at offset numInvoices. No invoices should be - // returned, as there are no invoices after this offset. - { - query: invpkg.InvoiceQuery{ - IndexOffset: numInvoices, - NumMaxInvoices: 1, - }, - expected: nil, - }, - // Fetch all pending invoices with a single query. - { - query: invpkg.InvoiceQuery{ - PendingOnly: true, - NumMaxInvoices: numInvoices, - }, - expected: pendingInvoices, - }, - // Fetch the first 12 pending invoices. - { - query: invpkg.InvoiceQuery{ - PendingOnly: true, - NumMaxInvoices: numInvoices / 4, - }, - expected: pendingInvoices[:len(pendingInvoices)/2], - }, - // Fetch the first 5 pending invoices, but this time iterating - // backwards. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 10, - PendingOnly: true, - Reversed: true, - NumMaxInvoices: numInvoices, - }, - // Since we seek to the invoice with index 10 and - // iterate backwards, there should only be 5 pending - // invoices before it as every other invoice within the - // index is settled. - expected: pendingInvoices[:5], - }, - // Fetch the last 15 invoices. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 20, - PendingOnly: true, - NumMaxInvoices: numInvoices, - }, - // Since we seek to the invoice with index 20, there are - // 30 invoices left. From these 30, only 15 of them are - // still pending. - expected: pendingInvoices[len(pendingInvoices)-15:], - }, - // Fetch all invoices paginating backwards, with an index offset - // that is beyond our last offset. We expect all invoices to be - // returned. - { - query: invpkg.InvoiceQuery{ - IndexOffset: numInvoices * 2, - PendingOnly: false, - Reversed: true, - NumMaxInvoices: numInvoices, - }, - expected: invoices, - }, - // Fetch invoices <= 25 by creation date. - { - query: invpkg.InvoiceQuery{ - NumMaxInvoices: numInvoices, - CreationDateEnd: 25, - }, - expected: invoices[:25], - }, - // Fetch invoices >= 26 creation date. - { - query: invpkg.InvoiceQuery{ - NumMaxInvoices: numInvoices, - CreationDateStart: 26, - }, - expected: invoices[25:], - }, - // Fetch pending invoices <= 25 by creation date. - { - query: invpkg.InvoiceQuery{ - PendingOnly: true, - NumMaxInvoices: numInvoices, - CreationDateEnd: 25, - }, - expected: pendingInvoices[:13], - }, - // Fetch pending invoices >= 26 creation date. - { - query: invpkg.InvoiceQuery{ - PendingOnly: true, - NumMaxInvoices: numInvoices, - CreationDateStart: 26, - }, - expected: pendingInvoices[13:], - }, - // Fetch pending invoices with offset and end creation date. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 20, - NumMaxInvoices: numInvoices, - CreationDateEnd: 30, - }, - // Since we're skipping to invoice 20 and iterating - // to invoice 30, we'll expect those invoices. - expected: invoices[20:30], - }, - // Fetch pending invoices with offset and start creation date - // in reversed order. - { - query: invpkg.InvoiceQuery{ - IndexOffset: 21, - Reversed: true, - NumMaxInvoices: numInvoices, - CreationDateStart: 11, - }, - // Since we're skipping to invoice 20 and iterating - // backward to invoice 10, we'll expect those invoices. - expected: invoices[10:20], - }, - // Fetch invoices with start and end creation date. - { - query: invpkg.InvoiceQuery{ - NumMaxInvoices: numInvoices, - CreationDateStart: 11, - CreationDateEnd: 20, - }, - expected: invoices[10:20], - }, - // Fetch pending invoices with start and end creation date. - { - query: invpkg.InvoiceQuery{ - PendingOnly: true, - NumMaxInvoices: numInvoices, - CreationDateStart: 11, - CreationDateEnd: 20, - }, - expected: pendingInvoices[5:10], - }, - // Fetch invoices with start and end creation date in reverse - // order. - { - query: invpkg.InvoiceQuery{ - Reversed: true, - NumMaxInvoices: numInvoices, - CreationDateStart: 11, - CreationDateEnd: 20, - }, - expected: invoices[10:20], - }, - // Fetch pending invoices with start and end creation date in - // reverse order. - { - query: invpkg.InvoiceQuery{ - PendingOnly: true, - Reversed: true, - NumMaxInvoices: numInvoices, - CreationDateStart: 11, - CreationDateEnd: 20, - }, - expected: pendingInvoices[5:10], - }, - // Fetch invoices with a start date greater than end date - // should result in an empty slice. - { - query: invpkg.InvoiceQuery{ - NumMaxInvoices: numInvoices, - CreationDateStart: 20, - CreationDateEnd: 11, - }, - expected: nil, - }, - } - - for i, testCase := range testCases { - response, err := db.QueryInvoices(ctxb, testCase.query) - if err != nil { - t.Fatalf("unable to query invoice database: %v", err) - } - - require.Equal(t, len(testCase.expected), len(response.Invoices)) - - for j, expected := range testCase.expected { - require.Equal(t, - expected, response.Invoices[j], - fmt.Sprintf("test: #%v, item: #%v", i, j), - ) - } - } -} - -// getUpdateInvoice returns an invoice update callback that, when called, -// settles the invoice with the given amount. -func getUpdateInvoice(amt lnwire.MilliSatoshi) invpkg.InvoiceUpdateCallback { - return func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, - error) { - - if invoice.State == invpkg.ContractSettled { - return nil, invpkg.ErrInvoiceAlreadySettled - } - - noRecords := make(record.CustomSet) - htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ - {}: { - Amt: amt, - CustomRecords: noRecords, - }, - } - update := &invpkg.InvoiceUpdateDesc{ - UpdateType: invpkg.AddHTLCsUpdate, - State: &invpkg.InvoiceStateUpdateDesc{ - Preimage: invoice.Terms.PaymentPreimage, - NewState: invpkg.ContractSettled, - }, - AddHtlcs: htlcs, - } - - return update, nil - } -} - -// TestCustomRecords tests that custom records are properly recorded in the -// invoice database. -func TestCustomRecords(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") - - preimage := lntypes.Preimage{1} - paymentHash := preimage.Hash() - - testInvoice := &invpkg.Invoice{ - Htlcs: map[models.CircuitKey]*invpkg.InvoiceHTLC{}, - Terms: invpkg.ContractTerm{ - Value: lnwire.NewMSatFromSatoshis(10000), - Features: emptyFeatures, - PaymentPreimage: &preimage, - }, - } - - ctxb := context.Background() - if _, err := db.AddInvoice(ctxb, testInvoice, paymentHash); err != nil { - t.Fatalf("unable to add invoice: %v", err) - } - - // Accept an htlc with custom records on this invoice. - key := models.CircuitKey{ - ChanID: lnwire.NewShortChanIDFromInt(1), - HtlcID: 4, - } - - records := record.CustomSet{ - 100000: []byte{}, - 100001: []byte{1, 2}, - } - - ref := invpkg.InvoiceRefByHash(paymentHash) - callback := func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, - error) { - - htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ - key: { - Amt: 500, - CustomRecords: records, - }, - } - - return &invpkg.InvoiceUpdateDesc{ - AddHtlcs: htlcs, - UpdateType: invpkg.AddHTLCsUpdate, - }, nil - } - - _, err = db.UpdateInvoice(ctxb, ref, nil, callback) - require.NoError(t, err, "unable to add invoice htlc") - - // Retrieve the invoice from that database and verify that the custom - // records are present. - dbInvoice, err := db.LookupInvoice(ctxb, ref) - require.NoError(t, err, "unable to lookup invoice") - - if len(dbInvoice.Htlcs) != 1 { - t.Fatalf("expected the htlc to be added") - } - - require.Equal(t, - records, dbInvoice.Htlcs[key].CustomRecords, - "invalid custom records", - ) -} - -// TestInvoiceHtlcAMPFields asserts that the set id and preimage fields are -// properly recorded when updating an invoice. -func TestInvoiceHtlcAMPFields(t *testing.T) { - t.Run("amp", func(t *testing.T) { - testInvoiceHtlcAMPFields(t, true) - }) - t.Run("no amp", func(t *testing.T) { - testInvoiceHtlcAMPFields(t, false) - }) -} - -func testInvoiceHtlcAMPFields(t *testing.T, isAMP bool) { - db, err := MakeTestInvoiceDB(t) - require.Nil(t, err) - - testInvoice, err := randInvoice(1000) - require.Nil(t, err) - - if isAMP { - testInvoice.Terms.Features = ampFeatures - } - - ctxb := context.Background() - payHash := testInvoice.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, testInvoice, payHash) - require.Nil(t, err) - - // Accept an htlc with custom records on this invoice. - key := models.CircuitKey{ - ChanID: lnwire.NewShortChanIDFromInt(1), - HtlcID: 4, - } - records := make(map[uint64][]byte) - - var ampData *invpkg.InvoiceHtlcAMPData - if isAMP { - amp := record.NewAMP([32]byte{1}, [32]byte{2}, 3) - preimage := &lntypes.Preimage{4} - - ampData = &invpkg.InvoiceHtlcAMPData{ - Record: *amp, - Hash: preimage.Hash(), - Preimage: preimage, - } - } - - callback := func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, - error) { - - htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ - key: { - Amt: 500, - AMP: ampData, - CustomRecords: records, - }, - } - - return &invpkg.InvoiceUpdateDesc{ - AddHtlcs: htlcs, - UpdateType: invpkg.AddHTLCsUpdate, - }, nil - } - - ref := invpkg.InvoiceRefByHash(payHash) - _, err = db.UpdateInvoice(ctxb, ref, nil, callback) - require.Nil(t, err) - - // Retrieve the invoice from that database and verify that the AMP - // fields are as expected. - dbInvoice, err := db.LookupInvoice(ctxb, ref) - require.Nil(t, err) - - require.Equal(t, 1, len(dbInvoice.Htlcs)) - require.Equal(t, ampData, dbInvoice.Htlcs[key].AMP) -} - -// TestInvoiceRef asserts that the proper identifiers are returned from an -// InvoiceRef depending on the constructor used. -func TestInvoiceRef(t *testing.T) { - payHash := lntypes.Hash{0x01} - payAddr := [32]byte{0x02} - setID := [32]byte{0x03} - - // An InvoiceRef by hash should return the provided hash and a nil - // payment addr. - refByHash := invpkg.InvoiceRefByHash(payHash) - require.Equal(t, &payHash, refByHash.PayHash()) - require.Equal(t, (*[32]byte)(nil), refByHash.PayAddr()) - require.Equal(t, (*[32]byte)(nil), refByHash.SetID()) - - // An InvoiceRef by hash and addr should return the payment hash and - // payment addr passed to the constructor. - refByHashAndAddr := invpkg.InvoiceRefByHashAndAddr(payHash, payAddr) - require.Equal(t, &payHash, refByHashAndAddr.PayHash()) - require.Equal(t, &payAddr, refByHashAndAddr.PayAddr()) - require.Equal(t, (*[32]byte)(nil), refByHashAndAddr.SetID()) - - // An InvoiceRef by set id should return an empty pay hash, a nil pay - // addr, and a reference to the given set id. - refBySetID := invpkg.InvoiceRefBySetID(setID) - require.Equal(t, (*lntypes.Hash)(nil), refBySetID.PayHash()) - require.Equal(t, (*[32]byte)(nil), refBySetID.PayAddr()) - require.Equal(t, &setID, refBySetID.SetID()) - - // An InvoiceRef by pay addr should only return a pay addr, but nil for - // pay hash and set id. - refByAddr := invpkg.InvoiceRefByAddr(payAddr) - require.Equal(t, (*lntypes.Hash)(nil), refByAddr.PayHash()) - require.Equal(t, &payAddr, refByAddr.PayAddr()) - require.Equal(t, (*[32]byte)(nil), refByAddr.SetID()) -} - -// TestHTLCSet asserts that HTLCSet returns the proper set of accepted HTLCs -// that can be considered for settlement. It asserts that MPP and AMP HTLCs do -// not comingle, and also that HTLCs with disjoint set ids appear in different -// sets. -func TestHTLCSet(t *testing.T) { - inv := &invpkg.Invoice{ - Htlcs: make(map[models.CircuitKey]*invpkg.InvoiceHTLC), - } - - // Construct two distinct set id's, in this test we'll also track the - // nil set id as a third group. - setID1 := &[32]byte{1} - setID2 := &[32]byte{2} - - // Create the expected htlc sets for each group, these will be updated - // as the invoice is modified. - - expSetNil := make(map[models.CircuitKey]*invpkg.InvoiceHTLC) - expSet1 := make(map[models.CircuitKey]*invpkg.InvoiceHTLC) - expSet2 := make(map[models.CircuitKey]*invpkg.InvoiceHTLC) - - checkHTLCSets := func() { - require.Equal( - t, expSetNil, - inv.HTLCSet(nil, invpkg.HtlcStateAccepted), - ) - require.Equal( - t, expSet1, - inv.HTLCSet(setID1, invpkg.HtlcStateAccepted), - ) - require.Equal( - t, expSet2, - inv.HTLCSet(setID2, invpkg.HtlcStateAccepted), - ) - } - - // All HTLC sets should be empty initially. - checkHTLCSets() - - // Add the following sequence of HTLCs to the invoice, sanity checking - // all three HTLC sets after each transition. This sequence asserts: - // - both nil and non-nil set ids can have multiple htlcs. - // - there may be distinct htlc sets with non-nil set ids. - // - only accepted htlcs are returned as part of the set. - htlcs := []struct { - setID *[32]byte - state invpkg.HtlcState - }{ - {nil, invpkg.HtlcStateAccepted}, - {nil, invpkg.HtlcStateAccepted}, - {setID1, invpkg.HtlcStateAccepted}, - {setID1, invpkg.HtlcStateAccepted}, - {setID2, invpkg.HtlcStateAccepted}, - {setID2, invpkg.HtlcStateAccepted}, - {nil, invpkg.HtlcStateCanceled}, - {setID1, invpkg.HtlcStateCanceled}, - {setID2, invpkg.HtlcStateCanceled}, - {nil, invpkg.HtlcStateSettled}, - {setID1, invpkg.HtlcStateSettled}, - {setID2, invpkg.HtlcStateSettled}, - } - - for i, h := range htlcs { - var ampData *invpkg.InvoiceHtlcAMPData - if h.setID != nil { - ampData = &invpkg.InvoiceHtlcAMPData{ - Record: *record.NewAMP( - [32]byte{0}, *h.setID, 0, - ), - } - } - - // Add the HTLC to the invoice's set of HTLCs. - key := models.CircuitKey{HtlcID: uint64(i)} - htlc := &invpkg.InvoiceHTLC{ - AMP: ampData, - State: h.state, - } - inv.Htlcs[key] = htlc - - // Update our expected htlc set if the htlc is accepted, - // otherwise it shouldn't be reflected. - if h.state == invpkg.HtlcStateAccepted { - switch h.setID { - case nil: - expSetNil[key] = htlc - case setID1: - expSet1[key] = htlc - case setID2: - expSet2[key] = htlc - default: - t.Fatalf("unexpected set id") - } - } - - checkHTLCSets() - } -} - -// TestAddInvoiceWithHTLCs asserts that you can't insert an invoice that already -// has HTLCs. -func TestAddInvoiceWithHTLCs(t *testing.T) { - db, err := MakeTestInvoiceDB(t) - require.Nil(t, err) - - testInvoice, err := randInvoice(1000) - require.Nil(t, err) - - key := models.CircuitKey{HtlcID: 1} - testInvoice.Htlcs[key] = &invpkg.InvoiceHTLC{} - - payHash := testInvoice.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(context.Background(), testInvoice, payHash) - require.Equal(t, invpkg.ErrInvoiceHasHtlcs, err) -} - -// TestSetIDIndex asserts that the set id index properly adds new invoices as we -// accept HTLCs, that they can be queried by their set id after accepting, and -// that invoices with duplicate set ids are disallowed. -func TestSetIDIndex(t *testing.T) { - testClock := clock.NewTestClock(testNow) - db, err := MakeTestInvoiceDB(t, OptionClock(testClock)) - require.Nil(t, err) - - // We'll start out by creating an invoice and writing it to the DB. - amt := lnwire.NewMSatFromSatoshis(1000) - invoice, err := randInvoice(amt) - require.Nil(t, err) - - // Set AMP-specific features so that we can settle with HTLC-level - // preimages. - invoice.Terms.Features = ampFeatures - - ctxb := context.Background() - preimage := *invoice.Terms.PaymentPreimage - payHash := preimage.Hash() - _, err = db.AddInvoice(ctxb, invoice, payHash) - require.Nil(t, err) - - setID := &[32]byte{1} - - // Update the invoice with an accepted HTLC that also accepts the - // invoice. - ref := invpkg.InvoiceRefByHashAndAddr( - payHash, invoice.Terms.PaymentAddr, - ) - dbInvoice, err := db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID), - updateAcceptAMPHtlc(0, amt, setID, true), - ) - require.Nil(t, err) - - // We'll update what we expect the accepted invoice to be so that our - // comparison below has the correct assumption. - invoice.State = invpkg.ContractOpen - invoice.AmtPaid = amt - invoice.SettleDate = dbInvoice.SettleDate - htlc0 := models.CircuitKey{HtlcID: 0} - invoice.Htlcs = map[models.CircuitKey]*invpkg.InvoiceHTLC{ - htlc0: makeAMPInvoiceHTLC(amt, *setID, payHash, &preimage), - } - invoice.AMPState = map[invpkg.SetID]invpkg.InvoiceStateAMP{} - invoice.AMPState[*setID] = invpkg.InvoiceStateAMP{ - State: invpkg.HtlcStateAccepted, - AmtPaid: amt, - InvoiceKeys: map[models.CircuitKey]struct{}{ - htlc0: {}, - }, - } - - // We should get back the exact same invoice that we just inserted. - require.Equal(t, invoice, dbInvoice) - - // Now lookup the invoice by set id and see that we get the same one. - refBySetID := invpkg.InvoiceRefBySetID(*setID) - dbInvoiceBySetID, err := db.LookupInvoice(ctxb, refBySetID) - require.Nil(t, err) - require.Equal(t, invoice, &dbInvoiceBySetID) - - // Trying to accept an HTLC to a different invoice, but using the same - // set id should fail. - invoice2, err := randInvoice(amt) - require.Nil(t, err) - - // Set AMP-specific features so that we can settle with HTLC-level - // preimages. - invoice2.Terms.Features = ampFeatures - - payHash2 := invoice2.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice2, payHash2) - require.Nil(t, err) - - ref2 := invpkg.InvoiceRefByHashAndAddr( - payHash2, invoice2.Terms.PaymentAddr, - ) - _, err = db.UpdateInvoice( - ctxb, ref2, (*invpkg.SetID)(setID), - updateAcceptAMPHtlc(0, amt, setID, true), - ) - require.Equal(t, invpkg.ErrDuplicateSetID{SetID: *setID}, err) - - // Now, begin constructing a second htlc set under a different set id. - // This set will contain two distinct HTLCs. - setID2 := &[32]byte{2} - - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID2), - updateAcceptAMPHtlc(1, amt, setID2, false), - ) - require.Nil(t, err) - dbInvoice, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID2), - updateAcceptAMPHtlc(2, amt, setID2, false), - ) - require.Nil(t, err) - - // We'll update what we expect the settle invoice to be so that our - // comparison below has the correct assumption. - invoice.State = invpkg.ContractOpen - invoice.AmtPaid += 2 * amt - invoice.SettleDate = dbInvoice.SettleDate - htlc1 := models.CircuitKey{HtlcID: 1} - htlc2 := models.CircuitKey{HtlcID: 2} - invoice.Htlcs = map[models.CircuitKey]*invpkg.InvoiceHTLC{ - htlc0: makeAMPInvoiceHTLC(amt, *setID, payHash, &preimage), - htlc1: makeAMPInvoiceHTLC(amt, *setID2, payHash, nil), - htlc2: makeAMPInvoiceHTLC(amt, *setID2, payHash, nil), - } - invoice.AMPState[*setID] = invpkg.InvoiceStateAMP{ - State: invpkg.HtlcStateAccepted, - AmtPaid: amt, - InvoiceKeys: map[models.CircuitKey]struct{}{ - htlc0: {}, - }, - } - invoice.AMPState[*setID2] = invpkg.InvoiceStateAMP{ - State: invpkg.HtlcStateAccepted, - AmtPaid: amt * 2, - InvoiceKeys: map[models.CircuitKey]struct{}{ - htlc1: {}, - htlc2: {}, - }, - } - - // Since UpdateInvoice will only return the sub-set of updated HTLcs, - // we'll query again to ensure we get the full set of HTLCs returned. - freshInvoice, err := db.LookupInvoice(ctxb, ref) - require.Nil(t, err) - dbInvoice = &freshInvoice - - // We should get back the exact same invoice that we just inserted. - require.Equal(t, invoice, dbInvoice) - - // Now lookup the invoice by second set id and see that we get the same - // index, including the htlcs under the first set id. - refBySetID = invpkg.InvoiceRefBySetID(*setID2) - dbInvoiceBySetID, err = db.LookupInvoice(ctxb, refBySetID) - require.Nil(t, err) - require.Equal(t, invoice, &dbInvoiceBySetID) - - // Now attempt to settle a non-existent HTLC set, this set ID is the - // zero setID so it isn't used for anything internally. - _, err = db.UpdateInvoice( - ctxb, ref, nil, - getUpdateInvoiceAMPSettle( - &[32]byte{}, [32]byte{}, - models.CircuitKey{HtlcID: 99}, - ), - ) - require.Equal(t, invpkg.ErrEmptyHTLCSet, err) - - // Now settle the first htlc set. The existing HTLCs should remain in - // the accepted state and shouldn't be canceled, since we permit an - // invoice to be settled multiple times. - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID), - getUpdateInvoiceAMPSettle( - setID, preimage, models.CircuitKey{HtlcID: 0}, - ), - ) - require.Nil(t, err) - - freshInvoice, err = db.LookupInvoice(ctxb, ref) - require.Nil(t, err) - dbInvoice = &freshInvoice - - invoice.State = invpkg.ContractOpen - - // The amount paid should reflect that we have 3 present HTLCs, each - // with an amount of the original invoice. - invoice.AmtPaid = amt * 3 - - ampState := invoice.AMPState[*setID] - ampState.State = invpkg.HtlcStateSettled - ampState.SettleDate = testNow - ampState.SettleIndex = 1 - - invoice.AMPState[*setID] = ampState - - invoice.Htlcs[htlc0].State = invpkg.HtlcStateSettled - invoice.Htlcs[htlc0].ResolveTime = time.Unix(1, 0) - - require.Equal(t, invoice, dbInvoice) - - // If we try to settle the same set ID again, then we should get an - // error, as it's already been settled. - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID), - getUpdateInvoiceAMPSettle( - setID, preimage, models.CircuitKey{HtlcID: 0}, - ), - ) - require.Equal(t, invpkg.ErrEmptyHTLCSet, err) - - // Next, let's attempt to settle the other active set ID for this - // invoice. This will allow us to exercise the case where we go to - // settle an invoice with a new setID after one has already been fully - // settled. - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID2), - getUpdateInvoiceAMPSettle( - setID2, preimage, models.CircuitKey{HtlcID: 1}, - models.CircuitKey{HtlcID: 2}, - ), - ) - require.Nil(t, err) - - freshInvoice, err = db.LookupInvoice(ctxb, ref) - require.Nil(t, err) - dbInvoice = &freshInvoice - - // Now the rest of the HTLCs should show as fully settled. - ampState = invoice.AMPState[*setID2] - ampState.State = invpkg.HtlcStateSettled - ampState.SettleDate = testNow - ampState.SettleIndex = 2 - - invoice.AMPState[*setID2] = ampState - - invoice.Htlcs[htlc1].State = invpkg.HtlcStateSettled - invoice.Htlcs[htlc1].ResolveTime = time.Unix(1, 0) - invoice.Htlcs[htlc1].AMP.Preimage = &preimage - - invoice.Htlcs[htlc2].State = invpkg.HtlcStateSettled - invoice.Htlcs[htlc2].ResolveTime = time.Unix(1, 0) - invoice.Htlcs[htlc2].AMP.Preimage = &preimage - - require.Equal(t, invoice, dbInvoice) - - // Lastly, querying for an unknown set id should fail. - refUnknownSetID := invpkg.InvoiceRefBySetID([32]byte{}) - _, err = db.LookupInvoice(ctxb, refUnknownSetID) - require.Equal(t, invpkg.ErrInvoiceNotFound, err) -} - -func makeAMPInvoiceHTLC(amt lnwire.MilliSatoshi, setID [32]byte, - hash lntypes.Hash, preimage *lntypes.Preimage) *invpkg.InvoiceHTLC { - - return &invpkg.InvoiceHTLC{ - Amt: amt, - AcceptTime: testNow, - ResolveTime: time.Time{}, - State: invpkg.HtlcStateAccepted, - CustomRecords: make(record.CustomSet), - AMP: &invpkg.InvoiceHtlcAMPData{ - Record: *record.NewAMP([32]byte{}, setID, 0), - Hash: hash, - Preimage: preimage, - }, - } -} - -// updateAcceptAMPHtlc returns an invoice update callback that, when called, -// settles the invoice with the given amount. -func updateAcceptAMPHtlc(id uint64, amt lnwire.MilliSatoshi, - setID *[32]byte, accept bool) invpkg.InvoiceUpdateCallback { - - return func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, - error) { - - if invoice.State == invpkg.ContractSettled { - return nil, invpkg.ErrInvoiceAlreadySettled - } - - noRecords := make(record.CustomSet) - - var ( - state *invpkg.InvoiceStateUpdateDesc - preimage *lntypes.Preimage - ) - if accept { - state = &invpkg.InvoiceStateUpdateDesc{ - NewState: invpkg.ContractAccepted, - SetID: setID, - } - pre := *invoice.Terms.PaymentPreimage - preimage = &pre - } - - ampData := &invpkg.InvoiceHtlcAMPData{ - Record: *record.NewAMP([32]byte{}, *setID, 0), - Hash: invoice.Terms.PaymentPreimage.Hash(), - Preimage: preimage, - } - - htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ - {HtlcID: id}: { - Amt: amt, - CustomRecords: noRecords, - AMP: ampData, - }, - } - - update := &invpkg.InvoiceUpdateDesc{ - State: state, - AddHtlcs: htlcs, - UpdateType: invpkg.AddHTLCsUpdate, - } - - return update, nil - } -} - -func getUpdateInvoiceAMPSettle(setID *[32]byte, preimage [32]byte, - circuitKeys ...models.CircuitKey) invpkg.InvoiceUpdateCallback { - - return func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, - error) { - - if invoice.State == invpkg.ContractSettled { - return nil, invpkg.ErrInvoiceAlreadySettled - } - - preImageSet := make(map[models.CircuitKey]lntypes.Preimage) - for _, key := range circuitKeys { - preImageSet[key] = preimage - } - - update := &invpkg.InvoiceUpdateDesc{ - // TODO(positiveblue): this would be an invalid update - // because tires to settle an AMP invoice without adding - // any new htlc. - UpdateType: invpkg.AddHTLCsUpdate, - State: &invpkg.InvoiceStateUpdateDesc{ - Preimage: nil, - NewState: invpkg.ContractSettled, - SetID: setID, - HTLCPreimages: preImageSet, - }, - } - - return update, nil - } -} - -// TestUnexpectedInvoicePreimage asserts that legacy or MPP invoices cannot be -// settled when referenced by payment address only. Since regular or MPP -// payments do not store the payment hash explicitly (it is stored in the -// index), this enforces that they can only be updated using a InvoiceRefByHash -// or InvoiceRefByHashOrAddr. -func TestUnexpectedInvoicePreimage(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") - - invoice, err := randInvoice(lnwire.MilliSatoshi(100)) - require.NoError(t, err) - - ctxb := context.Background() - - // Add a random invoice indexed by payment hash and payment addr. - paymentHash := invoice.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice, paymentHash) - require.NoError(t, err) - - // Attempt to update the invoice by pay addr only. This will fail since, - // in order to settle an MPP invoice, the InvoiceRef must present a - // payment hash against which to validate the preimage. - _, err = db.UpdateInvoice( - ctxb, invpkg.InvoiceRefByAddr(invoice.Terms.PaymentAddr), nil, - getUpdateInvoice(invoice.Terms.Value), - ) - - // Assert that we get ErrUnexpectedInvoicePreimage. - require.Error(t, invpkg.ErrUnexpectedInvoicePreimage, err) -} - -type updateHTLCPreimageTestCase struct { - name string - settleSamePreimage bool - expError error -} - -// TestUpdateHTLCPreimages asserts various properties of setting HTLC-level -// preimages on invoice state transitions. -func TestUpdateHTLCPreimages(t *testing.T) { - t.Parallel() - - tests := []updateHTLCPreimageTestCase{ - { - name: "same preimage on settle", - settleSamePreimage: true, - expError: nil, - }, - { - name: "diff preimage on settle", - settleSamePreimage: false, - expError: invpkg.ErrHTLCPreimageAlreadyExists, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - testUpdateHTLCPreimages(t, test) - }) - } -} - -func testUpdateHTLCPreimages(t *testing.T, test updateHTLCPreimageTestCase) { - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") - - // We'll start out by creating an invoice and writing it to the DB. - amt := lnwire.NewMSatFromSatoshis(1000) - invoice, err := randInvoice(amt) - require.Nil(t, err) - - preimage := *invoice.Terms.PaymentPreimage - payHash := preimage.Hash() - - // Set AMP-specific features so that we can settle with HTLC-level - // preimages. - invoice.Terms.Features = ampFeatures - - ctxb := context.Background() - _, err = db.AddInvoice(ctxb, invoice, payHash) - require.Nil(t, err) - - setID := &[32]byte{1} - - // Update the invoice with an accepted HTLC that also accepts the - // invoice. - ref := invpkg.InvoiceRefByAddr(invoice.Terms.PaymentAddr) - dbInvoice, err := db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID), - updateAcceptAMPHtlc(0, amt, setID, true), - ) - require.Nil(t, err) - - htlcPreimages := make(map[models.CircuitKey]lntypes.Preimage) - for key := range dbInvoice.Htlcs { - // Set the either the same preimage used to accept above, or a - // blank preimage depending on the test case. - var pre lntypes.Preimage - if test.settleSamePreimage { - pre = preimage - } - htlcPreimages[key] = pre - } - - updateInvoice := func( - invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { - - update := &invpkg.InvoiceUpdateDesc{ - // TODO(positiveblue): this would be an invalid update - // because tires to settle an AMP invoice without adding - // any new htlc. - State: &invpkg.InvoiceStateUpdateDesc{ - Preimage: nil, - NewState: invpkg.ContractSettled, - HTLCPreimages: htlcPreimages, - SetID: setID, - }, - UpdateType: invpkg.AddHTLCsUpdate, - } - - return update, nil - } - - // Now settle the HTLC set and assert the resulting error. - _, err = db.UpdateInvoice( - ctxb, ref, (*invpkg.SetID)(setID), updateInvoice, - ) - require.Equal(t, test.expError, err) -} - -// TestDeleteInvoices tests that deleting a list of invoices will succeed -// if all delete references are valid, or will fail otherwise. -func TestDeleteInvoices(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") - - // Add some invoices to the test db. - numInvoices := 3 - invoicesToDelete := make([]invpkg.InvoiceDeleteRef, numInvoices) - - ctxb := context.Background() - for i := 0; i < numInvoices; i++ { - invoice, err := randInvoice(lnwire.MilliSatoshi(i + 1)) - require.NoError(t, err) - - paymentHash := invoice.Terms.PaymentPreimage.Hash() - addIndex, err := db.AddInvoice(ctxb, invoice, paymentHash) - require.NoError(t, err) - - // Settle the second invoice. - if i == 1 { - invoice, err = db.UpdateInvoice( - ctxb, invpkg.InvoiceRefByHash(paymentHash), nil, - getUpdateInvoice(invoice.Terms.Value), - ) - require.NoError(t, err, "unable to settle invoice") - } - - // store the delete ref for later. - invoicesToDelete[i] = invpkg.InvoiceDeleteRef{ - PayHash: paymentHash, - PayAddr: &invoice.Terms.PaymentAddr, - AddIndex: addIndex, - SettleIndex: invoice.SettleIndex, - } - } - - // assertInvoiceCount asserts that the number of invoices equals - // to the passed count. - assertInvoiceCount := func(count int) { - // Query to collect all invoices. - query := invpkg.InvoiceQuery{ - IndexOffset: 0, - NumMaxInvoices: math.MaxUint64, - } - - // Check that we really have 3 invoices. - response, err := db.QueryInvoices(ctxb, query) - require.NoError(t, err) - require.Equal(t, count, len(response.Invoices)) - } - - // XOR one byte of one of the references' hash and attempt to delete. - invoicesToDelete[0].PayHash[2] ^= 3 - require.Error(t, db.DeleteInvoice(ctxb, invoicesToDelete)) - assertInvoiceCount(3) - - // Restore the hash. - invoicesToDelete[0].PayHash[2] ^= 3 - - // XOR the second invoice's payment settle index as it is settled, and - // attempt to delete. - invoicesToDelete[1].SettleIndex ^= 11 - require.Error(t, db.DeleteInvoice(ctxb, invoicesToDelete)) - assertInvoiceCount(3) - - // Restore the settle index. - invoicesToDelete[1].SettleIndex ^= 11 - - // XOR the add index for one of the references and attempt to delete. - invoicesToDelete[2].AddIndex ^= 13 - require.Error(t, db.DeleteInvoice(ctxb, invoicesToDelete)) - assertInvoiceCount(3) - - // Restore the add index. - invoicesToDelete[2].AddIndex ^= 13 - - // Delete should succeed with all the valid references. - require.NoError(t, db.DeleteInvoice(ctxb, invoicesToDelete)) - assertInvoiceCount(0) -} - -// TestDeleteCanceledInvoices tests that deleting canceled invoices with the -// specific DeleteCanceledInvoices method works correctly. -func TestDeleteCanceledInvoices(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") - - // Updatefunc is used to cancel an invoice. - updateFunc := func(invoice *invpkg.Invoice) ( - *invpkg.InvoiceUpdateDesc, error) { - - return &invpkg.InvoiceUpdateDesc{ - UpdateType: invpkg.CancelInvoiceUpdate, - State: &invpkg.InvoiceStateUpdateDesc{ - NewState: invpkg.ContractCanceled, - }, - }, nil - } - - // Add some invoices to the test db. - ctxb := context.Background() - var invoices []invpkg.Invoice - for i := 0; i < 10; i++ { - invoice, err := randInvoice(lnwire.MilliSatoshi(i + 1)) - require.NoError(t, err) - - paymentHash := invoice.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(ctxb, invoice, paymentHash) - require.NoError(t, err) - - // Cancel every second invoice. - if i%2 == 0 { - invoice, err = db.UpdateInvoice( - ctxb, invpkg.InvoiceRefByHash(paymentHash), nil, - updateFunc, - ) - require.NoError(t, err) - } else { - invoices = append(invoices, *invoice) - } - } - - // Delete canceled invoices. - require.NoError(t, db.DeleteCanceledInvoices(ctxb)) - - // Query to collect all invoices. - query := invpkg.InvoiceQuery{ - IndexOffset: 0, - NumMaxInvoices: math.MaxUint64, - } - - dbInvoices, err := db.QueryInvoices(ctxb, query) - require.NoError(t, err) - - // Check that we really have the expected invoices. - require.Equal(t, invoices, dbInvoices.Invoices) -} - -// TestAddInvoiceInvalidFeatureDeps asserts that inserting an invoice with -// invalid transitive feature dependencies fails with the appropriate error. -func TestAddInvoiceInvalidFeatureDeps(t *testing.T) { - t.Parallel() - - db, err := MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") - - invoice, err := randInvoice(500) - require.NoError(t, err) - - invoice.Terms.Features = lnwire.NewFeatureVector( - lnwire.NewRawFeatureVector( - lnwire.TLVOnionPayloadRequired, - lnwire.MPPOptional, - ), - lnwire.Features, - ) - - hash := invoice.Terms.PaymentPreimage.Hash() - _, err = db.AddInvoice(context.Background(), invoice, hash) - require.Error(t, err, feature.NewErrMissingFeatureDep( - lnwire.PaymentAddrOptional, - )) -} +var testNow = time.Unix(1, 0) // TestEncodeDecodeAmpInvoiceState asserts that the nested TLV // encoding+decoding for the AMPInvoiceState struct works as expected. diff --git a/invoices/invoices_test.go b/invoices/invoices_test.go new file mode 100644 index 0000000000..263563dfb3 --- /dev/null +++ b/invoices/invoices_test.go @@ -0,0 +1,2506 @@ +package invoices_test + +import ( + "context" + "crypto/rand" + "fmt" + "math" + "testing" + "time" + + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/channeldb/models" + "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/feature" + invpkg "github.com/lightningnetwork/lnd/invoices" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" + "github.com/stretchr/testify/require" +) + +var ( + emptyFeatures = lnwire.NewFeatureVector(nil, lnwire.Features) + + ampFeatures = lnwire.NewFeatureVector( + lnwire.NewRawFeatureVector( + lnwire.TLVOnionPayloadRequired, + lnwire.PaymentAddrOptional, + lnwire.AMPRequired, + ), + lnwire.Features, + ) + + testNow = time.Unix(1, 0) + + testClock = clock.NewTestClock(testNow) +) + +func randInvoice(value lnwire.MilliSatoshi) (*invpkg.Invoice, error) { + var ( + pre lntypes.Preimage + payAddr [32]byte + ) + if _, err := rand.Read(pre[:]); err != nil { + return nil, err + } + if _, err := rand.Read(payAddr[:]); err != nil { + return nil, err + } + + i := &invpkg.Invoice{ + CreationDate: testNow, + Terms: invpkg.ContractTerm{ + Expiry: 4000, + PaymentPreimage: &pre, + PaymentAddr: payAddr, + Value: value, + Features: emptyFeatures, + }, + Htlcs: map[models.CircuitKey]*invpkg.InvoiceHTLC{}, + AMPState: map[invpkg.SetID]invpkg.InvoiceStateAMP{}, + } + i.Memo = []byte("memo") + + // Create a random byte slice of MaxPaymentRequestSize bytes to be used + // as a dummy paymentrequest, and determine if it should be set based + // on one of the random bytes. + var r [invpkg.MaxPaymentRequestSize]byte + if _, err := rand.Read(r[:]); err != nil { + return nil, err + } + if r[0]&1 == 0 { + i.PaymentRequest = r[:] + } else { + i.PaymentRequest = []byte("") + } + + return i, nil +} + +// settleTestInvoice settles a test invoice. +func settleTestInvoice(invoice *invpkg.Invoice, settleIndex uint64) { + invoice.SettleDate = testNow + invoice.AmtPaid = invoice.Terms.Value + invoice.State = invpkg.ContractSettled + invoice.Htlcs[models.CircuitKey{}] = &invpkg.InvoiceHTLC{ + Amt: invoice.Terms.Value, + AcceptTime: testNow, + ResolveTime: testNow, + State: invpkg.HtlcStateSettled, + CustomRecords: make(record.CustomSet), + } + invoice.SettleIndex = settleIndex +} + +// Tests that pending invoices are those which are either in ContractOpen or +// in ContractAccepted state. +func TestInvoiceIsPending(t *testing.T) { + contractStates := []invpkg.ContractState{ + invpkg.ContractOpen, invpkg.ContractSettled, + invpkg.ContractCanceled, invpkg.ContractAccepted, + } + + for _, state := range contractStates { + invoice := invpkg.Invoice{ + State: state, + } + + // We expect that an invoice is pending if it's either in + // ContractOpen or ContractAccepted state. + open := invpkg.ContractOpen + accepted := invpkg.ContractAccepted + pending := (state == open || state == accepted) + + require.Equal(t, pending, invoice.IsPending()) + } +} + +type invWorkflowTest struct { + name string + queryPayHash bool + queryPayAddr bool +} + +var invWorkflowTests = []invWorkflowTest{ + { + name: "unknown", + queryPayHash: false, + queryPayAddr: false, + }, + { + name: "only payhash known", + queryPayHash: true, + queryPayAddr: false, + }, + { + name: "payaddr and payhash known", + queryPayHash: true, + queryPayAddr: true, + }, +} + +// TestInvoiceWorkflow asserts the basic process of inserting, fetching, and +// updating an invoice. We assert that the flow is successful using when +// querying with various combinations of payment hash and payment address. +func TestInvoiceWorkflow(t *testing.T) { + t.Parallel() + + for _, test := range invWorkflowTests { + test := test + t.Run(test.name, func(t *testing.T) { + testInvoiceWorkflow(t, test) + }) + } +} + +func testInvoiceWorkflow(t *testing.T, test invWorkflowTest) { + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err, "unable to make test db") + + // Create a fake invoice which we'll use several times in the tests + // below. + fakeInvoice, err := randInvoice(10000) + require.NoError(t, err, "unable to create invoice") + invPayHash := fakeInvoice.Terms.PaymentPreimage.Hash() + + // Select the payment hash and payment address we will use to lookup or + // update the invoice for the remainder of the test. + var ( + payHash lntypes.Hash + payAddr *[32]byte + ref invpkg.InvoiceRef + ) + switch { + case test.queryPayHash && test.queryPayAddr: + payHash = invPayHash + payAddr = &fakeInvoice.Terms.PaymentAddr + ref = invpkg.InvoiceRefByHashAndAddr(payHash, *payAddr) + case test.queryPayHash: + payHash = invPayHash + ref = invpkg.InvoiceRefByHash(payHash) + } + + ctxb := context.Background() + // Add the invoice to the database, this should succeed as there aren't + // any existing invoices within the database with the same payment + // hash. + if _, err := db.AddInvoice(ctxb, fakeInvoice, invPayHash); err != nil { + t.Fatalf("unable to find invoice: %v", err) + } + + // Attempt to retrieve the invoice which was just added to the + // database. It should be found, and the invoice returned should be + // identical to the one created above. + dbInvoice, err := db.LookupInvoice(ctxb, ref) + if !test.queryPayAddr && !test.queryPayHash { + require.ErrorIs(t, err, invpkg.ErrInvoiceNotFound) + return + } + + require.Equal(t, + *fakeInvoice, dbInvoice, + "invoice fetched from db doesn't match original", + ) + + // The add index of the invoice retrieved from the database should now + // be fully populated. As this is the first index written to the DB, + // the addIndex should be 1. + if dbInvoice.AddIndex != 1 { + t.Fatalf("wrong add index: expected %v, got %v", 1, + dbInvoice.AddIndex) + } + + // Settle the invoice, the version retrieved from the database should + // now have the settled bit toggle to true and a non-default + // SettledDate + payAmt := fakeInvoice.Terms.Value * 2 + _, err = db.UpdateInvoice(ctxb, ref, nil, getUpdateInvoice(payAmt)) + require.NoError(t, err, "unable to settle invoice") + dbInvoice2, err := db.LookupInvoice(ctxb, ref) + require.NoError(t, err, "unable to fetch invoice") + if dbInvoice2.State != invpkg.ContractSettled { + t.Fatalf("invoice should now be settled but isn't") + } + if dbInvoice2.SettleDate.IsZero() { + t.Fatalf("invoice should have non-zero SettledDate but isn't") + } + + // Our 2x payment should be reflected, and also the settle index of 1 + // should also have been committed for this index. + if dbInvoice2.AmtPaid != payAmt { + t.Fatalf("wrong amt paid: expected %v, got %v", payAmt, + dbInvoice2.AmtPaid) + } + if dbInvoice2.SettleIndex != 1 { + t.Fatalf("wrong settle index: expected %v, got %v", 1, + dbInvoice2.SettleIndex) + } + + // Attempt to insert generated above again, this should fail as + // duplicates are rejected by the processing logic. + _, err = db.AddInvoice(ctxb, fakeInvoice, payHash) + require.ErrorIs(t, err, invpkg.ErrDuplicateInvoice) + + // Attempt to look up a non-existent invoice, this should also fail but + // with a "not found" error. + var fakeHash [32]byte + fakeRef := invpkg.InvoiceRefByHash(fakeHash) + _, err = db.LookupInvoice(ctxb, fakeRef) + require.ErrorIs(t, err, invpkg.ErrInvoiceNotFound) + + // Add 10 random invoices. + const numInvoices = 10 + amt := lnwire.NewMSatFromSatoshis(1000) + invoices := make([]*invpkg.Invoice, numInvoices+1) + invoices[0] = &dbInvoice2 + for i := 1; i < len(invoices); i++ { + invoice, err := randInvoice(amt) + if err != nil { + t.Fatalf("unable to create invoice: %v", err) + } + + hash := invoice.Terms.PaymentPreimage.Hash() + if _, err := db.AddInvoice(ctxb, invoice, hash); err != nil { + t.Fatalf("unable to add invoice %v", err) + } + + invoices[i] = invoice + } + + // Perform a scan to collect all the active invoices. + query := invpkg.InvoiceQuery{ + IndexOffset: 0, + NumMaxInvoices: math.MaxUint64, + PendingOnly: false, + } + + response, err := db.QueryInvoices(ctxb, query) + require.NoError(t, err, "invoice query failed") + + // The retrieve list of invoices should be identical as since we're + // using big endian, the invoices should be retrieved in ascending + // order (and the primary key should be incremented with each + // insertion). + for i := 0; i < len(invoices); i++ { + require.Equal(t, + *invoices[i], response.Invoices[i], + "retrieved invoice doesn't match", + ) + } +} + +// TestAddDuplicatePayAddr asserts that the payment addresses of inserted +// invoices are unique. +func TestAddDuplicatePayAddr(t *testing.T) { + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err) + + // Create two invoices with the same payment addr. + invoice1, err := randInvoice(1000) + require.NoError(t, err) + + invoice2, err := randInvoice(20000) + require.NoError(t, err) + invoice2.Terms.PaymentAddr = invoice1.Terms.PaymentAddr + + ctxb := context.Background() + + // First insert should succeed. + inv1Hash := invoice1.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice1, inv1Hash) + require.NoError(t, err) + + // Second insert should fail with duplicate payment addr. + inv2Hash := invoice2.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice2, inv2Hash) + require.Error(t, err, invpkg.ErrDuplicatePayAddr) +} + +// TestAddDuplicateKeysendPayAddr asserts that we permit duplicate payment +// addresses to be inserted if they are blank to support JIT legacy keysend +// invoices. +func TestAddDuplicateKeysendPayAddr(t *testing.T) { + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err) + + // Create two invoices with the same _blank_ payment addr. + invoice1, err := randInvoice(1000) + require.NoError(t, err) + invoice1.Terms.PaymentAddr = invpkg.BlankPayAddr + + invoice2, err := randInvoice(20000) + require.NoError(t, err) + invoice2.Terms.PaymentAddr = invpkg.BlankPayAddr + + ctxb := context.Background() + + // Inserting both should succeed without a duplicate payment address + // failure. + inv1Hash := invoice1.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice1, inv1Hash) + require.NoError(t, err) + + inv2Hash := invoice2.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice2, inv2Hash) + require.NoError(t, err) + + // Querying for each should succeed. Here we use hash+addr refs since + // the lookup will fail if the hash and addr point to different + // invoices, so if both succeed we can be assured they aren't included + // in the payment address index. + ref1 := invpkg.InvoiceRefByHashAndAddr(inv1Hash, invpkg.BlankPayAddr) + dbInv1, err := db.LookupInvoice(ctxb, ref1) + require.NoError(t, err) + require.Equal(t, invoice1, &dbInv1) + + ref2 := invpkg.InvoiceRefByHashAndAddr(inv2Hash, invpkg.BlankPayAddr) + dbInv2, err := db.LookupInvoice(ctxb, ref2) + require.NoError(t, err) + require.Equal(t, invoice2, &dbInv2) +} + +// TestFailInvoiceLookupMPPPayAddrOnly asserts that looking up a MPP invoice +// that matches _only_ by payment address fails with ErrInvoiceNotFound. This +// ensures that the HTLC's payment hash always matches the payment hash in the +// returned invoice. +func TestFailInvoiceLookupMPPPayAddrOnly(t *testing.T) { + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err) + + // Create and insert a random invoice. + invoice, err := randInvoice(1000) + require.NoError(t, err) + + payHash := invoice.Terms.PaymentPreimage.Hash() + payAddr := invoice.Terms.PaymentAddr + + ctxb := context.Background() + _, err = db.AddInvoice(ctxb, invoice, payHash) + require.NoError(t, err) + + // Modify the queried payment hash to be invalid. + payHash[0] ^= 0x01 + + // Lookup the invoice by (invalid) payment hash and payment address. The + // lookup should fail since we require the payment hash to match for + // legacy/MPP invoices, as this guarantees that the preimage is valid + // for the given HTLC. + ref := invpkg.InvoiceRefByHashAndAddr(payHash, payAddr) + _, err = db.LookupInvoice(ctxb, ref) + require.Equal(t, invpkg.ErrInvoiceNotFound, err) +} + +// TestInvRefEquivocation asserts that retrieving or updating an invoice using +// an equivocating InvoiceRef results in ErrInvRefEquivocation. +func TestInvRefEquivocation(t *testing.T) { + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err) + + // Add two random invoices. + invoice1, err := randInvoice(1000) + require.NoError(t, err) + + ctxb := context.Background() + inv1Hash := invoice1.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice1, inv1Hash) + require.NoError(t, err) + + invoice2, err := randInvoice(2000) + require.NoError(t, err) + + inv2Hash := invoice2.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice2, inv2Hash) + require.NoError(t, err) + + // Now, query using invoice 1's payment address, but invoice 2's payment + // hash. We expect an error since the invref points to multiple + // invoices. + ref := invpkg.InvoiceRefByHashAndAddr( + inv2Hash, invoice1.Terms.PaymentAddr, + ) + _, err = db.LookupInvoice(ctxb, ref) + require.Error(t, err, invpkg.ErrInvRefEquivocation) + + // The same error should be returned when updating an equivocating + // reference. + nop := func(_ *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { + return nil, nil + } + _, err = db.UpdateInvoice(ctxb, ref, nil, nop) + require.Error(t, err, invpkg.ErrInvRefEquivocation) +} + +// TestInvoiceCancelSingleHtlc tests that a single htlc can be canceled on the +// invoice. +func TestInvoiceCancelSingleHtlc(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err, "unable to make test db") + + preimage := lntypes.Preimage{1} + paymentHash := preimage.Hash() + + testInvoice := &invpkg.Invoice{ + Htlcs: map[models.CircuitKey]*invpkg.InvoiceHTLC{}, + Terms: invpkg.ContractTerm{ + Value: lnwire.NewMSatFromSatoshis(10000), + Features: emptyFeatures, + PaymentPreimage: &preimage, + }, + } + + ctxb := context.Background() + if _, err := db.AddInvoice(ctxb, testInvoice, paymentHash); err != nil { + t.Fatalf("unable to find invoice: %v", err) + } + + // Accept an htlc on this invoice. + key := models.CircuitKey{ + ChanID: lnwire.NewShortChanIDFromInt(1), + HtlcID: 4, + } + htlc := invpkg.HtlcAcceptDesc{ + Amt: 500, + CustomRecords: make(record.CustomSet), + } + + callback := func( + invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { + + htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ + key: &htlc, + } + + return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.AddHTLCsUpdate, + AddHtlcs: htlcs, + }, nil + } + + ref := invpkg.InvoiceRefByHash(paymentHash) + invoice, err := db.UpdateInvoice(ctxb, ref, nil, callback) + require.NoError(t, err, "unable to add invoice htlc") + if len(invoice.Htlcs) != 1 { + t.Fatalf("expected the htlc to be added") + } + if invoice.Htlcs[key].State != invpkg.HtlcStateAccepted { + t.Fatalf("expected htlc in state accepted") + } + + callback = func( + invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { + + return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelHTLCsUpdate, + CancelHtlcs: map[models.CircuitKey]struct{}{ + key: {}, + }, + }, nil + } + + // Cancel the htlc again. + invoice, err = db.UpdateInvoice(ctxb, ref, nil, callback) + require.NoError(t, err, "unable to cancel htlc") + if len(invoice.Htlcs) != 1 { + t.Fatalf("expected the htlc to be present") + } + if invoice.Htlcs[key].State != invpkg.HtlcStateCanceled { + t.Fatalf("expected htlc in state canceled") + } +} + +// TestInvoiceCancelSingleHtlcAMP tests that it's possible to cancel a single +// invoice of an AMP HTLC across multiple set IDs, and also have that update +// the amount paid and other related fields as well. +func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + require.NoError(t, err, "unable to make test db: %v", err) + + // We'll start out by creating an invoice and writing it to the DB. + amt := lnwire.NewMSatFromSatoshis(1000) + invoice, err := randInvoice(amt) + require.Nil(t, err) + + // Set AMP-specific features so that we can settle with HTLC-level + // preimages. + invoice.Terms.Features = ampFeatures + + ctxb := context.Background() + preimage := *invoice.Terms.PaymentPreimage + payHash := preimage.Hash() + _, err = db.AddInvoice(ctxb, invoice, payHash) + require.Nil(t, err) + + // Add two HTLC sets, one with one HTLC and the other with two. + setID1 := &[32]byte{1} + setID2 := &[32]byte{2} + + ref := invpkg.InvoiceRefByHashAndAddr( + payHash, invoice.Terms.PaymentAddr, + ) + + // The first set ID with a single HTLC added. + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID1), + updateAcceptAMPHtlc(0, amt, setID1, true), + ) + require.Nil(t, err) + + // The second set ID with two HTLCs added. + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID2), + updateAcceptAMPHtlc(1, amt, setID2, true), + ) + require.Nil(t, err) + dbInvoice, err := db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID2), + updateAcceptAMPHtlc(2, amt, setID2, true), + ) + require.Nil(t, err) + + // At this point, we should detect that 3k satoshis total has been + // paid. + require.Equal(t, dbInvoice.AmtPaid, amt*3) + + callback := func( + invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { + + return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelHTLCsUpdate, + CancelHtlcs: map[models.CircuitKey]struct{}{ + {HtlcID: 0}: {}, + }, + SetID: (*invpkg.SetID)(setID1), + }, nil + } + + // Now we'll cancel a single invoice, and assert that the amount paid + // is decremented, and the state for that HTLC set reflects that is + // been cancelled. + _, err = db.UpdateInvoice(ctxb, ref, (*invpkg.SetID)(setID1), callback) + require.NoError(t, err, "unable to cancel htlc") + + freshInvoice, err := db.LookupInvoice(ctxb, ref) + require.Nil(t, err) + dbInvoice = &freshInvoice + + // The amount paid should reflect that an invoice was cancelled. + require.Equal(t, dbInvoice.AmtPaid, amt*2) + + // The HTLC and AMP state should also show that only one HTLC set is + // left. + invoice.State = invpkg.ContractOpen + invoice.AmtPaid = 2 * amt + invoice.SettleDate = dbInvoice.SettleDate + + htlc0 := models.CircuitKey{HtlcID: 0} + htlc1 := models.CircuitKey{HtlcID: 1} + htlc2 := models.CircuitKey{HtlcID: 2} + + invoice.Htlcs = map[models.CircuitKey]*invpkg.InvoiceHTLC{ + htlc0: makeAMPInvoiceHTLC(amt, *setID1, payHash, &preimage), + htlc1: makeAMPInvoiceHTLC(amt, *setID2, payHash, &preimage), + htlc2: makeAMPInvoiceHTLC(amt, *setID2, payHash, &preimage), + } + invoice.AMPState[*setID1] = invpkg.InvoiceStateAMP{ + State: invpkg.HtlcStateCanceled, + InvoiceKeys: map[models.CircuitKey]struct{}{ + {HtlcID: 0}: {}, + }, + } + invoice.AMPState[*setID2] = invpkg.InvoiceStateAMP{ + State: invpkg.HtlcStateAccepted, + AmtPaid: amt * 2, + InvoiceKeys: map[models.CircuitKey]struct{}{ + {HtlcID: 1}: {}, + {HtlcID: 2}: {}, + }, + } + + invoice.Htlcs[htlc0].State = invpkg.HtlcStateCanceled + invoice.Htlcs[htlc0].ResolveTime = time.Unix(1, 0) + + require.Equal(t, invoice, dbInvoice) + + // Next, we'll cancel the _other_ HTLCs active, but we'll do them one + // by one. + _, err = db.UpdateInvoice(ctxb, ref, (*invpkg.SetID)(setID2), + func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, + error) { + + return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelHTLCsUpdate, + CancelHtlcs: map[models.CircuitKey]struct{}{ + {HtlcID: 1}: {}, + }, + SetID: (*invpkg.SetID)(setID2), + }, nil + }) + require.NoError(t, err, "unable to cancel htlc") + + freshInvoice, err = db.LookupInvoice(ctxb, ref) + require.Nil(t, err) + dbInvoice = &freshInvoice + + invoice.Htlcs[htlc1].State = invpkg.HtlcStateCanceled + invoice.Htlcs[htlc1].ResolveTime = time.Unix(1, 0) + invoice.AmtPaid = amt + + ampState := invoice.AMPState[*setID2] + ampState.State = invpkg.HtlcStateCanceled + ampState.AmtPaid = amt + invoice.AMPState[*setID2] = ampState + + require.Equal(t, invoice, dbInvoice) + + callback = func( + invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { + + return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelHTLCsUpdate, + CancelHtlcs: map[models.CircuitKey]struct{}{ + {HtlcID: 2}: {}, + }, + SetID: (*invpkg.SetID)(setID2), + }, nil + } + + // Now we'll cancel the final HTLC, which should cause all the active + // HTLCs to transition to the cancelled state. + _, err = db.UpdateInvoice(ctxb, ref, (*invpkg.SetID)(setID2), callback) + require.NoError(t, err, "unable to cancel htlc") + + freshInvoice, err = db.LookupInvoice(ctxb, ref) + require.Nil(t, err) + dbInvoice = &freshInvoice + + ampState = invoice.AMPState[*setID2] + ampState.AmtPaid = 0 + invoice.AMPState[*setID2] = ampState + + invoice.Htlcs[htlc2].State = invpkg.HtlcStateCanceled + invoice.Htlcs[htlc2].ResolveTime = time.Unix(1, 0) + invoice.AmtPaid = 0 + + require.Equal(t, invoice, dbInvoice) +} + +// TestInvoiceTimeSeries tests that newly added invoices invoices, as well as +// settled invoices are added to the database are properly placed in the add +// add or settle index which serves as an event time series. +func TestInvoiceAddTimeSeries(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + require.NoError(t, err, "unable to make test db") + + ctxb := context.Background() + _, err = db.InvoicesAddedSince(ctxb, 0) + require.NoError(t, err) + + // We'll start off by creating 20 random invoices, and inserting them + // into the database. + const numInvoices = 20 + amt := lnwire.NewMSatFromSatoshis(1000) + invoices := make([]invpkg.Invoice, numInvoices) + for i := 0; i < len(invoices); i++ { + invoice, err := randInvoice(amt) + if err != nil { + t.Fatalf("unable to create invoice: %v", err) + } + + paymentHash := invoice.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice, paymentHash) + if err != nil { + t.Fatalf("unable to add invoice %v", err) + } + + invoices[i] = *invoice + } + + // With the invoices constructed, we'll now create a series of queries + // that we'll use to assert expected return values of + // InvoicesAddedSince. + addQueries := []struct { + sinceAddIndex uint64 + + resp []invpkg.Invoice + }{ + // If we specify a value of zero, we shouldn't get any invoices + // back. + { + sinceAddIndex: 0, + }, + + // If we specify a value well beyond the number of inserted + // invoices, we shouldn't get any invoices back. + { + sinceAddIndex: 99999999, + }, + + // Using an index of 1 should result in all values, but the + // first one being returned. + { + sinceAddIndex: 1, + resp: invoices[1:], + }, + + // If we use an index of 10, then we should retrieve the + // reaming 10 invoices. + { + sinceAddIndex: 10, + resp: invoices[10:], + }, + } + + for i, query := range addQueries { + resp, err := db.InvoicesAddedSince(ctxb, query.sinceAddIndex) + if err != nil { + t.Fatalf("unable to query: %v", err) + } + + require.Equal(t, len(query.resp), len(resp)) + + for j := 0; j < len(query.resp); j++ { + require.Equal(t, + query.resp[j], resp[j], + fmt.Sprintf("test: #%v, item: #%v", i, j), + ) + } + } + + _, err = db.InvoicesSettledSince(ctxb, 0) + require.NoError(t, err) + + var settledInvoices []invpkg.Invoice + var settleIndex uint64 = 1 + // We'll now only settle the latter half of each of those invoices. + for i := 10; i < len(invoices); i++ { + invoice := &invoices[i] + + paymentHash := invoice.Terms.PaymentPreimage.Hash() + + ref := invpkg.InvoiceRefByHash(paymentHash) + _, err := db.UpdateInvoice( + ctxb, ref, nil, getUpdateInvoice(invoice.Terms.Value), + ) + if err != nil { + t.Fatalf("unable to settle invoice: %v", err) + } + + // Create the settled invoice for the expectation set. + settleTestInvoice(invoice, settleIndex) + settleIndex++ + + settledInvoices = append(settledInvoices, *invoice) + } + + // We'll now prepare an additional set of queries to ensure the settle + // time series has properly been maintained in the database. + settleQueries := []struct { + sinceSettleIndex uint64 + + resp []invpkg.Invoice + }{ + // If we specify a value of zero, we shouldn't get any settled + // invoices back. + { + sinceSettleIndex: 0, + }, + + // If we specify a value well beyond the number of settled + // invoices, we shouldn't get any invoices back. + { + sinceSettleIndex: 99999999, + }, + + // Using an index of 1 should result in the final 10 invoices + // being returned, as we only settled those. + { + sinceSettleIndex: 1, + resp: settledInvoices[1:], + }, + } + + for i, query := range settleQueries { + resp, err := db.InvoicesSettledSince( + ctxb, query.sinceSettleIndex, + ) + if err != nil { + t.Fatalf("unable to query: %v", err) + } + + require.Equal(t, len(query.resp), len(resp)) + + for j := 0; j < len(query.resp); j++ { + require.Equal(t, + query.resp[j], resp[j], + fmt.Sprintf("test: #%v, item: #%v", i, j), + ) + } + } +} + +// TestSettleIndexAmpPayments tests that repeated settles of the same invoice +// end up properly adding entries to the settle index, and the +// InvoicesSettledSince will emit a "projected" version of the invoice w/ +// _just_ that HTLC information. +func TestSettleIndexAmpPayments(t *testing.T) { + t.Parallel() + + testClock := clock.NewTestClock(testNow) + db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + require.Nil(t, err) + + // First, we'll make a sample invoice that'll be paid to several times + // below. + amt := lnwire.NewMSatFromSatoshis(1000) + testInvoice, err := randInvoice(amt) + require.Nil(t, err) + testInvoice.Terms.Features = ampFeatures + + // Add the invoice to the DB, we use a dummy payment hash here but the + // invoice will have a valid payment address set. + ctxb := context.Background() + preimage := *testInvoice.Terms.PaymentPreimage + payHash := preimage.Hash() + _, err = db.AddInvoice(ctxb, testInvoice, payHash) + require.Nil(t, err) + + // Now that we have the invoice, we'll simulate 3 different HTLC sets + // being attached to the invoice. These represent 3 different + // concurrent payments. + setID1 := &[32]byte{1} + setID2 := &[32]byte{2} + setID3 := &[32]byte{3} + + ref := invpkg.InvoiceRefByHashAndAddr( + payHash, testInvoice.Terms.PaymentAddr, + ) + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID1), + updateAcceptAMPHtlc(1, amt, setID1, true), + ) + require.Nil(t, err) + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID2), + updateAcceptAMPHtlc(2, amt, setID2, true), + ) + require.Nil(t, err) + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID3), + updateAcceptAMPHtlc(3, amt, setID3, true), + ) + require.Nil(t, err) + + // Now that the invoices have been accepted, we'll exercise the + // behavior of the LookupInvoice call that allows us to modify exactly + // how we query for invoices. + // + // First, we'll query for the invoice with just the payment addr, but + // specify no HTLcs are to be included. + refNoHtlcs := invpkg.InvoiceRefByAddrBlankHtlc( + testInvoice.Terms.PaymentAddr, + ) + invoiceNoHTLCs, err := db.LookupInvoice(ctxb, refNoHtlcs) + require.Nil(t, err) + + require.Equal(t, 0, len(invoiceNoHTLCs.Htlcs)) + + // We'll now look up the HTLCs based on the individual setIDs added + // above. + for i, setID := range []*[32]byte{setID1, setID2, setID3} { + refFiltered := invpkg.InvoiceRefBySetIDFiltered(*setID) + invoiceFiltered, err := db.LookupInvoice(ctxb, refFiltered) + require.Nil(t, err) + + // Only a single HTLC should be present. + require.Equal(t, 1, len(invoiceFiltered.Htlcs)) + + // The set ID for the HTLC should match the queried set ID. + key := models.CircuitKey{HtlcID: uint64(i + 1)} + htlc := invoiceFiltered.Htlcs[key] + require.Equal(t, *setID, htlc.AMP.Record.SetID()) + + // The HTLC should show that it's in the accepted state. + require.Equal(t, htlc.State, invpkg.HtlcStateAccepted) + } + + // Now that we know the invoices are in the proper state, we'll settle + // them on by one in distinct updates. + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID1), + getUpdateInvoiceAMPSettle( + setID1, preimage, models.CircuitKey{HtlcID: 1}, + ), + ) + require.Nil(t, err) + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID2), + getUpdateInvoiceAMPSettle( + setID2, preimage, models.CircuitKey{HtlcID: 2}, + ), + ) + require.Nil(t, err) + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID3), + getUpdateInvoiceAMPSettle( + setID3, preimage, models.CircuitKey{HtlcID: 3}, + ), + ) + require.Nil(t, err) + + // Now that all the invoices have been settled, we'll ensure that the + // settle index was updated properly by obtaining all the currently + // settled invoices in the time series. We use a value of 1 here to + // ensure we get _all_ the invoices back. + settledInvoices, err := db.InvoicesSettledSince(ctxb, 1) + require.Nil(t, err) + + // To get around the settle index quirk, we'll fetch the very first + // invoice in the HTLC filtered mode and append it to the set of + // invoices. + firstInvoice, err := db.LookupInvoice( + ctxb, invpkg.InvoiceRefBySetIDFiltered(*setID1), + ) + require.Nil(t, err) + settledInvoices = append( + []invpkg.Invoice{firstInvoice}, settledInvoices..., + ) + + // There should be 3 invoices settled, as we created 3 "sub-invoices" + // above. + numInvoices := 3 + require.Equal(t, numInvoices, len(settledInvoices)) + + // Each invoice should match the set of invoices we settled above, and + // the AMPState should be set accordingly. + for i, settledInvoice := range settledInvoices { + // Only one HTLC should be projected for this settled index. + require.Equal(t, 1, len(settledInvoice.Htlcs)) + + // The invoice should show up as settled, and match the settle + // index increment. + invSetID := &[32]byte{byte(i + 1)} + subInvoiceState, ok := settledInvoice.AMPState[*invSetID] + require.True(t, ok) + + require.Equal(t, subInvoiceState.State, invpkg.HtlcStateSettled) + require.Equal(t, int(subInvoiceState.SettleIndex), i+1) + + invoiceKey := models.CircuitKey{HtlcID: uint64(i + 1)} + _, keyFound := subInvoiceState.InvoiceKeys[invoiceKey] + require.True(t, keyFound) + } + + // If we attempt to look up the invoice by the payment addr, with all + // the HTLCs, the main invoice should have 3 HTLCs present. + refWithHtlcs := invpkg.InvoiceRefByAddr(testInvoice.Terms.PaymentAddr) + invoiceWithHTLCs, err := db.LookupInvoice(ctxb, refWithHtlcs) + require.Nil(t, err) + require.Equal(t, numInvoices, len(invoiceWithHTLCs.Htlcs)) + + // Finally, delete the invoice. If we query again, then nothing should + // be found. + err = db.DeleteInvoice(ctxb, []invpkg.InvoiceDeleteRef{ + { + PayHash: payHash, + PayAddr: &testInvoice.Terms.PaymentAddr, + AddIndex: testInvoice.AddIndex, + }, + }) + require.Nil(t, err) +} + +// TestFetchPendingInvoices tests that we can fetch all pending invoices from +// the database using the FetchPendingInvoices method. +func TestFetchPendingInvoices(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + require.NoError(t, err, "unable to make test db") + + ctxb := context.Background() + + // Make sure that fetching pending invoices from an empty database + // returns an empty result and no errors. + pending, err := db.FetchPendingInvoices(ctxb) + require.NoError(t, err) + require.Empty(t, pending) + + const numInvoices = 20 + var settleIndex uint64 = 1 + pendingInvoices := make(map[lntypes.Hash]invpkg.Invoice) + + for i := 1; i <= numInvoices; i++ { + amt := lnwire.MilliSatoshi(i * 1000) + invoice, err := randInvoice(amt) + require.NoError(t, err) + + invoice.CreationDate = invoice.CreationDate.Add( + time.Duration(i-1) * time.Second, + ) + + paymentHash := invoice.Terms.PaymentPreimage.Hash() + + _, err = db.AddInvoice(ctxb, invoice, paymentHash) + require.NoError(t, err) + + // Settle every second invoice. + if i%2 == 0 { + pendingInvoices[paymentHash] = *invoice + continue + } + + ref := invpkg.InvoiceRefByHash(paymentHash) + _, err = db.UpdateInvoice(ctxb, ref, nil, getUpdateInvoice(amt)) + require.NoError(t, err) + + settleTestInvoice(invoice, settleIndex) + settleIndex++ + } + + // Fetch all pending invoices. + pending, err = db.FetchPendingInvoices(ctxb) + require.NoError(t, err) + require.Equal(t, pendingInvoices, pending) +} + +// TestDuplicateSettleInvoice tests that if we add a new invoice and settle it +// twice, then the second time we also receive the invoice that we settled as a +// return argument. +func TestDuplicateSettleInvoice(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + require.NoError(t, err, "unable to make test db") + + // We'll start out by creating an invoice and writing it to the DB. + amt := lnwire.NewMSatFromSatoshis(1000) + invoice, err := randInvoice(amt) + require.NoError(t, err, "unable to create invoice") + + payHash := invoice.Terms.PaymentPreimage.Hash() + + ctxb := context.Background() + if _, err := db.AddInvoice(ctxb, invoice, payHash); err != nil { + t.Fatalf("unable to add invoice %v", err) + } + + // With the invoice in the DB, we'll now attempt to settle the invoice. + ref := invpkg.InvoiceRefByHash(payHash) + dbInvoice, err := db.UpdateInvoice( + ctxb, ref, nil, getUpdateInvoice(amt), + ) + require.NoError(t, err, "unable to settle invoice") + + // We'll update what we expect the settle invoice to be so that our + // comparison below has the correct assumption. + invoice.SettleIndex = 1 + invoice.State = invpkg.ContractSettled + invoice.AmtPaid = amt + invoice.SettleDate = dbInvoice.SettleDate + invoice.Htlcs = map[models.CircuitKey]*invpkg.InvoiceHTLC{ + {}: { + Amt: amt, + AcceptTime: time.Unix(1, 0), + ResolveTime: time.Unix(1, 0), + State: invpkg.HtlcStateSettled, + CustomRecords: make(record.CustomSet), + }, + } + + // We should get back the exact same invoice that we just inserted. + require.Equal(t, invoice, dbInvoice, "wrong invoice after settle") + + // If we try to settle the invoice again, then we should get the very + // same invoice back, but with an error this time. + dbInvoice, err = db.UpdateInvoice(ctxb, ref, nil, getUpdateInvoice(amt)) + require.ErrorIs(t, err, invpkg.ErrInvoiceAlreadySettled) + + if dbInvoice == nil { + t.Fatalf("invoice from db is nil after settle!") + } + + invoice.SettleDate = dbInvoice.SettleDate + require.Equal( + t, invoice, dbInvoice, "wrong invoice after second settle", + ) +} + +// TestQueryInvoices ensures that we can properly query the invoice database for +// invoices using different types of queries. +func TestQueryInvoices(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + require.NoError(t, err, "unable to make test db") + + // To begin the test, we'll add 50 invoices to the database. We'll + // assume that the index of the invoice within the database is the same + // as the amount of the invoice itself. + const numInvoices = 50 + var settleIndex uint64 = 1 + var invoices []invpkg.Invoice + var pendingInvoices []invpkg.Invoice + + ctxb := context.Background() + for i := 1; i <= numInvoices; i++ { + amt := lnwire.MilliSatoshi(i) + invoice, err := randInvoice(amt) + invoice.CreationDate = invoice.CreationDate.Add( + time.Duration(i-1) * time.Second, + ) + if err != nil { + t.Fatalf("unable to create invoice: %v", err) + } + + paymentHash := invoice.Terms.PaymentPreimage.Hash() + + if _, err := db.AddInvoice( + ctxb, invoice, paymentHash, + ); err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + + // We'll only settle half of all invoices created. + if i%2 == 0 { + ref := invpkg.InvoiceRefByHash(paymentHash) + _, err := db.UpdateInvoice( + ctxb, ref, nil, getUpdateInvoice(amt), + ) + if err != nil { + t.Fatalf("unable to settle invoice: %v", err) + } + + // Create the settled invoice for the expectation set. + settleTestInvoice(invoice, settleIndex) + settleIndex++ + } else { + pendingInvoices = append(pendingInvoices, *invoice) + } + + invoices = append(invoices, *invoice) + } + + // The test will consist of several queries along with their respective + // expected response. Each query response should match its expected one. + testCases := []struct { + query invpkg.InvoiceQuery + expected []invpkg.Invoice + }{ + // Fetch all invoices with a single query. + { + query: invpkg.InvoiceQuery{ + NumMaxInvoices: numInvoices, + }, + expected: invoices, + }, + // Fetch all invoices with a single query, reversed. + { + query: invpkg.InvoiceQuery{ + Reversed: true, + NumMaxInvoices: numInvoices, + }, + expected: invoices, + }, + // Fetch the first 25 invoices. + { + query: invpkg.InvoiceQuery{ + NumMaxInvoices: numInvoices / 2, + }, + expected: invoices[:numInvoices/2], + }, + // Fetch the first 10 invoices, but this time iterating + // backwards. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 11, + Reversed: true, + NumMaxInvoices: numInvoices, + }, + expected: invoices[:10], + }, + // Fetch the last 40 invoices. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 10, + NumMaxInvoices: numInvoices, + }, + expected: invoices[10:], + }, + // Fetch all but the first invoice. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 1, + NumMaxInvoices: numInvoices, + }, + expected: invoices[1:], + }, + // Fetch one invoice, reversed, with index offset 3. This + // should give us the second invoice in the array. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 3, + Reversed: true, + NumMaxInvoices: 1, + }, + expected: invoices[1:2], + }, + // Same as above, at index 2. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 2, + Reversed: true, + NumMaxInvoices: 1, + }, + expected: invoices[0:1], + }, + // Fetch one invoice, at index 1, reversed. Since invoice#1 is + // the very first, there won't be any left in a reverse search, + // so we expect no invoices to be returned. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 1, + Reversed: true, + NumMaxInvoices: 1, + }, + expected: nil, + }, + // Same as above, but don't restrict the number of invoices to + // 1. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 1, + Reversed: true, + NumMaxInvoices: numInvoices, + }, + expected: nil, + }, + // Fetch one invoice, reversed, with no offset set. We expect + // the last invoice in the response. + { + query: invpkg.InvoiceQuery{ + Reversed: true, + NumMaxInvoices: 1, + }, + expected: invoices[numInvoices-1:], + }, + // Fetch one invoice, reversed, the offset set at numInvoices+1. + // We expect this to return the last invoice. + { + query: invpkg.InvoiceQuery{ + IndexOffset: numInvoices + 1, + Reversed: true, + NumMaxInvoices: 1, + }, + expected: invoices[numInvoices-1:], + }, + // Same as above, at offset numInvoices. + { + query: invpkg.InvoiceQuery{ + IndexOffset: numInvoices, + Reversed: true, + NumMaxInvoices: 1, + }, + expected: invoices[numInvoices-2 : numInvoices-1], + }, + // Fetch one invoice, at no offset (same as offset 0). We + // expect the first invoice only in the response. + { + query: invpkg.InvoiceQuery{ + NumMaxInvoices: 1, + }, + expected: invoices[:1], + }, + // Same as above, at offset 1. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 1, + NumMaxInvoices: 1, + }, + expected: invoices[1:2], + }, + // Same as above, at offset 2. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 2, + NumMaxInvoices: 1, + }, + expected: invoices[2:3], + }, + // Same as above, at offset numInvoices-1. Expect the last + // invoice to be returned. + { + query: invpkg.InvoiceQuery{ + IndexOffset: numInvoices - 1, + NumMaxInvoices: 1, + }, + expected: invoices[numInvoices-1:], + }, + // Same as above, at offset numInvoices. No invoices should be + // returned, as there are no invoices after this offset. + { + query: invpkg.InvoiceQuery{ + IndexOffset: numInvoices, + NumMaxInvoices: 1, + }, + expected: nil, + }, + // Fetch all pending invoices with a single query. + { + query: invpkg.InvoiceQuery{ + PendingOnly: true, + NumMaxInvoices: numInvoices, + }, + expected: pendingInvoices, + }, + // Fetch the first 12 pending invoices. + { + query: invpkg.InvoiceQuery{ + PendingOnly: true, + NumMaxInvoices: numInvoices / 4, + }, + expected: pendingInvoices[:len(pendingInvoices)/2], + }, + // Fetch the first 5 pending invoices, but this time iterating + // backwards. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 10, + PendingOnly: true, + Reversed: true, + NumMaxInvoices: numInvoices, + }, + // Since we seek to the invoice with index 10 and + // iterate backwards, there should only be 5 pending + // invoices before it as every other invoice within the + // index is settled. + expected: pendingInvoices[:5], + }, + // Fetch the last 15 invoices. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 20, + PendingOnly: true, + NumMaxInvoices: numInvoices, + }, + // Since we seek to the invoice with index 20, there are + // 30 invoices left. From these 30, only 15 of them are + // still pending. + expected: pendingInvoices[len(pendingInvoices)-15:], + }, + // Fetch all invoices paginating backwards, with an index offset + // that is beyond our last offset. We expect all invoices to be + // returned. + { + query: invpkg.InvoiceQuery{ + IndexOffset: numInvoices * 2, + PendingOnly: false, + Reversed: true, + NumMaxInvoices: numInvoices, + }, + expected: invoices, + }, + // Fetch invoices <= 25 by creation date. + { + query: invpkg.InvoiceQuery{ + NumMaxInvoices: numInvoices, + CreationDateEnd: 25, + }, + expected: invoices[:25], + }, + // Fetch invoices >= 26 creation date. + { + query: invpkg.InvoiceQuery{ + NumMaxInvoices: numInvoices, + CreationDateStart: 26, + }, + expected: invoices[25:], + }, + // Fetch pending invoices <= 25 by creation date. + { + query: invpkg.InvoiceQuery{ + PendingOnly: true, + NumMaxInvoices: numInvoices, + CreationDateEnd: 25, + }, + expected: pendingInvoices[:13], + }, + // Fetch pending invoices >= 26 creation date. + { + query: invpkg.InvoiceQuery{ + PendingOnly: true, + NumMaxInvoices: numInvoices, + CreationDateStart: 26, + }, + expected: pendingInvoices[13:], + }, + // Fetch pending invoices with offset and end creation date. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 20, + NumMaxInvoices: numInvoices, + CreationDateEnd: 30, + }, + // Since we're skipping to invoice 20 and iterating + // to invoice 30, we'll expect those invoices. + expected: invoices[20:30], + }, + // Fetch pending invoices with offset and start creation date + // in reversed order. + { + query: invpkg.InvoiceQuery{ + IndexOffset: 21, + Reversed: true, + NumMaxInvoices: numInvoices, + CreationDateStart: 11, + }, + // Since we're skipping to invoice 20 and iterating + // backward to invoice 10, we'll expect those invoices. + expected: invoices[10:20], + }, + // Fetch invoices with start and end creation date. + { + query: invpkg.InvoiceQuery{ + NumMaxInvoices: numInvoices, + CreationDateStart: 11, + CreationDateEnd: 20, + }, + expected: invoices[10:20], + }, + // Fetch pending invoices with start and end creation date. + { + query: invpkg.InvoiceQuery{ + PendingOnly: true, + NumMaxInvoices: numInvoices, + CreationDateStart: 11, + CreationDateEnd: 20, + }, + expected: pendingInvoices[5:10], + }, + // Fetch invoices with start and end creation date in reverse + // order. + { + query: invpkg.InvoiceQuery{ + Reversed: true, + NumMaxInvoices: numInvoices, + CreationDateStart: 11, + CreationDateEnd: 20, + }, + expected: invoices[10:20], + }, + // Fetch pending invoices with start and end creation date in + // reverse order. + { + query: invpkg.InvoiceQuery{ + PendingOnly: true, + Reversed: true, + NumMaxInvoices: numInvoices, + CreationDateStart: 11, + CreationDateEnd: 20, + }, + expected: pendingInvoices[5:10], + }, + // Fetch invoices with a start date greater than end date + // should result in an empty slice. + { + query: invpkg.InvoiceQuery{ + NumMaxInvoices: numInvoices, + CreationDateStart: 20, + CreationDateEnd: 11, + }, + expected: nil, + }, + } + + for i, testCase := range testCases { + response, err := db.QueryInvoices(ctxb, testCase.query) + if err != nil { + t.Fatalf("unable to query invoice database: %v", err) + } + + require.Equal(t, len(testCase.expected), len(response.Invoices)) + + for j, expected := range testCase.expected { + require.Equal(t, + expected, response.Invoices[j], + fmt.Sprintf("test: #%v, item: #%v", i, j), + ) + } + } +} + +// getUpdateInvoice returns an invoice update callback that, when called, +// settles the invoice with the given amount. +func getUpdateInvoice(amt lnwire.MilliSatoshi) invpkg.InvoiceUpdateCallback { + return func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, + error) { + + if invoice.State == invpkg.ContractSettled { + return nil, invpkg.ErrInvoiceAlreadySettled + } + + noRecords := make(record.CustomSet) + htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ + {}: { + Amt: amt, + CustomRecords: noRecords, + }, + } + update := &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.AddHTLCsUpdate, + State: &invpkg.InvoiceStateUpdateDesc{ + Preimage: invoice.Terms.PaymentPreimage, + NewState: invpkg.ContractSettled, + }, + AddHtlcs: htlcs, + } + + return update, nil + } +} + +// TestCustomRecords tests that custom records are properly recorded in the +// invoice database. +func TestCustomRecords(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err, "unable to make test db") + + preimage := lntypes.Preimage{1} + paymentHash := preimage.Hash() + + testInvoice := &invpkg.Invoice{ + Htlcs: map[models.CircuitKey]*invpkg.InvoiceHTLC{}, + Terms: invpkg.ContractTerm{ + Value: lnwire.NewMSatFromSatoshis(10000), + Features: emptyFeatures, + PaymentPreimage: &preimage, + }, + } + + ctxb := context.Background() + if _, err := db.AddInvoice(ctxb, testInvoice, paymentHash); err != nil { + t.Fatalf("unable to add invoice: %v", err) + } + + // Accept an htlc with custom records on this invoice. + key := models.CircuitKey{ + ChanID: lnwire.NewShortChanIDFromInt(1), + HtlcID: 4, + } + + records := record.CustomSet{ + 100000: []byte{}, + 100001: []byte{1, 2}, + } + + ref := invpkg.InvoiceRefByHash(paymentHash) + callback := func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, + error) { + + htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ + key: { + Amt: 500, + CustomRecords: records, + }, + } + + return &invpkg.InvoiceUpdateDesc{ + AddHtlcs: htlcs, + UpdateType: invpkg.AddHTLCsUpdate, + }, nil + } + + _, err = db.UpdateInvoice(ctxb, ref, nil, callback) + require.NoError(t, err, "unable to add invoice htlc") + + // Retrieve the invoice from that database and verify that the custom + // records are present. + dbInvoice, err := db.LookupInvoice(ctxb, ref) + require.NoError(t, err, "unable to lookup invoice") + + if len(dbInvoice.Htlcs) != 1 { + t.Fatalf("expected the htlc to be added") + } + + require.Equal(t, + records, dbInvoice.Htlcs[key].CustomRecords, + "invalid custom records", + ) +} + +// TestInvoiceHtlcAMPFields asserts that the set id and preimage fields are +// properly recorded when updating an invoice. +func TestInvoiceHtlcAMPFields(t *testing.T) { + t.Run("amp", func(t *testing.T) { + testInvoiceHtlcAMPFields(t, true) + }) + t.Run("no amp", func(t *testing.T) { + testInvoiceHtlcAMPFields(t, false) + }) +} + +func testInvoiceHtlcAMPFields(t *testing.T, isAMP bool) { + db, err := channeldb.MakeTestInvoiceDB(t) + require.Nil(t, err) + + testInvoice, err := randInvoice(1000) + require.Nil(t, err) + + if isAMP { + testInvoice.Terms.Features = ampFeatures + } + + ctxb := context.Background() + payHash := testInvoice.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, testInvoice, payHash) + require.Nil(t, err) + + // Accept an htlc with custom records on this invoice. + key := models.CircuitKey{ + ChanID: lnwire.NewShortChanIDFromInt(1), + HtlcID: 4, + } + records := make(map[uint64][]byte) + + var ampData *invpkg.InvoiceHtlcAMPData + if isAMP { + amp := record.NewAMP([32]byte{1}, [32]byte{2}, 3) + preimage := &lntypes.Preimage{4} + + ampData = &invpkg.InvoiceHtlcAMPData{ + Record: *amp, + Hash: preimage.Hash(), + Preimage: preimage, + } + } + + callback := func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, + error) { + + htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ + key: { + Amt: 500, + AMP: ampData, + CustomRecords: records, + }, + } + + return &invpkg.InvoiceUpdateDesc{ + AddHtlcs: htlcs, + UpdateType: invpkg.AddHTLCsUpdate, + }, nil + } + + ref := invpkg.InvoiceRefByHash(payHash) + _, err = db.UpdateInvoice(ctxb, ref, nil, callback) + require.Nil(t, err) + + // Retrieve the invoice from that database and verify that the AMP + // fields are as expected. + dbInvoice, err := db.LookupInvoice(ctxb, ref) + require.Nil(t, err) + + require.Equal(t, 1, len(dbInvoice.Htlcs)) + require.Equal(t, ampData, dbInvoice.Htlcs[key].AMP) +} + +// TestInvoiceRef asserts that the proper identifiers are returned from an +// InvoiceRef depending on the constructor used. +func TestInvoiceRef(t *testing.T) { + payHash := lntypes.Hash{0x01} + payAddr := [32]byte{0x02} + setID := [32]byte{0x03} + + // An InvoiceRef by hash should return the provided hash and a nil + // payment addr. + refByHash := invpkg.InvoiceRefByHash(payHash) + require.Equal(t, &payHash, refByHash.PayHash()) + require.Equal(t, (*[32]byte)(nil), refByHash.PayAddr()) + require.Equal(t, (*[32]byte)(nil), refByHash.SetID()) + + // An InvoiceRef by hash and addr should return the payment hash and + // payment addr passed to the constructor. + refByHashAndAddr := invpkg.InvoiceRefByHashAndAddr(payHash, payAddr) + require.Equal(t, &payHash, refByHashAndAddr.PayHash()) + require.Equal(t, &payAddr, refByHashAndAddr.PayAddr()) + require.Equal(t, (*[32]byte)(nil), refByHashAndAddr.SetID()) + + // An InvoiceRef by set id should return an empty pay hash, a nil pay + // addr, and a reference to the given set id. + refBySetID := invpkg.InvoiceRefBySetID(setID) + require.Equal(t, (*lntypes.Hash)(nil), refBySetID.PayHash()) + require.Equal(t, (*[32]byte)(nil), refBySetID.PayAddr()) + require.Equal(t, &setID, refBySetID.SetID()) + + // An InvoiceRef by pay addr should only return a pay addr, but nil for + // pay hash and set id. + refByAddr := invpkg.InvoiceRefByAddr(payAddr) + require.Equal(t, (*lntypes.Hash)(nil), refByAddr.PayHash()) + require.Equal(t, &payAddr, refByAddr.PayAddr()) + require.Equal(t, (*[32]byte)(nil), refByAddr.SetID()) +} + +// TestHTLCSet asserts that HTLCSet returns the proper set of accepted HTLCs +// that can be considered for settlement. It asserts that MPP and AMP HTLCs do +// not comingle, and also that HTLCs with disjoint set ids appear in different +// sets. +func TestHTLCSet(t *testing.T) { + inv := &invpkg.Invoice{ + Htlcs: make(map[models.CircuitKey]*invpkg.InvoiceHTLC), + } + + // Construct two distinct set id's, in this test we'll also track the + // nil set id as a third group. + setID1 := &[32]byte{1} + setID2 := &[32]byte{2} + + // Create the expected htlc sets for each group, these will be updated + // as the invoice is modified. + + expSetNil := make(map[models.CircuitKey]*invpkg.InvoiceHTLC) + expSet1 := make(map[models.CircuitKey]*invpkg.InvoiceHTLC) + expSet2 := make(map[models.CircuitKey]*invpkg.InvoiceHTLC) + + checkHTLCSets := func() { + require.Equal( + t, expSetNil, + inv.HTLCSet(nil, invpkg.HtlcStateAccepted), + ) + require.Equal( + t, expSet1, + inv.HTLCSet(setID1, invpkg.HtlcStateAccepted), + ) + require.Equal( + t, expSet2, + inv.HTLCSet(setID2, invpkg.HtlcStateAccepted), + ) + } + + // All HTLC sets should be empty initially. + checkHTLCSets() + + // Add the following sequence of HTLCs to the invoice, sanity checking + // all three HTLC sets after each transition. This sequence asserts: + // - both nil and non-nil set ids can have multiple htlcs. + // - there may be distinct htlc sets with non-nil set ids. + // - only accepted htlcs are returned as part of the set. + htlcs := []struct { + setID *[32]byte + state invpkg.HtlcState + }{ + {nil, invpkg.HtlcStateAccepted}, + {nil, invpkg.HtlcStateAccepted}, + {setID1, invpkg.HtlcStateAccepted}, + {setID1, invpkg.HtlcStateAccepted}, + {setID2, invpkg.HtlcStateAccepted}, + {setID2, invpkg.HtlcStateAccepted}, + {nil, invpkg.HtlcStateCanceled}, + {setID1, invpkg.HtlcStateCanceled}, + {setID2, invpkg.HtlcStateCanceled}, + {nil, invpkg.HtlcStateSettled}, + {setID1, invpkg.HtlcStateSettled}, + {setID2, invpkg.HtlcStateSettled}, + } + + for i, h := range htlcs { + var ampData *invpkg.InvoiceHtlcAMPData + if h.setID != nil { + ampData = &invpkg.InvoiceHtlcAMPData{ + Record: *record.NewAMP( + [32]byte{0}, *h.setID, 0, + ), + } + } + + // Add the HTLC to the invoice's set of HTLCs. + key := models.CircuitKey{HtlcID: uint64(i)} + htlc := &invpkg.InvoiceHTLC{ + AMP: ampData, + State: h.state, + } + inv.Htlcs[key] = htlc + + // Update our expected htlc set if the htlc is accepted, + // otherwise it shouldn't be reflected. + if h.state == invpkg.HtlcStateAccepted { + switch h.setID { + case nil: + expSetNil[key] = htlc + case setID1: + expSet1[key] = htlc + case setID2: + expSet2[key] = htlc + default: + t.Fatalf("unexpected set id") + } + } + + checkHTLCSets() + } +} + +// TestAddInvoiceWithHTLCs asserts that you can't insert an invoice that already +// has HTLCs. +func TestAddInvoiceWithHTLCs(t *testing.T) { + db, err := channeldb.MakeTestInvoiceDB(t) + require.Nil(t, err) + + testInvoice, err := randInvoice(1000) + require.Nil(t, err) + + key := models.CircuitKey{HtlcID: 1} + testInvoice.Htlcs[key] = &invpkg.InvoiceHTLC{} + + payHash := testInvoice.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(context.Background(), testInvoice, payHash) + require.Equal(t, invpkg.ErrInvoiceHasHtlcs, err) +} + +// TestSetIDIndex asserts that the set id index properly adds new invoices as we +// accept HTLCs, that they can be queried by their set id after accepting, and +// that invoices with duplicate set ids are disallowed. +func TestSetIDIndex(t *testing.T) { + testClock := clock.NewTestClock(testNow) + db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + require.Nil(t, err) + + // We'll start out by creating an invoice and writing it to the DB. + amt := lnwire.NewMSatFromSatoshis(1000) + invoice, err := randInvoice(amt) + require.Nil(t, err) + + // Set AMP-specific features so that we can settle with HTLC-level + // preimages. + invoice.Terms.Features = ampFeatures + + ctxb := context.Background() + preimage := *invoice.Terms.PaymentPreimage + payHash := preimage.Hash() + _, err = db.AddInvoice(ctxb, invoice, payHash) + require.Nil(t, err) + + setID := &[32]byte{1} + + // Update the invoice with an accepted HTLC that also accepts the + // invoice. + ref := invpkg.InvoiceRefByHashAndAddr( + payHash, invoice.Terms.PaymentAddr, + ) + dbInvoice, err := db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID), + updateAcceptAMPHtlc(0, amt, setID, true), + ) + require.Nil(t, err) + + // We'll update what we expect the accepted invoice to be so that our + // comparison below has the correct assumption. + invoice.State = invpkg.ContractOpen + invoice.AmtPaid = amt + invoice.SettleDate = dbInvoice.SettleDate + htlc0 := models.CircuitKey{HtlcID: 0} + invoice.Htlcs = map[models.CircuitKey]*invpkg.InvoiceHTLC{ + htlc0: makeAMPInvoiceHTLC(amt, *setID, payHash, &preimage), + } + invoice.AMPState = map[invpkg.SetID]invpkg.InvoiceStateAMP{} + invoice.AMPState[*setID] = invpkg.InvoiceStateAMP{ + State: invpkg.HtlcStateAccepted, + AmtPaid: amt, + InvoiceKeys: map[models.CircuitKey]struct{}{ + htlc0: {}, + }, + } + + // We should get back the exact same invoice that we just inserted. + require.Equal(t, invoice, dbInvoice) + + // Now lookup the invoice by set id and see that we get the same one. + refBySetID := invpkg.InvoiceRefBySetID(*setID) + dbInvoiceBySetID, err := db.LookupInvoice(ctxb, refBySetID) + require.Nil(t, err) + require.Equal(t, invoice, &dbInvoiceBySetID) + + // Trying to accept an HTLC to a different invoice, but using the same + // set id should fail. + invoice2, err := randInvoice(amt) + require.Nil(t, err) + + // Set AMP-specific features so that we can settle with HTLC-level + // preimages. + invoice2.Terms.Features = ampFeatures + + payHash2 := invoice2.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice2, payHash2) + require.Nil(t, err) + + ref2 := invpkg.InvoiceRefByHashAndAddr( + payHash2, invoice2.Terms.PaymentAddr, + ) + _, err = db.UpdateInvoice( + ctxb, ref2, (*invpkg.SetID)(setID), + updateAcceptAMPHtlc(0, amt, setID, true), + ) + require.Equal(t, invpkg.ErrDuplicateSetID{SetID: *setID}, err) + + // Now, begin constructing a second htlc set under a different set id. + // This set will contain two distinct HTLCs. + setID2 := &[32]byte{2} + + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID2), + updateAcceptAMPHtlc(1, amt, setID2, false), + ) + require.Nil(t, err) + dbInvoice, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID2), + updateAcceptAMPHtlc(2, amt, setID2, false), + ) + require.Nil(t, err) + + // We'll update what we expect the settle invoice to be so that our + // comparison below has the correct assumption. + invoice.State = invpkg.ContractOpen + invoice.AmtPaid += 2 * amt + invoice.SettleDate = dbInvoice.SettleDate + htlc1 := models.CircuitKey{HtlcID: 1} + htlc2 := models.CircuitKey{HtlcID: 2} + invoice.Htlcs = map[models.CircuitKey]*invpkg.InvoiceHTLC{ + htlc0: makeAMPInvoiceHTLC(amt, *setID, payHash, &preimage), + htlc1: makeAMPInvoiceHTLC(amt, *setID2, payHash, nil), + htlc2: makeAMPInvoiceHTLC(amt, *setID2, payHash, nil), + } + invoice.AMPState[*setID] = invpkg.InvoiceStateAMP{ + State: invpkg.HtlcStateAccepted, + AmtPaid: amt, + InvoiceKeys: map[models.CircuitKey]struct{}{ + htlc0: {}, + }, + } + invoice.AMPState[*setID2] = invpkg.InvoiceStateAMP{ + State: invpkg.HtlcStateAccepted, + AmtPaid: amt * 2, + InvoiceKeys: map[models.CircuitKey]struct{}{ + htlc1: {}, + htlc2: {}, + }, + } + + // Since UpdateInvoice will only return the sub-set of updated HTLcs, + // we'll query again to ensure we get the full set of HTLCs returned. + freshInvoice, err := db.LookupInvoice(ctxb, ref) + require.Nil(t, err) + dbInvoice = &freshInvoice + + // We should get back the exact same invoice that we just inserted. + require.Equal(t, invoice, dbInvoice) + + // Now lookup the invoice by second set id and see that we get the same + // index, including the htlcs under the first set id. + refBySetID = invpkg.InvoiceRefBySetID(*setID2) + dbInvoiceBySetID, err = db.LookupInvoice(ctxb, refBySetID) + require.Nil(t, err) + require.Equal(t, invoice, &dbInvoiceBySetID) + + // Now attempt to settle a non-existent HTLC set, this set ID is the + // zero setID so it isn't used for anything internally. + _, err = db.UpdateInvoice( + ctxb, ref, nil, + getUpdateInvoiceAMPSettle( + &[32]byte{}, [32]byte{}, + models.CircuitKey{HtlcID: 99}, + ), + ) + require.Equal(t, invpkg.ErrEmptyHTLCSet, err) + + // Now settle the first htlc set. The existing HTLCs should remain in + // the accepted state and shouldn't be canceled, since we permit an + // invoice to be settled multiple times. + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID), + getUpdateInvoiceAMPSettle( + setID, preimage, models.CircuitKey{HtlcID: 0}, + ), + ) + require.Nil(t, err) + + freshInvoice, err = db.LookupInvoice(ctxb, ref) + require.Nil(t, err) + dbInvoice = &freshInvoice + + invoice.State = invpkg.ContractOpen + + // The amount paid should reflect that we have 3 present HTLCs, each + // with an amount of the original invoice. + invoice.AmtPaid = amt * 3 + + ampState := invoice.AMPState[*setID] + ampState.State = invpkg.HtlcStateSettled + ampState.SettleDate = testNow + ampState.SettleIndex = 1 + + invoice.AMPState[*setID] = ampState + + invoice.Htlcs[htlc0].State = invpkg.HtlcStateSettled + invoice.Htlcs[htlc0].ResolveTime = time.Unix(1, 0) + + require.Equal(t, invoice, dbInvoice) + + // If we try to settle the same set ID again, then we should get an + // error, as it's already been settled. + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID), + getUpdateInvoiceAMPSettle( + setID, preimage, models.CircuitKey{HtlcID: 0}, + ), + ) + require.Equal(t, invpkg.ErrEmptyHTLCSet, err) + + // Next, let's attempt to settle the other active set ID for this + // invoice. This will allow us to exercise the case where we go to + // settle an invoice with a new setID after one has already been fully + // settled. + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID2), + getUpdateInvoiceAMPSettle( + setID2, preimage, models.CircuitKey{HtlcID: 1}, + models.CircuitKey{HtlcID: 2}, + ), + ) + require.Nil(t, err) + + freshInvoice, err = db.LookupInvoice(ctxb, ref) + require.Nil(t, err) + dbInvoice = &freshInvoice + + // Now the rest of the HTLCs should show as fully settled. + ampState = invoice.AMPState[*setID2] + ampState.State = invpkg.HtlcStateSettled + ampState.SettleDate = testNow + ampState.SettleIndex = 2 + + invoice.AMPState[*setID2] = ampState + + invoice.Htlcs[htlc1].State = invpkg.HtlcStateSettled + invoice.Htlcs[htlc1].ResolveTime = time.Unix(1, 0) + invoice.Htlcs[htlc1].AMP.Preimage = &preimage + + invoice.Htlcs[htlc2].State = invpkg.HtlcStateSettled + invoice.Htlcs[htlc2].ResolveTime = time.Unix(1, 0) + invoice.Htlcs[htlc2].AMP.Preimage = &preimage + + require.Equal(t, invoice, dbInvoice) + + // Lastly, querying for an unknown set id should fail. + refUnknownSetID := invpkg.InvoiceRefBySetID([32]byte{}) + _, err = db.LookupInvoice(ctxb, refUnknownSetID) + require.Equal(t, invpkg.ErrInvoiceNotFound, err) +} + +func makeAMPInvoiceHTLC(amt lnwire.MilliSatoshi, setID [32]byte, + hash lntypes.Hash, preimage *lntypes.Preimage) *invpkg.InvoiceHTLC { + + return &invpkg.InvoiceHTLC{ + Amt: amt, + AcceptTime: testNow, + ResolveTime: time.Time{}, + State: invpkg.HtlcStateAccepted, + CustomRecords: make(record.CustomSet), + AMP: &invpkg.InvoiceHtlcAMPData{ + Record: *record.NewAMP([32]byte{}, setID, 0), + Hash: hash, + Preimage: preimage, + }, + } +} + +// updateAcceptAMPHtlc returns an invoice update callback that, when called, +// settles the invoice with the given amount. +func updateAcceptAMPHtlc(id uint64, amt lnwire.MilliSatoshi, + setID *[32]byte, accept bool) invpkg.InvoiceUpdateCallback { + + return func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, + error) { + + if invoice.State == invpkg.ContractSettled { + return nil, invpkg.ErrInvoiceAlreadySettled + } + + noRecords := make(record.CustomSet) + + var ( + state *invpkg.InvoiceStateUpdateDesc + preimage *lntypes.Preimage + ) + if accept { + state = &invpkg.InvoiceStateUpdateDesc{ + NewState: invpkg.ContractAccepted, + SetID: setID, + } + pre := *invoice.Terms.PaymentPreimage + preimage = &pre + } + + ampData := &invpkg.InvoiceHtlcAMPData{ + Record: *record.NewAMP([32]byte{}, *setID, 0), + Hash: invoice.Terms.PaymentPreimage.Hash(), + Preimage: preimage, + } + + htlcs := map[models.CircuitKey]*invpkg.HtlcAcceptDesc{ + {HtlcID: id}: { + Amt: amt, + CustomRecords: noRecords, + AMP: ampData, + }, + } + + update := &invpkg.InvoiceUpdateDesc{ + State: state, + AddHtlcs: htlcs, + UpdateType: invpkg.AddHTLCsUpdate, + } + + return update, nil + } +} + +func getUpdateInvoiceAMPSettle(setID *[32]byte, preimage [32]byte, + circuitKeys ...models.CircuitKey) invpkg.InvoiceUpdateCallback { + + return func(invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, + error) { + + if invoice.State == invpkg.ContractSettled { + return nil, invpkg.ErrInvoiceAlreadySettled + } + + preImageSet := make(map[models.CircuitKey]lntypes.Preimage) + for _, key := range circuitKeys { + preImageSet[key] = preimage + } + + update := &invpkg.InvoiceUpdateDesc{ + // TODO(positiveblue): this would be an invalid update + // because tires to settle an AMP invoice without adding + // any new htlc. + UpdateType: invpkg.AddHTLCsUpdate, + State: &invpkg.InvoiceStateUpdateDesc{ + Preimage: nil, + NewState: invpkg.ContractSettled, + SetID: setID, + HTLCPreimages: preImageSet, + }, + } + + return update, nil + } +} + +// TestUnexpectedInvoicePreimage asserts that legacy or MPP invoices cannot be +// settled when referenced by payment address only. Since regular or MPP +// payments do not store the payment hash explicitly (it is stored in the +// index), this enforces that they can only be updated using a InvoiceRefByHash +// or InvoiceRefByHashOrAddr. +func TestUnexpectedInvoicePreimage(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err, "unable to make test db") + + invoice, err := randInvoice(lnwire.MilliSatoshi(100)) + require.NoError(t, err) + + ctxb := context.Background() + + // Add a random invoice indexed by payment hash and payment addr. + paymentHash := invoice.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice, paymentHash) + require.NoError(t, err) + + // Attempt to update the invoice by pay addr only. This will fail since, + // in order to settle an MPP invoice, the InvoiceRef must present a + // payment hash against which to validate the preimage. + _, err = db.UpdateInvoice( + ctxb, invpkg.InvoiceRefByAddr(invoice.Terms.PaymentAddr), nil, + getUpdateInvoice(invoice.Terms.Value), + ) + + // Assert that we get ErrUnexpectedInvoicePreimage. + require.Error(t, invpkg.ErrUnexpectedInvoicePreimage, err) +} + +type updateHTLCPreimageTestCase struct { + name string + settleSamePreimage bool + expError error +} + +// TestUpdateHTLCPreimages asserts various properties of setting HTLC-level +// preimages on invoice state transitions. +func TestUpdateHTLCPreimages(t *testing.T) { + t.Parallel() + + tests := []updateHTLCPreimageTestCase{ + { + name: "same preimage on settle", + settleSamePreimage: true, + expError: nil, + }, + { + name: "diff preimage on settle", + settleSamePreimage: false, + expError: invpkg.ErrHTLCPreimageAlreadyExists, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + testUpdateHTLCPreimages(t, test) + }) + } +} + +func testUpdateHTLCPreimages(t *testing.T, test updateHTLCPreimageTestCase) { + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err, "unable to make test db") + + // We'll start out by creating an invoice and writing it to the DB. + amt := lnwire.NewMSatFromSatoshis(1000) + invoice, err := randInvoice(amt) + require.Nil(t, err) + + preimage := *invoice.Terms.PaymentPreimage + payHash := preimage.Hash() + + // Set AMP-specific features so that we can settle with HTLC-level + // preimages. + invoice.Terms.Features = ampFeatures + + ctxb := context.Background() + _, err = db.AddInvoice(ctxb, invoice, payHash) + require.Nil(t, err) + + setID := &[32]byte{1} + + // Update the invoice with an accepted HTLC that also accepts the + // invoice. + ref := invpkg.InvoiceRefByAddr(invoice.Terms.PaymentAddr) + dbInvoice, err := db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID), + updateAcceptAMPHtlc(0, amt, setID, true), + ) + require.Nil(t, err) + + htlcPreimages := make(map[models.CircuitKey]lntypes.Preimage) + for key := range dbInvoice.Htlcs { + // Set the either the same preimage used to accept above, or a + // blank preimage depending on the test case. + var pre lntypes.Preimage + if test.settleSamePreimage { + pre = preimage + } + htlcPreimages[key] = pre + } + + updateInvoice := func( + invoice *invpkg.Invoice) (*invpkg.InvoiceUpdateDesc, error) { + + update := &invpkg.InvoiceUpdateDesc{ + // TODO(positiveblue): this would be an invalid update + // because tires to settle an AMP invoice without adding + // any new htlc. + State: &invpkg.InvoiceStateUpdateDesc{ + Preimage: nil, + NewState: invpkg.ContractSettled, + HTLCPreimages: htlcPreimages, + SetID: setID, + }, + UpdateType: invpkg.AddHTLCsUpdate, + } + + return update, nil + } + + // Now settle the HTLC set and assert the resulting error. + _, err = db.UpdateInvoice( + ctxb, ref, (*invpkg.SetID)(setID), updateInvoice, + ) + require.Equal(t, test.expError, err) +} + +// TestDeleteInvoices tests that deleting a list of invoices will succeed +// if all delete references are valid, or will fail otherwise. +func TestDeleteInvoices(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err, "unable to make test db") + + // Add some invoices to the test db. + numInvoices := 3 + invoicesToDelete := make([]invpkg.InvoiceDeleteRef, numInvoices) + + ctxb := context.Background() + for i := 0; i < numInvoices; i++ { + invoice, err := randInvoice(lnwire.MilliSatoshi(i + 1)) + require.NoError(t, err) + + paymentHash := invoice.Terms.PaymentPreimage.Hash() + addIndex, err := db.AddInvoice(ctxb, invoice, paymentHash) + require.NoError(t, err) + + // Settle the second invoice. + if i == 1 { + invoice, err = db.UpdateInvoice( + ctxb, invpkg.InvoiceRefByHash(paymentHash), nil, + getUpdateInvoice(invoice.Terms.Value), + ) + require.NoError(t, err, "unable to settle invoice") + } + + // store the delete ref for later. + invoicesToDelete[i] = invpkg.InvoiceDeleteRef{ + PayHash: paymentHash, + PayAddr: &invoice.Terms.PaymentAddr, + AddIndex: addIndex, + SettleIndex: invoice.SettleIndex, + } + } + + // assertInvoiceCount asserts that the number of invoices equals + // to the passed count. + assertInvoiceCount := func(count int) { + // Query to collect all invoices. + query := invpkg.InvoiceQuery{ + IndexOffset: 0, + NumMaxInvoices: math.MaxUint64, + } + + // Check that we really have 3 invoices. + response, err := db.QueryInvoices(ctxb, query) + require.NoError(t, err) + require.Equal(t, count, len(response.Invoices)) + } + + // XOR one byte of one of the references' hash and attempt to delete. + invoicesToDelete[0].PayHash[2] ^= 3 + require.Error(t, db.DeleteInvoice(ctxb, invoicesToDelete)) + assertInvoiceCount(3) + + // Restore the hash. + invoicesToDelete[0].PayHash[2] ^= 3 + + // XOR the second invoice's payment settle index as it is settled, and + // attempt to delete. + invoicesToDelete[1].SettleIndex ^= 11 + require.Error(t, db.DeleteInvoice(ctxb, invoicesToDelete)) + assertInvoiceCount(3) + + // Restore the settle index. + invoicesToDelete[1].SettleIndex ^= 11 + + // XOR the add index for one of the references and attempt to delete. + invoicesToDelete[2].AddIndex ^= 13 + require.Error(t, db.DeleteInvoice(ctxb, invoicesToDelete)) + assertInvoiceCount(3) + + // Restore the add index. + invoicesToDelete[2].AddIndex ^= 13 + + // Delete should succeed with all the valid references. + require.NoError(t, db.DeleteInvoice(ctxb, invoicesToDelete)) + assertInvoiceCount(0) +} + +// TestDeleteCanceledInvoices tests that deleting canceled invoices with the +// specific DeleteCanceledInvoices method works correctly. +func TestDeleteCanceledInvoices(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err, "unable to make test db") + + // Updatefunc is used to cancel an invoice. + updateFunc := func(invoice *invpkg.Invoice) ( + *invpkg.InvoiceUpdateDesc, error) { + + return &invpkg.InvoiceUpdateDesc{ + UpdateType: invpkg.CancelInvoiceUpdate, + State: &invpkg.InvoiceStateUpdateDesc{ + NewState: invpkg.ContractCanceled, + }, + }, nil + } + + // Add some invoices to the test db. + ctxb := context.Background() + var invoices []invpkg.Invoice + for i := 0; i < 10; i++ { + invoice, err := randInvoice(lnwire.MilliSatoshi(i + 1)) + require.NoError(t, err) + + paymentHash := invoice.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(ctxb, invoice, paymentHash) + require.NoError(t, err) + + // Cancel every second invoice. + if i%2 == 0 { + invoice, err = db.UpdateInvoice( + ctxb, invpkg.InvoiceRefByHash(paymentHash), nil, + updateFunc, + ) + require.NoError(t, err) + } else { + invoices = append(invoices, *invoice) + } + } + + // Delete canceled invoices. + require.NoError(t, db.DeleteCanceledInvoices(ctxb)) + + // Query to collect all invoices. + query := invpkg.InvoiceQuery{ + IndexOffset: 0, + NumMaxInvoices: math.MaxUint64, + } + + dbInvoices, err := db.QueryInvoices(ctxb, query) + require.NoError(t, err) + + // Check that we really have the expected invoices. + require.Equal(t, invoices, dbInvoices.Invoices) +} + +// TestAddInvoiceInvalidFeatureDeps asserts that inserting an invoice with +// invalid transitive feature dependencies fails with the appropriate error. +func TestAddInvoiceInvalidFeatureDeps(t *testing.T) { + t.Parallel() + + db, err := channeldb.MakeTestInvoiceDB(t) + require.NoError(t, err, "unable to make test db") + + invoice, err := randInvoice(500) + require.NoError(t, err) + + invoice.Terms.Features = lnwire.NewFeatureVector( + lnwire.NewRawFeatureVector( + lnwire.TLVOnionPayloadRequired, + lnwire.MPPOptional, + ), + lnwire.Features, + ) + + hash := invoice.Terms.PaymentPreimage.Hash() + _, err = db.AddInvoice(context.Background(), invoice, hash) + require.Error(t, err, feature.NewErrMissingFeatureDep( + lnwire.PaymentAddrOptional, + )) +} diff --git a/invoices/setup_test.go b/invoices/setup_test.go new file mode 100644 index 0000000000..6ca087ba3b --- /dev/null +++ b/invoices/setup_test.go @@ -0,0 +1,11 @@ +package invoices + +import ( + "testing" + + "github.com/lightningnetwork/lnd/kvdb" +) + +func TestMain(m *testing.M) { + kvdb.RunTests(m) +} From 0e2b39ed44e9a1b1b1c78eeb9b2f6b4c906c4721 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 26 Jan 2024 19:18:24 +0100 Subject: [PATCH 12/15] invoices: fix linter issues after the move --- invoices/invoices_test.go | 30 ++++++++++++++++++++++-------- invoices/update_invoice.go | 2 ++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/invoices/invoices_test.go b/invoices/invoices_test.go index 263563dfb3..259a977d4d 100644 --- a/invoices/invoices_test.go +++ b/invoices/invoices_test.go @@ -517,7 +517,9 @@ func TestInvoiceCancelSingleHtlc(t *testing.T) { func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { t.Parallel() - db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + db, err := channeldb.MakeTestInvoiceDB( + t, channeldb.OptionClock(testClock), + ) require.NoError(t, err, "unable to make test db: %v", err) // We'll start out by creating an invoice and writing it to the DB. @@ -691,11 +693,13 @@ func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { // TestInvoiceTimeSeries tests that newly added invoices invoices, as well as // settled invoices are added to the database are properly placed in the add -// add or settle index which serves as an event time series. +// or settle index which serves as an event time series. func TestInvoiceAddTimeSeries(t *testing.T) { t.Parallel() - db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + db, err := channeldb.MakeTestInvoiceDB( + t, channeldb.OptionClock(testClock), + ) require.NoError(t, err, "unable to make test db") ctxb := context.Background() @@ -853,7 +857,9 @@ func TestSettleIndexAmpPayments(t *testing.T) { t.Parallel() testClock := clock.NewTestClock(testNow) - db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + db, err := channeldb.MakeTestInvoiceDB( + t, channeldb.OptionClock(testClock), + ) require.Nil(t, err) // First, we'll make a sample invoice that'll be paid to several times @@ -1021,7 +1027,9 @@ func TestSettleIndexAmpPayments(t *testing.T) { func TestFetchPendingInvoices(t *testing.T) { t.Parallel() - db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + db, err := channeldb.MakeTestInvoiceDB( + t, channeldb.OptionClock(testClock), + ) require.NoError(t, err, "unable to make test db") ctxb := context.Background() @@ -1076,7 +1084,9 @@ func TestFetchPendingInvoices(t *testing.T) { func TestDuplicateSettleInvoice(t *testing.T) { t.Parallel() - db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + db, err := channeldb.MakeTestInvoiceDB( + t, channeldb.OptionClock(testClock), + ) require.NoError(t, err, "unable to make test db") // We'll start out by creating an invoice and writing it to the DB. @@ -1137,7 +1147,9 @@ func TestDuplicateSettleInvoice(t *testing.T) { func TestQueryInvoices(t *testing.T) { t.Parallel() - db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + db, err := channeldb.MakeTestInvoiceDB( + t, channeldb.OptionClock(testClock), + ) require.NoError(t, err, "unable to make test db") // To begin the test, we'll add 50 invoices to the database. We'll @@ -1869,7 +1881,9 @@ func TestAddInvoiceWithHTLCs(t *testing.T) { // that invoices with duplicate set ids are disallowed. func TestSetIDIndex(t *testing.T) { testClock := clock.NewTestClock(testNow) - db, err := channeldb.MakeTestInvoiceDB(t, channeldb.OptionClock(testClock)) + db, err := channeldb.MakeTestInvoiceDB( + t, channeldb.OptionClock(testClock), + ) require.Nil(t, err) // We'll start out by creating an invoice and writing it to the DB. diff --git a/invoices/update_invoice.go b/invoices/update_invoice.go index dc81d9db0f..a2de1b8f21 100644 --- a/invoices/update_invoice.go +++ b/invoices/update_invoice.go @@ -199,6 +199,8 @@ func cancelHTLCs(invoice *Invoice, updateTime time.Time, } // addHTLCs tries to add the htlcs in the given InvoiceUpdateDesc. +// +//nolint:funlen func addHTLCs(invoice *Invoice, hash *lntypes.Hash, updateTime time.Time, update *InvoiceUpdateDesc, updater InvoiceUpdater) error { From 7d56d5385d6ce56c8c9a2457257da4ef850bcfd7 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Thu, 26 Oct 2023 11:35:31 +0200 Subject: [PATCH 13/15] invoices: parametrize invoice(registry) tests with InvoiceDB constructor This commit extracts the InvoiceDB construction from all invoice and registry tests such that we can later on run subtests with multiple backends without needing to use tags. --- invoices/invoiceregistry_test.go | 310 ++++++++++++++++++------- invoices/invoices_test.go | 374 +++++++++++++++++++++---------- invoices/test_utils_test.go | 38 +--- 3 files changed, 479 insertions(+), 243 deletions(-) diff --git a/invoices/invoiceregistry_test.go b/invoices/invoiceregistry_test.go index a108425561..04fe310b56 100644 --- a/invoices/invoiceregistry_test.go +++ b/invoices/invoiceregistry_test.go @@ -11,6 +11,7 @@ import ( "github.com/lightningnetwork/lnd/amp" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" invpkg "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lntest/wait" @@ -20,11 +21,115 @@ import ( "github.com/stretchr/testify/require" ) -// TestSettleInvoice tests settling of an invoice and related notifications. -func TestSettleInvoice(t *testing.T) { +// TestInvoiceRegistry is a master test which encompasses all tests using an +// InvoiceDB instance. The purpose of this test is to be able to run all tests +// with a custom DB instance, so that we can test the same logic with different +// DB implementations. +func TestInvoiceRegistry(t *testing.T) { + testList := []struct { + name string + test func(t *testing.T, + makeDB func(t *testing.T) ( + invpkg.InvoiceDB, *clock.TestClock)) + }{ + { + name: "SettleInvoice", + test: testSettleInvoice, + }, + { + name: "CancelInvoice", + test: testCancelInvoice, + }, + { + name: "SettleHoldInvoice", + test: testSettleHoldInvoice, + }, + { + name: "CancelHoldInvoice", + test: testCancelHoldInvoice, + }, + { + name: "UnknownInvoice", + test: testUnknownInvoice, + }, + { + name: "KeySend", + test: testKeySend, + }, + { + name: "HoldKeysend", + test: testHoldKeysend, + }, + { + name: "MppPayment", + test: testMppPayment, + }, + { + name: "MppPaymentWithOverpayment", + test: testMppPaymentWithOverpayment, + }, + { + name: "InvoiceExpiryWithRegistry", + test: testInvoiceExpiryWithRegistry, + }, + { + name: "OldInvoiceRemovalOnStart", + test: testOldInvoiceRemovalOnStart, + }, + { + name: "HeightExpiryWithRegistry", + test: testHeightExpiryWithRegistry, + }, + { + name: "MultipleSetHeightExpiry", + test: testMultipleSetHeightExpiry, + }, + { + name: "SettleInvoicePaymentAddrRequired", + test: testSettleInvoicePaymentAddrRequired, + }, + { + name: "SettleInvoicePaymentAddrRequiredOptionalGrace", + test: testSettleInvoicePaymentAddrRequiredOptionalGrace, + }, + { + name: "AMPWithoutMPPPayload", + test: testAMPWithoutMPPPayload, + }, + { + name: "SpontaneousAmpPayment", + test: testSpontaneousAmpPayment, + }, + } + + makeKeyValueDB := func(t *testing.T) (invpkg.InvoiceDB, + *clock.TestClock) { + + testClock := clock.NewTestClock(testNow) + db, err := channeldb.MakeTestInvoiceDB( + t, channeldb.OptionClock(testClock), + ) + require.NoError(t, err, "unable to make test db") + + return db, testClock + } + + for _, test := range testList { + test := test + + t.Run(test.name, func(t *testing.T) { + test.test(t, makeKeyValueDB) + }) + } +} + +// testSettleInvoice tests settling of an invoice and related notifications. +func testSettleInvoice(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() - ctx := newTestContext(t, nil) + ctx := newTestContext(t, nil, makeDB) ctxb := context.Background() allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0) @@ -199,7 +304,9 @@ func TestSettleInvoice(t *testing.T) { } } -func testCancelInvoice(t *testing.T, gc bool) { +func testCancelInvoiceImpl(t *testing.T, gc bool, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() cfg := defaultRegistryConfig() @@ -207,7 +314,7 @@ func testCancelInvoice(t *testing.T, gc bool) { // If set to true, then also delete the invoice from the DB after // cancellation. cfg.GcCanceledInvoicesOnTheFly = gc - ctx := newTestContext(t, &cfg) + ctx := newTestContext(t, &cfg, makeDB) ctxb := context.Background() allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0) @@ -329,36 +436,37 @@ func testCancelInvoice(t *testing.T, gc bool) { require.Equal(t, testCurrentHeight, failResolution.AcceptHeight) } -// TestCancelInvoice tests cancellation of an invoice and related notifications. -func TestCancelInvoice(t *testing.T) { +// testCancelInvoice tests cancellation of an invoice and related notifications. +func testCancelInvoice(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() // Test cancellation both with garbage collection (meaning that canceled // invoice will be deleted) and without (meaning it'll be kept). t.Run("garbage collect", func(t *testing.T) { - testCancelInvoice(t, true) + testCancelInvoiceImpl(t, true, makeDB) }) t.Run("no garbage collect", func(t *testing.T) { - testCancelInvoice(t, false) + testCancelInvoiceImpl(t, false, makeDB) }) } -// TestSettleHoldInvoice tests settling of a hold invoice and related +// testSettleHoldInvoice tests settling of a hold invoice and related // notifications. -func TestSettleHoldInvoice(t *testing.T) { +func testSettleHoldInvoice(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() defer timeout()() - idb, err := newTestChannelDB(t, clock.NewTestClock(time.Time{})) - if err != nil { - t.Fatal(err) - } + idb, clock := makeDB(t) // Instantiate and start the invoice ctx.registry. cfg := invpkg.RegistryConfig{ FinalCltvRejectDelta: testFinalCltvRejectDelta, - Clock: clock.NewTestClock(testTime), + Clock: clock, } expiryWatcher := invpkg.NewInvoiceExpiryWatcher( @@ -366,7 +474,7 @@ func TestSettleHoldInvoice(t *testing.T) { ) registry := invpkg.NewRegistry(idb, expiryWatcher, &cfg) - err = registry.Start() + err := registry.Start() require.NoError(t, err) defer registry.Stop() @@ -511,15 +619,15 @@ func TestSettleHoldInvoice(t *testing.T) { ) } -// TestCancelHoldInvoice tests canceling of a hold invoice and related +// testCancelHoldInvoice tests canceling of a hold invoice and related // notifications. -func TestCancelHoldInvoice(t *testing.T) { +func testCancelHoldInvoice(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() defer timeout()() - testClock := clock.NewTestClock(testTime) - idb, err := newTestChannelDB(t, testClock) - require.NoError(t, err) + idb, testClock := makeDB(t) // Instantiate and start the invoice ctx.registry. cfg := invpkg.RegistryConfig{ @@ -531,7 +639,7 @@ func TestCancelHoldInvoice(t *testing.T) { ) registry := invpkg.NewRegistry(idb, expiryWatcher, &cfg) - err = registry.Start() + err := registry.Start() if err != nil { t.Fatal(err) } @@ -587,14 +695,16 @@ func TestCancelHoldInvoice(t *testing.T) { require.Equal(t, testCurrentHeight, failResolution.AcceptHeight) } -// TestUnknownInvoice tests that invoice registry returns an error when the +// testUnknownInvoice tests that invoice registry returns an error when the // invoice is unknown. This is to guard against returning a cancel htlc // resolution for forwarded htlcs. In the link, NotifyExitHopHtlc is only called // if we are the exit hop, but in htlcIncomingContestResolver it is called with // forwarded htlc hashes as well. -func TestUnknownInvoice(t *testing.T) { +func testUnknownInvoice(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() - ctx := newTestContext(t, nil) + ctx := newTestContext(t, nil, makeDB) // Notify arrival of a new htlc paying to this invoice. This should // succeed. @@ -611,28 +721,32 @@ func TestUnknownInvoice(t *testing.T) { checkFailResolution(t, resolution, invpkg.ResultInvoiceNotFound) } -// TestKeySend tests receiving a spontaneous payment with and without keysend +// testKeySend tests receiving a spontaneous payment with and without keysend // enabled. -func TestKeySend(t *testing.T) { +func testKeySend(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() t.Run("enabled", func(t *testing.T) { - testKeySend(t, true) + testKeySendImpl(t, true, makeDB) }) t.Run("disabled", func(t *testing.T) { - testKeySend(t, false) + testKeySendImpl(t, false, makeDB) }) } -// testKeySend is the inner test function that tests keysend for a particular -// enabled state on the receiver end. -func testKeySend(t *testing.T, keySendEnabled bool) { +// testKeySendImpl is the inner test function that tests keysend for a +// particular enabled state on the receiver end. +func testKeySendImpl(t *testing.T, keySendEnabled bool, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() defer timeout()() cfg := defaultRegistryConfig() cfg.AcceptKeySend = keySendEnabled - ctx := newTestContext(t, &cfg) + ctx := newTestContext(t, &cfg, makeDB) allSubscriptions, err := ctx.registry.SubscribeNotifications( context.Background(), 0, 0, @@ -742,20 +856,24 @@ func testKeySend(t *testing.T, keySendEnabled bool) { checkSubscription() } -// TestHoldKeysend tests receiving a spontaneous payment that is held. -func TestHoldKeysend(t *testing.T) { +// testHoldKeysend tests receiving a spontaneous payment that is held. +func testHoldKeysend(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() t.Run("settle", func(t *testing.T) { - testHoldKeysend(t, false) + testHoldKeysendImpl(t, false, makeDB) }) t.Run("timeout", func(t *testing.T) { - testHoldKeysend(t, true) + testHoldKeysendImpl(t, true, makeDB) }) } -// testHoldKeysend is the inner test function that tests hold-keysend. -func testHoldKeysend(t *testing.T, timeoutKeysend bool) { +// testHoldKeysendImpl is the inner test function that tests hold-keysend. +func testHoldKeysendImpl(t *testing.T, timeoutKeysend bool, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() defer timeout()() @@ -764,7 +882,7 @@ func testHoldKeysend(t *testing.T, timeoutKeysend bool) { cfg := defaultRegistryConfig() cfg.AcceptKeySend = true cfg.KeysendHoldTime = holdDuration - ctx := newTestContext(t, &cfg) + ctx := newTestContext(t, &cfg, makeDB) ctxb := context.Background() allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0) @@ -844,14 +962,16 @@ func testHoldKeysend(t *testing.T, timeoutKeysend bool) { require.Equal(t, settledInvoice.State, invpkg.ContractSettled) } -// TestMppPayment tests settling of an invoice with multiple partial payments. +// testMppPayment tests settling of an invoice with multiple partial payments. // It covers the case where there is a mpp timeout before the whole invoice is // paid and the case where the invoice is settled in time. -func TestMppPayment(t *testing.T) { +func testMppPayment(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() defer timeout()() - ctx := newTestContext(t, nil) + ctx := newTestContext(t, nil, makeDB) ctxb := context.Background() // Add the invoice. @@ -940,15 +1060,17 @@ func TestMppPayment(t *testing.T) { } } -// TestMppPaymentWithOverpayment tests settling of an invoice with multiple +// testMppPaymentWithOverpayment tests settling of an invoice with multiple // partial payments. It covers the case where the mpp overpays what is in the // invoice. -func TestMppPaymentWithOverpayment(t *testing.T) { +func testMppPaymentWithOverpayment(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() ctxb := context.Background() f := func(overpaymentRand uint64) bool { - ctx := newTestContext(t, nil) + ctx := newTestContext(t, nil, makeDB) // Add the invoice. testInvoice := newInvoice(t, false) @@ -1017,13 +1139,13 @@ func TestMppPaymentWithOverpayment(t *testing.T) { } } -// Tests that invoices are canceled after expiration. -func TestInvoiceExpiryWithRegistry(t *testing.T) { - t.Parallel() +// testInvoiceExpiryWithRegistry tests that invoices are canceled after +// expiration. +func testInvoiceExpiryWithRegistry(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { - testClock := clock.NewTestClock(testTime) - idb, err := newTestChannelDB(t, testClock) - require.NoError(t, err) + t.Parallel() + idb, testClock := makeDB(t) cfg := invpkg.RegistryConfig{ FinalCltvRejectDelta: testFinalCltvRejectDelta, @@ -1119,21 +1241,20 @@ func TestInvoiceExpiryWithRegistry(t *testing.T) { // Retrospectively check that all invoices that were expected to be // canceled are indeed canceled. - err = wait.NoError(canceled, testTimeout) + err := wait.NoError(canceled, testTimeout) require.NoError(t, err, "timeout checking invoice state") // Finally stop the registry. require.NoError(t, registry.Stop(), "failed to stop invoice registry") } -// TestOldInvoiceRemovalOnStart tests that we'll attempt to remove old canceled +// testOldInvoiceRemovalOnStart tests that we'll attempt to remove old canceled // invoices upon start while keeping all settled ones. -func TestOldInvoiceRemovalOnStart(t *testing.T) { - t.Parallel() +func testOldInvoiceRemovalOnStart(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { - testClock := clock.NewTestClock(testTime) - idb, err := newTestChannelDB(t, testClock) - require.NoError(t, err) + t.Parallel() + idb, testClock := makeDB(t) cfg := invpkg.RegistryConfig{ FinalCltvRejectDelta: testFinalCltvRejectDelta, @@ -1203,35 +1324,39 @@ func TestOldInvoiceRemovalOnStart(t *testing.T) { require.Equal(t, expected, response.Invoices) } -// TestHeightExpiryWithRegistry tests our height-based invoice expiry for +// testHeightExpiryWithRegistry tests our height-based invoice expiry for // invoices paid with single and multiple htlcs, testing the case where the // invoice is settled before expiry (and thus not canceled), and the case // where the invoice is expired. -func TestHeightExpiryWithRegistry(t *testing.T) { +func testHeightExpiryWithRegistry(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() t.Run("single shot settled before expiry", func(t *testing.T) { - testHeightExpiryWithRegistry(t, 1, true) + testHeightExpiryWithRegistryImpl(t, 1, true, makeDB) }) t.Run("single shot expires", func(t *testing.T) { - testHeightExpiryWithRegistry(t, 1, false) + testHeightExpiryWithRegistryImpl(t, 1, false, makeDB) }) t.Run("mpp settled before expiry", func(t *testing.T) { - testHeightExpiryWithRegistry(t, 2, true) + testHeightExpiryWithRegistryImpl(t, 2, true, makeDB) }) t.Run("mpp expires", func(t *testing.T) { - testHeightExpiryWithRegistry(t, 2, false) + testHeightExpiryWithRegistryImpl(t, 2, false, makeDB) }) } -func testHeightExpiryWithRegistry(t *testing.T, numParts int, settle bool) { +func testHeightExpiryWithRegistryImpl(t *testing.T, numParts int, settle bool, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() defer timeout()() - ctx := newTestContext(t, nil) + ctx := newTestContext(t, nil, makeDB) require.Greater(t, numParts, 0, "test requires at least one part") @@ -1337,15 +1462,17 @@ func testHeightExpiryWithRegistry(t *testing.T, numParts int, settle bool) { "hold invoice: %v, got: %v", expectedState, inv.State) } -// TestMultipleSetHeightExpiry pays a hold invoice with two mpp sets, testing +// testMultipleSetHeightExpiry pays a hold invoice with two mpp sets, testing // that the invoice expiry watcher only uses the expiry height of the second, // successful set to cancel the invoice, and does not cancel early using the // expiry height of the first set that was canceled back due to mpp timeout. -func TestMultipleSetHeightExpiry(t *testing.T) { +func testMultipleSetHeightExpiry(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() defer timeout()() - ctx := newTestContext(t, nil) + ctx := newTestContext(t, nil, makeDB) // Add a hold invoice. testInvoice := newInvoice(t, true) @@ -1431,13 +1558,15 @@ func TestMultipleSetHeightExpiry(t *testing.T) { }, testTimeout, time.Millisecond*100, "invoice not canceled") } -// TestSettleInvoicePaymentAddrRequired tests that if an incoming payment has +// testSettleInvoicePaymentAddrRequired tests that if an incoming payment has // an invoice that requires the payment addr bit to be set, and the incoming // payment doesn't include an mpp payload, then the payment is rejected. -func TestSettleInvoicePaymentAddrRequired(t *testing.T) { +func testSettleInvoicePaymentAddrRequired(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() - ctx := newTestContext(t, nil) + ctx := newTestContext(t, nil, makeDB) ctxb := context.Background() allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0) @@ -1519,14 +1648,16 @@ func TestSettleInvoicePaymentAddrRequired(t *testing.T) { require.Equal(t, failResolution.Outcome, invpkg.ResultAddressMismatch) } -// TestSettleInvoicePaymentAddrRequiredOptionalGrace tests that if an invoice +// testSettleInvoicePaymentAddrRequiredOptionalGrace tests that if an invoice // in the database has an optional payment addr required bit set, then we'll // still allow it to be paid by an incoming HTLC that doesn't include the MPP // payload. This ensures we don't break payment for any invoices in the wild. -func TestSettleInvoicePaymentAddrRequiredOptionalGrace(t *testing.T) { +func testSettleInvoicePaymentAddrRequiredOptionalGrace(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() - ctx := newTestContext(t, nil) + ctx := newTestContext(t, nil, makeDB) ctxb := context.Background() allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0) @@ -1633,15 +1764,17 @@ func TestSettleInvoicePaymentAddrRequiredOptionalGrace(t *testing.T) { } } -// TestAMPWithoutMPPPayload asserts that we correctly reject an AMP HTLC that +// testAMPWithoutMPPPayload asserts that we correctly reject an AMP HTLC that // does not include an MPP record. -func TestAMPWithoutMPPPayload(t *testing.T) { +func testAMPWithoutMPPPayload(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() defer timeout()() cfg := defaultRegistryConfig() cfg.AcceptAMP = true - ctx := newTestContext(t, &cfg) + ctx := newTestContext(t, &cfg, makeDB) const ( shardAmt = lnwire.MilliSatoshi(10) @@ -1666,9 +1799,11 @@ func TestAMPWithoutMPPPayload(t *testing.T) { checkFailResolution(t, resolution, invpkg.ResultAmpError) } -// TestSpontaneousAmpPayment tests receiving a spontaneous AMP payment with both +// testSpontaneousAmpPayment tests receiving a spontaneous AMP payment with both // valid and invalid reconstructions. -func TestSpontaneousAmpPayment(t *testing.T) { +func testSpontaneousAmpPayment(t *testing.T, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { + t.Parallel() tests := []struct { @@ -1712,24 +1847,25 @@ func TestSpontaneousAmpPayment(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - testSpontaneousAmpPayment( + testSpontaneousAmpPaymentImpl( t, test.ampEnabled, test.failReconstruction, - test.numShards, + test.numShards, makeDB, ) }) } } // testSpontaneousAmpPayment runs a specific spontaneous AMP test case. -func testSpontaneousAmpPayment( - t *testing.T, ampEnabled, failReconstruction bool, numShards int) { +func testSpontaneousAmpPaymentImpl( + t *testing.T, ampEnabled, failReconstruction bool, numShards int, + makeDB func(t *testing.T) (invpkg.InvoiceDB, *clock.TestClock)) { t.Parallel() defer timeout()() cfg := defaultRegistryConfig() cfg.AcceptAMP = ampEnabled - ctx := newTestContext(t, &cfg) + ctx := newTestContext(t, &cfg, makeDB) ctxb := context.Background() allSubscriptions, err := ctx.registry.SubscribeNotifications(ctxb, 0, 0) diff --git a/invoices/invoices_test.go b/invoices/invoices_test.go index 259a977d4d..caf58f13aa 100644 --- a/invoices/invoices_test.go +++ b/invoices/invoices_test.go @@ -32,8 +32,6 @@ var ( ) testNow = time.Unix(1, 0) - - testClock = clock.NewTestClock(testNow) ) func randInvoice(value lnwire.MilliSatoshi) (*invpkg.Invoice, error) { @@ -93,9 +91,125 @@ func settleTestInvoice(invoice *invpkg.Invoice, settleIndex uint64) { invoice.SettleIndex = settleIndex } -// Tests that pending invoices are those which are either in ContractOpen or -// in ContractAccepted state. +// TestInvoices is a master test which encompasses all tests using an InvoiceDB +// instance. The purpose of this test is to be able to run all tests with a +// custom DB instance, so that we can test the same logic with different DB +// implementations. +func TestInvoices(t *testing.T) { + testList := []struct { + name string + test func(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) + }{ + { + name: "InvoiceWorkflow", + test: testInvoiceWorkflow, + }, + { + name: "AddDuplicatePayAddr", + test: testAddDuplicatePayAddr, + }, + { + name: "AddDuplicateKeysendPayAddr", + test: testAddDuplicateKeysendPayAddr, + }, + { + name: "FailInvoiceLookupMPPPayAddrOnly", + test: testFailInvoiceLookupMPPPayAddrOnly, + }, + { + name: "InvRefEquivocation", + test: testInvRefEquivocation, + }, + { + name: "InvoiceCancelSingleHtlc", + test: testInvoiceCancelSingleHtlc, + }, + { + name: "InvoiceCancelSingleHtlcAMP", + test: testInvoiceCancelSingleHtlcAMP, + }, + { + name: "InvoiceAddTimeSeries", + test: testInvoiceAddTimeSeries, + }, + { + name: "SettleIndexAmpPayments", + test: testSettleIndexAmpPayments, + }, + { + name: "FetchPendingInvoices", + test: testFetchPendingInvoices, + }, + { + name: "DuplicateSettleInvoice", + test: testDuplicateSettleInvoice, + }, + { + name: "QueryInvoices", + test: testQueryInvoices, + }, + { + name: "CustomRecords", + test: testCustomRecords, + }, + { + name: "InvoiceHtlcAMPFields", + test: testInvoiceHtlcAMPFields, + }, + { + name: "AddInvoiceWithHTLCs", + test: testAddInvoiceWithHTLCs, + }, + { + name: "SetIDIndex", + test: testSetIDIndex, + }, + { + name: "UnexpectedInvoicePreimage", + test: testUnexpectedInvoicePreimage, + }, + { + name: "UpdateHTLCPreimages", + test: testUpdateHTLCPreimages, + }, + { + name: "DeleteInvoices", + test: testDeleteInvoices, + }, + { + name: "DeleteCanceledInvoices", + test: testDeleteCanceledInvoices, + }, + { + name: "AddInvoiceInvalidFeatureDeps", + test: testAddInvoiceInvalidFeatureDeps, + }, + } + + makeKeyValueDB := func(t *testing.T) invpkg.InvoiceDB { + db, err := channeldb.MakeTestInvoiceDB( + t, channeldb.OptionClock(clock.NewTestClock(testNow)), + ) + require.NoError(t, err, "unable to make test db") + + return db + } + + for _, test := range testList { + test := test + + t.Run(test.name, func(t *testing.T) { + test.test(t, makeKeyValueDB) + }) + } +} + +// TestInvoiceIsPending tests that pending invoices are those which are either +// in ContractOpen or in ContractAccepted state. func TestInvoiceIsPending(t *testing.T) { + t.Parallel() + contractStates := []invpkg.ContractState{ invpkg.ContractOpen, invpkg.ContractSettled, invpkg.ContractCanceled, invpkg.ContractAccepted, @@ -143,20 +257,24 @@ var invWorkflowTests = []invWorkflowTest{ // TestInvoiceWorkflow asserts the basic process of inserting, fetching, and // updating an invoice. We assert that the flow is successful using when // querying with various combinations of payment hash and payment address. -func TestInvoiceWorkflow(t *testing.T) { +func testInvoiceWorkflow(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + t.Parallel() for _, test := range invWorkflowTests { test := test t.Run(test.name, func(t *testing.T) { - testInvoiceWorkflow(t, test) + t.Parallel() + testInvoiceWorkflowImpl(t, test, makeDB) }) } } -func testInvoiceWorkflow(t *testing.T, test invWorkflowTest) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") +func testInvoiceWorkflowImpl(t *testing.T, test invWorkflowTest, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + db := makeDB(t) // Create a fake invoice which we'll use several times in the tests // below. @@ -290,11 +408,13 @@ func testInvoiceWorkflow(t *testing.T, test invWorkflowTest) { } } -// TestAddDuplicatePayAddr asserts that the payment addresses of inserted +// testAddDuplicatePayAddr asserts that the payment addresses of inserted // invoices are unique. -func TestAddDuplicatePayAddr(t *testing.T) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err) +func testAddDuplicatePayAddr(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) // Create two invoices with the same payment addr. invoice1, err := randInvoice(1000) @@ -317,12 +437,14 @@ func TestAddDuplicatePayAddr(t *testing.T) { require.Error(t, err, invpkg.ErrDuplicatePayAddr) } -// TestAddDuplicateKeysendPayAddr asserts that we permit duplicate payment +// testAddDuplicateKeysendPayAddr asserts that we permit duplicate payment // addresses to be inserted if they are blank to support JIT legacy keysend // invoices. -func TestAddDuplicateKeysendPayAddr(t *testing.T) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err) +func testAddDuplicateKeysendPayAddr(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) // Create two invoices with the same _blank_ payment addr. invoice1, err := randInvoice(1000) @@ -360,13 +482,15 @@ func TestAddDuplicateKeysendPayAddr(t *testing.T) { require.Equal(t, invoice2, &dbInv2) } -// TestFailInvoiceLookupMPPPayAddrOnly asserts that looking up a MPP invoice +// testFailInvoiceLookupMPPPayAddrOnly asserts that looking up a MPP invoice // that matches _only_ by payment address fails with ErrInvoiceNotFound. This // ensures that the HTLC's payment hash always matches the payment hash in the // returned invoice. -func TestFailInvoiceLookupMPPPayAddrOnly(t *testing.T) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err) +func testFailInvoiceLookupMPPPayAddrOnly(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) // Create and insert a random invoice. invoice, err := randInvoice(1000) @@ -391,11 +515,13 @@ func TestFailInvoiceLookupMPPPayAddrOnly(t *testing.T) { require.Equal(t, invpkg.ErrInvoiceNotFound, err) } -// TestInvRefEquivocation asserts that retrieving or updating an invoice using +// testInvRefEquivocation asserts that retrieving or updating an invoice using // an equivocating InvoiceRef results in ErrInvRefEquivocation. -func TestInvRefEquivocation(t *testing.T) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err) +func testInvRefEquivocation(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) // Add two random invoices. invoice1, err := randInvoice(1000) @@ -431,13 +557,13 @@ func TestInvRefEquivocation(t *testing.T) { require.Error(t, err, invpkg.ErrInvRefEquivocation) } -// TestInvoiceCancelSingleHtlc tests that a single htlc can be canceled on the +// testInvoiceCancelSingleHtlc tests that a single htlc can be canceled on the // invoice. -func TestInvoiceCancelSingleHtlc(t *testing.T) { - t.Parallel() +func testInvoiceCancelSingleHtlc(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) preimage := lntypes.Preimage{1} paymentHash := preimage.Hash() @@ -511,16 +637,14 @@ func TestInvoiceCancelSingleHtlc(t *testing.T) { } } -// TestInvoiceCancelSingleHtlcAMP tests that it's possible to cancel a single +// testInvoiceCancelSingleHtlcAMP tests that it's possible to cancel a single // invoice of an AMP HTLC across multiple set IDs, and also have that update // the amount paid and other related fields as well. -func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { - t.Parallel() +func testInvoiceCancelSingleHtlcAMP(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB( - t, channeldb.OptionClock(testClock), - ) - require.NoError(t, err, "unable to make test db: %v", err) + t.Parallel() + db := makeDB(t) // We'll start out by creating an invoice and writing it to the DB. amt := lnwire.NewMSatFromSatoshis(1000) @@ -691,19 +815,17 @@ func TestInvoiceCancelSingleHtlcAMP(t *testing.T) { require.Equal(t, invoice, dbInvoice) } -// TestInvoiceTimeSeries tests that newly added invoices invoices, as well as +// testInvoiceTimeSeries tests that newly added invoices invoices, as well as // settled invoices are added to the database are properly placed in the add // or settle index which serves as an event time series. -func TestInvoiceAddTimeSeries(t *testing.T) { - t.Parallel() +func testInvoiceAddTimeSeries(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB( - t, channeldb.OptionClock(testClock), - ) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) ctxb := context.Background() - _, err = db.InvoicesAddedSince(ctxb, 0) + _, err := db.InvoicesAddedSince(ctxb, 0) require.NoError(t, err) // We'll start off by creating 20 random invoices, and inserting them @@ -849,18 +971,15 @@ func TestInvoiceAddTimeSeries(t *testing.T) { } } -// TestSettleIndexAmpPayments tests that repeated settles of the same invoice +// testSettleIndexAmpPayments tests that repeated settles of the same invoice // end up properly adding entries to the settle index, and the // InvoicesSettledSince will emit a "projected" version of the invoice w/ // _just_ that HTLC information. -func TestSettleIndexAmpPayments(t *testing.T) { - t.Parallel() +func testSettleIndexAmpPayments(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - testClock := clock.NewTestClock(testNow) - db, err := channeldb.MakeTestInvoiceDB( - t, channeldb.OptionClock(testClock), - ) - require.Nil(t, err) + t.Parallel() + db := makeDB(t) // First, we'll make a sample invoice that'll be paid to several times // below. @@ -1022,15 +1141,13 @@ func TestSettleIndexAmpPayments(t *testing.T) { require.Nil(t, err) } -// TestFetchPendingInvoices tests that we can fetch all pending invoices from +// testFetchPendingInvoices tests that we can fetch all pending invoices from // the database using the FetchPendingInvoices method. -func TestFetchPendingInvoices(t *testing.T) { - t.Parallel() +func testFetchPendingInvoices(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB( - t, channeldb.OptionClock(testClock), - ) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) ctxb := context.Background() @@ -1078,16 +1195,14 @@ func TestFetchPendingInvoices(t *testing.T) { require.Equal(t, pendingInvoices, pending) } -// TestDuplicateSettleInvoice tests that if we add a new invoice and settle it +// testDuplicateSettleInvoice tests that if we add a new invoice and settle it // twice, then the second time we also receive the invoice that we settled as a // return argument. -func TestDuplicateSettleInvoice(t *testing.T) { - t.Parallel() +func testDuplicateSettleInvoice(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB( - t, channeldb.OptionClock(testClock), - ) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) // We'll start out by creating an invoice and writing it to the DB. amt := lnwire.NewMSatFromSatoshis(1000) @@ -1142,15 +1257,13 @@ func TestDuplicateSettleInvoice(t *testing.T) { ) } -// TestQueryInvoices ensures that we can properly query the invoice database for +// testQueryInvoices ensures that we can properly query the invoice database for // invoices using different types of queries. -func TestQueryInvoices(t *testing.T) { - t.Parallel() +func testQueryInvoices(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB( - t, channeldb.OptionClock(testClock), - ) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) // To begin the test, we'll add 50 invoices to the database. We'll // assume that the index of the invoice within the database is the same @@ -1577,13 +1690,13 @@ func getUpdateInvoice(amt lnwire.MilliSatoshi) invpkg.InvoiceUpdateCallback { } } -// TestCustomRecords tests that custom records are properly recorded in the +// testCustomRecords tests that custom records are properly recorded in the // invoice database. -func TestCustomRecords(t *testing.T) { - t.Parallel() +func testCustomRecords(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) preimage := lntypes.Preimage{1} paymentHash := preimage.Hash() @@ -1630,7 +1743,7 @@ func TestCustomRecords(t *testing.T) { }, nil } - _, err = db.UpdateInvoice(ctxb, ref, nil, callback) + _, err := db.UpdateInvoice(ctxb, ref, nil, callback) require.NoError(t, err, "unable to add invoice htlc") // Retrieve the invoice from that database and verify that the custom @@ -1648,20 +1761,27 @@ func TestCustomRecords(t *testing.T) { ) } -// TestInvoiceHtlcAMPFields asserts that the set id and preimage fields are +// testInvoiceHtlcAMPFields asserts that the set id and preimage fields are // properly recorded when updating an invoice. -func TestInvoiceHtlcAMPFields(t *testing.T) { +func testInvoiceHtlcAMPFields(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + t.Run("amp", func(t *testing.T) { - testInvoiceHtlcAMPFields(t, true) + t.Parallel() + testInvoiceHtlcAMPFieldsImpl(t, true, makeDB) }) t.Run("no amp", func(t *testing.T) { - testInvoiceHtlcAMPFields(t, false) + t.Parallel() + testInvoiceHtlcAMPFieldsImpl(t, false, makeDB) }) } -func testInvoiceHtlcAMPFields(t *testing.T, isAMP bool) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.Nil(t, err) +func testInvoiceHtlcAMPFieldsImpl(t *testing.T, isAMP bool, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + db := makeDB(t) testInvoice, err := randInvoice(1000) require.Nil(t, err) @@ -1727,6 +1847,8 @@ func testInvoiceHtlcAMPFields(t *testing.T, isAMP bool) { // TestInvoiceRef asserts that the proper identifiers are returned from an // InvoiceRef depending on the constructor used. func TestInvoiceRef(t *testing.T) { + t.Parallel() + payHash := lntypes.Hash{0x01} payAddr := [32]byte{0x02} setID := [32]byte{0x03} @@ -1765,6 +1887,8 @@ func TestInvoiceRef(t *testing.T) { // not comingle, and also that HTLCs with disjoint set ids appear in different // sets. func TestHTLCSet(t *testing.T) { + t.Parallel() + inv := &invpkg.Invoice{ Htlcs: make(map[models.CircuitKey]*invpkg.InvoiceHTLC), } @@ -1859,11 +1983,13 @@ func TestHTLCSet(t *testing.T) { } } -// TestAddInvoiceWithHTLCs asserts that you can't insert an invoice that already +// testAddInvoiceWithHTLCs asserts that you can't insert an invoice that already // has HTLCs. -func TestAddInvoiceWithHTLCs(t *testing.T) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.Nil(t, err) +func testAddInvoiceWithHTLCs(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) testInvoice, err := randInvoice(1000) require.Nil(t, err) @@ -1876,15 +2002,12 @@ func TestAddInvoiceWithHTLCs(t *testing.T) { require.Equal(t, invpkg.ErrInvoiceHasHtlcs, err) } -// TestSetIDIndex asserts that the set id index properly adds new invoices as we +// testSetIDIndex asserts that the set id index properly adds new invoices as we // accept HTLCs, that they can be queried by their set id after accepting, and // that invoices with duplicate set ids are disallowed. -func TestSetIDIndex(t *testing.T) { - testClock := clock.NewTestClock(testNow) - db, err := channeldb.MakeTestInvoiceDB( - t, channeldb.OptionClock(testClock), - ) - require.Nil(t, err) +func testSetIDIndex(t *testing.T, makeDB func(t *testing.T) invpkg.InvoiceDB) { + t.Parallel() + db := makeDB(t) // We'll start out by creating an invoice and writing it to the DB. amt := lnwire.NewMSatFromSatoshis(1000) @@ -2217,16 +2340,16 @@ func getUpdateInvoiceAMPSettle(setID *[32]byte, preimage [32]byte, } } -// TestUnexpectedInvoicePreimage asserts that legacy or MPP invoices cannot be +// testUnexpectedInvoicePreimage asserts that legacy or MPP invoices cannot be // settled when referenced by payment address only. Since regular or MPP // payments do not store the payment hash explicitly (it is stored in the // index), this enforces that they can only be updated using a InvoiceRefByHash // or InvoiceRefByHashOrAddr. -func TestUnexpectedInvoicePreimage(t *testing.T) { - t.Parallel() +func testUnexpectedInvoicePreimage(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) invoice, err := randInvoice(lnwire.MilliSatoshi(100)) require.NoError(t, err) @@ -2256,9 +2379,11 @@ type updateHTLCPreimageTestCase struct { expError error } -// TestUpdateHTLCPreimages asserts various properties of setting HTLC-level +// testUpdateHTLCPreimages asserts various properties of setting HTLC-level // preimages on invoice state transitions. -func TestUpdateHTLCPreimages(t *testing.T) { +func testUpdateHTLCPreimages(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + t.Parallel() tests := []updateHTLCPreimageTestCase{ @@ -2277,15 +2402,16 @@ func TestUpdateHTLCPreimages(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - testUpdateHTLCPreimages(t, test) + t.Parallel() + testUpdateHTLCPreimagesImpl(t, test, makeDB) }) } } -func testUpdateHTLCPreimages(t *testing.T, test updateHTLCPreimageTestCase) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") +func testUpdateHTLCPreimagesImpl(t *testing.T, test updateHTLCPreimageTestCase, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + db := makeDB(t) // We'll start out by creating an invoice and writing it to the DB. amt := lnwire.NewMSatFromSatoshis(1000) invoice, err := randInvoice(amt) @@ -2350,13 +2476,13 @@ func testUpdateHTLCPreimages(t *testing.T, test updateHTLCPreimageTestCase) { require.Equal(t, test.expError, err) } -// TestDeleteInvoices tests that deleting a list of invoices will succeed +// testDeleteInvoices tests that deleting a list of invoices will succeed // if all delete references are valid, or will fail otherwise. -func TestDeleteInvoices(t *testing.T) { - t.Parallel() +func testDeleteInvoices(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) // Add some invoices to the test db. numInvoices := 3 @@ -2434,13 +2560,13 @@ func TestDeleteInvoices(t *testing.T) { assertInvoiceCount(0) } -// TestDeleteCanceledInvoices tests that deleting canceled invoices with the +// testDeleteCanceledInvoices tests that deleting canceled invoices with the // specific DeleteCanceledInvoices method works correctly. -func TestDeleteCanceledInvoices(t *testing.T) { - t.Parallel() +func testDeleteCanceledInvoices(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) // Updatefunc is used to cancel an invoice. updateFunc := func(invoice *invpkg.Invoice) ( @@ -2493,13 +2619,13 @@ func TestDeleteCanceledInvoices(t *testing.T) { require.Equal(t, invoices, dbInvoices.Invoices) } -// TestAddInvoiceInvalidFeatureDeps asserts that inserting an invoice with +// testAddInvoiceInvalidFeatureDeps asserts that inserting an invoice with // invalid transitive feature dependencies fails with the appropriate error. -func TestAddInvoiceInvalidFeatureDeps(t *testing.T) { - t.Parallel() +func testAddInvoiceInvalidFeatureDeps(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { - db, err := channeldb.MakeTestInvoiceDB(t) - require.NoError(t, err, "unable to make test db") + t.Parallel() + db := makeDB(t) invoice, err := randInvoice(500) require.NoError(t, err) diff --git a/invoices/test_utils_test.go b/invoices/test_utils_test.go index 13cd11184d..dd2638408a 100644 --- a/invoices/test_utils_test.go +++ b/invoices/test_utils_test.go @@ -16,7 +16,6 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/chainntnfs" - "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/clock" invpkg "github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/lntypes" @@ -131,26 +130,8 @@ var ( testInvoiceCreationDate = testTime ) -func newTestChannelDB(t *testing.T, clock clock.Clock) (*channeldb.DB, error) { - t.Helper() - - // Create channeldb for the first time. - cdb, err := channeldb.Open( - t.TempDir(), channeldb.OptionClock(clock), - ) - if err != nil { - return nil, err - } - - t.Cleanup(func() { - cdb.Close() - }) - - return cdb, nil -} - type testContext struct { - idb *channeldb.DB + idb invpkg.InvoiceDB registry *invpkg.InvoiceRegistry notifier *mockChainNotifier clock *clock.TestClock @@ -166,17 +147,13 @@ func defaultRegistryConfig() invpkg.RegistryConfig { } func newTestContext(t *testing.T, - registryCfg *invpkg.RegistryConfig) *testContext { + registryCfg *invpkg.RegistryConfig, + makeDB func(t *testing.T) (invpkg.InvoiceDB, + *clock.TestClock)) *testContext { t.Helper() - clock := clock.NewTestClock(testTime) - - idb, err := newTestChannelDB(t, clock) - if err != nil { - t.Fatal(err) - } - + idb, clock := makeDB(t) notifier := newMockNotifier() expiryWatcher := invpkg.NewInvoiceExpiryWatcher( @@ -192,10 +169,7 @@ func newTestContext(t *testing.T, // Instantiate and start the invoice ctx.registry. registry := invpkg.NewRegistry(idb, expiryWatcher, &cfg) - err = registry.Start() - if err != nil { - t.Fatal(err) - } + require.NoError(t, registry.Start()) t.Cleanup(func() { require.NoError(t, registry.Stop()) }) From f5abc94bdf9bfe5dcae7fc98b4edfa41c3c21ccb Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Fri, 26 Jan 2024 19:43:07 +0100 Subject: [PATCH 14/15] mod: bump kvdb to v1.4.5 --- go.mod | 13 +++---------- go.sum | 38 +++++++------------------------------- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index 9f282bb360..4e98ebdb93 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/jessevdk/go-flags v1.4.0 github.com/jrick/logrotate v1.0.0 github.com/kkdai/bstream v1.0.0 - github.com/lib/pq v1.10.3 + github.com/lib/pq v1.10.4 github.com/lightninglabs/neutrino v0.16.0 github.com/lightninglabs/neutrino/cache v1.1.1 github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f @@ -41,7 +41,7 @@ require ( github.com/lightningnetwork/lnd/clock v1.1.1 github.com/lightningnetwork/lnd/fn v1.0.1 github.com/lightningnetwork/lnd/healthcheck v1.2.3 - github.com/lightningnetwork/lnd/kvdb v1.4.4 + github.com/lightningnetwork/lnd/kvdb v1.4.5 github.com/lightningnetwork/lnd/queue v1.1.1 github.com/lightningnetwork/lnd/ticker v1.1.1 github.com/lightningnetwork/lnd/tlv v1.2.1 @@ -93,9 +93,8 @@ require ( github.com/docker/docker v24.0.7+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dsnet/compress v0.0.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect - github.com/fergusstrange/embedded-postgres v1.10.0 // indirect + github.com/fergusstrange/embedded-postgres v1.25.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect @@ -120,24 +119,19 @@ require ( github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/klauspost/compress v1.13.6 // indirect - github.com/klauspost/pgzip v1.2.5 // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mholt/archiver/v3 v3.5.0 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect - github.com/nwaples/rardecode v1.1.2 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/onsi/gomega v1.26.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.5 // indirect - github.com/pierrec/lz4/v4 v4.1.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect @@ -154,7 +148,6 @@ require ( github.com/stretchr/objx v0.5.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect - github.com/ulikunitz/xz v0.5.11 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index 7e56a0b5bc..032dfab0fe 100644 --- a/go.sum +++ b/go.sum @@ -61,7 +61,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM= github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -182,9 +181,6 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= -github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= -github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -196,8 +192,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= -github.com/fergusstrange/embedded-postgres v1.10.0 h1:YnwF6xAQYmKLAXXrrRx4rHDLih47YJwVPvg8jeKfdNg= -github.com/fergusstrange/embedded-postgres v1.10.0/go.mod h1:a008U8/Rws5FtIOTGYDYa7beVWsT3qVKyqExqYYjL+c= +github.com/fergusstrange/embedded-postgres v1.25.0 h1:sa+k2Ycrtz40eCRPOzI7Ry7TtkWXXJ+YRsxpKMDhxK0= +github.com/fergusstrange/embedded-postgres v1.25.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= @@ -257,7 +253,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -408,14 +403,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= -github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= -github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -430,10 +417,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= -github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= github.com/lightninglabs/neutrino v0.16.0 h1:YNTQG32fPR/Zg0vvJVI65OBH8l3U18LSXXtX91hx0q0= @@ -453,8 +439,8 @@ github.com/lightningnetwork/lnd/fn v1.0.1 h1:4nAxKpGKgk4/xRQKxvim3BW0QM34S4BH6Qg github.com/lightningnetwork/lnd/fn v1.0.1/go.mod h1:XV+0vBXSnh3aUjskJUv58TOpsveiXQ+ac8rEnXZDGFc= github.com/lightningnetwork/lnd/healthcheck v1.2.3 h1:oqhOOy8WmIEa6RBkYKC0mmYZkhl8T2kGD97n9jpML8o= github.com/lightningnetwork/lnd/healthcheck v1.2.3/go.mod h1:eDxH3dEwV9DeBW/6inrmlVh1qBOFV0AI14EEPnGt9gc= -github.com/lightningnetwork/lnd/kvdb v1.4.4 h1:bCv63rVCvzqj1BkagN/EWTov6NDDgYEG/t0z2HepRMk= -github.com/lightningnetwork/lnd/kvdb v1.4.4/go.mod h1:9SuaIqMA9ugrVkdvgQkYXa8CAKYNYd4vsEYORP4V698= +github.com/lightningnetwork/lnd/kvdb v1.4.5 h1:wwX3hbFTsnxEIL5X2Pszq1o3Fd2OZGdyWIMr9QrMxL8= +github.com/lightningnetwork/lnd/kvdb v1.4.5/go.mod h1:oaGL6R/qwazM7hPurg8jSPYsWw3cGEOt6YJDs5TUNos= github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= @@ -481,8 +467,6 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE= -github.com/mholt/archiver/v3 v3.5.0/go.mod h1:qqTTPUK/HZPFgFQ/TJ3BzvTpF/dPtFVJXdQbCmeMxwc= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= @@ -500,9 +484,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= -github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M= -github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -528,9 +509,6 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= -github.com/pierrec/lz4/v4 v4.0.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.8 h1:ieHkV+i2BRzngO4Wd/3HGowuZStgq6QkPsD1eolNAO4= -github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -599,7 +577,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -613,8 +590,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= -github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= -github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw= github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -682,6 +657,7 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= From bcc6a3f5170afa4acab71f405ead290d10eb2f25 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Thu, 26 Oct 2023 14:42:20 +0200 Subject: [PATCH 15/15] docs: update release notes for 0.18.0 --- docs/release-notes/release-notes-0.18.0.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md index 1323c1f0ef..fcda0d2a43 100644 --- a/docs/release-notes/release-notes-0.18.0.md +++ b/docs/release-notes/release-notes-0.18.0.md @@ -296,6 +296,9 @@ * [Update](https://github.com/lightningnetwork/lnd/pull/8419) the embedded Postgres version and raise max connections. +* [Refactor UpdateInvoice](https://github.com/lightningnetwork/lnd/pull/8100) to + make it simpler to adjust code to also support SQL InvoiceDB implementation. + ## Code Health * [Remove database pointers](https://github.com/lightningnetwork/lnd/pull/8117)