From 6b0931af82e7f3539bf892b0a2a37f4e64fb4e93 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 19 Feb 2024 20:22:54 +0100 Subject: [PATCH] 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 d6d1eaf7a..d0d0518a0 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 d134c03b4..a7663cabf 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 000000000..dc81d9db0 --- /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 000000000..42d370971 --- /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) +}