diff --git a/channeldb/invoice_test.go b/channeldb/invoice_test.go index d7bbc5b5c5..3fe6b668e8 100644 --- a/channeldb/invoice_test.go +++ b/channeldb/invoice_test.go @@ -2,3174 +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) -} - -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() - _, err := updateHtlc(testNow, htlc, test.invState, test.setID) - 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) { - 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/channeldb/invoices.go b/channeldb/invoices.go index 9ffc6aa27d..a7663cabf6 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 } @@ -606,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) { @@ -641,10 +644,37 @@ 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 + } + + 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, setIDHint, invoices, settleIndex, setIDIndex, - invoiceNum, callback, + updatedInvoice, err = invpkg.UpdateInvoice( + payHash, updater.invoice, now, callback, updater, ) return err @@ -655,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 @@ -1724,929 +2064,6 @@ 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 update sthe 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) { - - 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{}), - } - } - - 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 { - // 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 -} - -// 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) { - - 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 - - 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 - } - } - - // 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 - } -} - -// 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) { - - // 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. - ampState := invoice.AMPState[setID] - ampState.State = invpkg.HtlcStateSettled - - ampState.InvoiceKeys[circuitKey] = struct{}{} - - invoice.AMPState[setID] = ampState - - // 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 -} - -// 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 - } - - // 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: - return d.cancelHTLCs(invoices, invoiceNum, &invoice, update) - - case invpkg.AddHTLCsUpdate: - return d.addHTLCs( - invoices, settleIndex, setIDIndex, invoiceNum, &invoice, - hash, update, - ) - - case invpkg.SettleHodlInvoiceUpdate: - return d.settleHodlInvoice( - invoices, settleIndex, invoiceNum, &invoice, hash, - update.State, - ) - - case invpkg.CancelInvoiceUpdate: - return d.cancelInvoice( - invoices, invoiceNum, &invoice, hash, update.State, - ) - - default: - return nil, fmt.Errorf("unknown update type: %s", - update.UpdateType) - } -} - -// 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() - - // Process add actions from update descriptor. - 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 - - // Check whether this htlc needs to be canceled. If it does, - // update the htlc state to Canceled. - _, cancel := cancelHtlcs[key] - if !cancel { - continue - } - - err := cancelSingleHtlc(timestamp, htlc, invoice.State) - if err != nil { - 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() { - cancelHtlcsAmp( - invoice, htlcsAmpUpdate, htlc, key, - ) - } - } - - // 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.serializeAndStoreInvoice(invoices, invoiceNum, invoice) - 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. - if invoice.IsAMP() { - err := updateAMPInvoices(invoices, invoiceNum, htlcsAmpUpdate) - if err != nil { - return nil, err - } - } - - return invoice, 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) { - - 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) - } - - // Force caller to supply htlc without custom records in a - // consistent way. - if htlcUpdate.CustomRecords == nil { - 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, - Expiry: htlcUpdate.Expiry, - AcceptHeight: uint32(htlcUpdate.AcceptHeight), - AcceptTime: timestamp, - State: invpkg.HtlcStateAccepted, - CustomRecords: htlcUpdate.CustomRecords, - AMP: htlcUpdate.AMP.Copy(), - } - - 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 { - updateHtlcsAmp( - invoice, htlcsAmpUpdate, htlc, - htlcUpdate.AMP.Record.SetID(), key, - ) - } - } - - // 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 := updateInvoiceState( - invoice, hash, *update.State, - ) - if err != nil { - return nil, 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 instead - // 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 - // 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 ( - settledSetIDs = make(map[invpkg.SetID]struct{}) - 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: - 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 - } - } - - // 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 - } - htlcSettled, err := updateHtlc( - timestamp, htlc, htlcContextState, setID, - ) - if err != nil { - return nil, err - } - - // 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 { - settleHtlcsAmp( - invoice, settledSetIDs, htlcsAmpUpdate, htlc, - key, - ) - } - - 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 { - invoice.AmtPaid = amtPaid - } else { - invoice.AmtPaid += amtPaid - } - - // 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 nil, err - } - } - - err := d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) - 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. - if invoiceIsAMP { - err := updateAMPInvoices(invoices, invoiceNum, htlcsAmpUpdate) - if err != nil { - return nil, err - } - } - - return invoice, nil -} - -// 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) { - - if !invoice.HodlInvoice { - return nil, 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 nil, 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: " + - "preimage is nil") - } - - // TODO(positiveblue): create a invoice.CanSettleHodlInvoice func. - newState, err := updateInvoiceState(invoice, hash, *update) - if err != nil { - return nil, err - } - - if newState == nil || *newState != invpkg.ContractSettled { - return nil, fmt.Errorf("unable to settle hodl invoice: "+ - "new computed state is not settled: %s", newState) - } - - 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 { - _, err := updateHtlc( - timestamp, htlc, invpkg.ContractSettled, nil, - ) - if err != nil { - return nil, err - } - - if htlc.State == invpkg.HtlcStateSettled { - amtPaid += htlc.Amt - } - } - - invoice.AmtPaid = amtPaid - - err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) - if err != nil { - return nil, err - } - - return invoice, 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, - invoice *invpkg.Invoice, hash *lntypes.Hash, - update *invpkg.InvoiceStateUpdateDesc) (*invpkg.Invoice, error) { - - switch { - case update == nil: - fallthrough - - case update.NewState != invpkg.ContractCanceled: - return nil, 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 := updateInvoiceState(invoice, hash, *update) - if err != nil { - return nil, err - } - - if newState == nil || *newState != invpkg.ContractCanceled { - return nil, fmt.Errorf("unable to cancel invoice(%v): new "+ - "computed state is not canceled: %s", invoice.AddIndex, - newState) - } - - invoice.State = invpkg.ContractCanceled - timestamp := d.clock.Now() - - // TODO(positiveblue): this logic can be simplified. - for _, htlc := range invoice.Htlcs { - _, err := updateHtlc( - timestamp, htlc, invpkg.ContractCanceled, setID, - ) - if err != nil { - return nil, err - } - } - - err = d.serializeAndStoreInvoice(invoices, invoiceNum, invoice) - if err != nil { - return nil, err - } - - return invoice, nil -} - -// 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, - 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 - } - 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 - // 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") - } -} - -// cancelSingleHtlc validates cancellation of a single htlc and update its -// state. -func cancelSingleHtlc(resolveTime time.Time, htlc *invpkg.InvoiceHTLC, - invState 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) - } - - // 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) - } - - htlc.State = invpkg.HtlcStateCanceled - htlc.ResolveTime = resolveTime - - 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) { - - trySettle := func(persist bool) (bool, error) { - if htlc.State != invpkg.HtlcStateAccepted { - return false, 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 - 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, invpkg.ErrHTLCPreimageMissing - - // Fail if the accepted AMP HTLC has an invalid - // preimage. - case !htlc.AMP.Preimage.Matches(htlc.AMP.Hash): - return false, invpkg.ErrHTLCPreimageMismatch - } - - htlcState = invpkg.HtlcStateSettled - } - - // 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 - } - - return persist && htlcState == invpkg.HtlcStateSettled, nil - } - - if invState == 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, invpkg.ErrHTLCAlreadySettled - } - - switch invState { - case invpkg.ContractCanceled: - if htlc.State == invpkg.HtlcStateAccepted { - htlc.State = invpkg.HtlcStateCanceled - htlc.ResolveTime = resolveTime - } - return false, 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, nil - - default: - return false, errors.New("unknown state transition") - } -} - -// 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/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) 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= 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 +} 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 new file mode 100644 index 0000000000..caf58f13aa --- /dev/null +++ b/invoices/invoices_test.go @@ -0,0 +1,2646 @@ +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) +) + +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 +} + +// 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, + } + + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + + for _, test := range invWorkflowTests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + testInvoiceWorkflowImpl(t, test, makeDB) + }) + } +} + +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. + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + // 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, + 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) + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + // 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + // 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + 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, + 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) + 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 +// or settle index which serves as an event time series. +func testInvoiceAddTimeSeries(t *testing.T, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + // 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + 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, + 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) + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + 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 + // 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + + t.Run("amp", func(t *testing.T) { + t.Parallel() + testInvoiceHtlcAMPFieldsImpl(t, true, makeDB) + }) + t.Run("no amp", func(t *testing.T) { + t.Parallel() + testInvoiceHtlcAMPFieldsImpl(t, false, makeDB) + }) +} + +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) + + 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) { + t.Parallel() + + 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) { + t.Parallel() + + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + 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, 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) + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + 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) { + t.Parallel() + testUpdateHTLCPreimagesImpl(t, test, makeDB) + }) + } +} + +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) + 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + // 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + // 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, + makeDB func(t *testing.T) invpkg.InvoiceDB) { + + t.Parallel() + db := makeDB(t) + + 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) +} 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()) }) diff --git a/invoices/update_invoice.go b/invoices/update_invoice.go new file mode 100644 index 0000000000..a2de1b8f21 --- /dev/null +++ b/invoices/update_invoice.go @@ -0,0 +1,826 @@ +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. +// +//nolint:funlen +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) +}