package channeldb import ( "bytes" "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.TLVOnionPayloadOptional, 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 := MakeTestDB(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) } // 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(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(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(ref, nil, getUpdateInvoice(payAmt)) require.NoError(t, err, "unable to settle invoice") dbInvoice2, err := db.LookupInvoice(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(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(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(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(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 := MakeTestDB(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 // First insert should succeed. inv1Hash := invoice1.Terms.PaymentPreimage.Hash() _, err = db.AddInvoice(invoice1, inv1Hash) require.NoError(t, err) // Second insert should fail with duplicate payment addr. inv2Hash := invoice2.Terms.PaymentPreimage.Hash() _, err = db.AddInvoice(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 := MakeTestDB(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 // Inserting both should succeed without a duplicate payment address // failure. inv1Hash := invoice1.Terms.PaymentPreimage.Hash() _, err = db.AddInvoice(invoice1, inv1Hash) require.NoError(t, err) inv2Hash := invoice2.Terms.PaymentPreimage.Hash() _, err = db.AddInvoice(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(ref1) require.NoError(t, err) require.Equal(t, invoice1, &dbInv1) ref2 := invpkg.InvoiceRefByHashAndAddr(inv2Hash, invpkg.BlankPayAddr) dbInv2, err := db.LookupInvoice(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 := MakeTestDB(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 _, err = db.AddInvoice(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(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 := MakeTestDB(t) require.NoError(t, err) // Add two random invoices. invoice1, err := randInvoice(1000) require.NoError(t, err) inv1Hash := invoice1.Terms.PaymentPreimage.Hash() _, err = db.AddInvoice(invoice1, inv1Hash) require.NoError(t, err) invoice2, err := randInvoice(2000) require.NoError(t, err) inv2Hash := invoice2.Terms.PaymentPreimage.Hash() _, err = db.AddInvoice(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(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(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 := MakeTestDB(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, }, } if _, err := db.AddInvoice(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(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(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 := MakeTestDB(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 preimage := *invoice.Terms.PaymentPreimage payHash := preimage.Hash() _, err = db.AddInvoice(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( ref, (*invpkg.SetID)(setID1), updateAcceptAMPHtlc(0, amt, setID1, true), ) require.Nil(t, err) // The second set ID with two HTLCs added. _, err = db.UpdateInvoice( ref, (*invpkg.SetID)(setID2), updateAcceptAMPHtlc(1, amt, setID2, true), ) require.Nil(t, err) dbInvoice, err := db.UpdateInvoice( 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(ref, (*invpkg.SetID)(setID1), callback) require.NoError(t, err, "unable to cancel htlc") freshInvoice, err := db.LookupInvoice(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(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(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(ref, (*invpkg.SetID)(setID2), callback) require.NoError(t, err, "unable to cancel htlc") freshInvoice, err = db.LookupInvoice(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 := MakeTestDB(t, OptionClock(testClock)) require.NoError(t, err, "unable to make test db") _, err = db.InvoicesAddedSince(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() if _, err := db.AddInvoice(invoice, paymentHash); 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(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(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( 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(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 := MakeTestDB(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. preimage := *testInvoice.Terms.PaymentPreimage payHash := preimage.Hash() _, err = db.AddInvoice(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( ref, (*invpkg.SetID)(setID1), updateAcceptAMPHtlc(1, amt, setID1, true), ) require.Nil(t, err) _, err = db.UpdateInvoice( ref, (*invpkg.SetID)(setID2), updateAcceptAMPHtlc(2, amt, setID2, true), ) require.Nil(t, err) _, err = db.UpdateInvoice( 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(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(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( ref, (*invpkg.SetID)(setID1), getUpdateInvoiceAMPSettle( setID1, preimage, models.CircuitKey{HtlcID: 1}, ), ) require.Nil(t, err) _, err = db.UpdateInvoice( ref, (*invpkg.SetID)(setID2), getUpdateInvoiceAMPSettle( setID2, preimage, models.CircuitKey{HtlcID: 2}, ), ) require.Nil(t, err) _, err = db.UpdateInvoice( 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(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( 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(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([]invpkg.InvoiceDeleteRef{ { PayHash: payHash, PayAddr: &testInvoice.Terms.PaymentAddr, AddIndex: testInvoice.AddIndex, }, }) require.Nil(t, err) } // TestScanInvoices tests that ScanInvoices scans through all stored invoices // correctly. func TestScanInvoices(t *testing.T) { t.Parallel() db, err := MakeTestDB(t) require.NoError(t, err, "unable to make test db") var invoices map[lntypes.Hash]*invpkg.Invoice callCount := 0 resetCount := 0 // reset is used to reset/initialize results and is called once // upon calling ScanInvoices and when the underlying transaction is // retried. reset := func() { invoices = make(map[lntypes.Hash]*invpkg.Invoice) callCount = 0 resetCount++ } scanFunc := func(paymentHash lntypes.Hash, invoice *invpkg.Invoice) error { invoices[paymentHash] = invoice callCount++ return nil } // With an empty DB we expect to not scan any invoices. require.NoError(t, db.ScanInvoices(scanFunc, reset)) require.Equal(t, 0, len(invoices)) require.Equal(t, 0, callCount) require.Equal(t, 1, resetCount) numInvoices := 5 testInvoices := make(map[lntypes.Hash]*invpkg.Invoice) // Now populate the DB and check if we can get all invoices with their // payment hashes as expected. for i := 1; i <= numInvoices; i++ { invoice, err := randInvoice(lnwire.MilliSatoshi(i)) require.NoError(t, err) paymentHash := invoice.Terms.PaymentPreimage.Hash() testInvoices[paymentHash] = invoice _, err = db.AddInvoice(invoice, paymentHash) require.NoError(t, err) } resetCount = 0 require.NoError(t, db.ScanInvoices(scanFunc, reset)) require.Equal(t, numInvoices, callCount) require.Equal(t, testInvoices, invoices) require.Equal(t, 1, resetCount) } // 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 := MakeTestDB(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() if _, err := db.AddInvoice(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(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(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 := MakeTestDB(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 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(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( 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: time.Unix(25, 0), }, expected: invoices[:25], }, // Fetch invoices >= 26 creation date. { query: invpkg.InvoiceQuery{ NumMaxInvoices: numInvoices, CreationDateStart: time.Unix(26, 0), }, expected: invoices[25:], }, // Fetch pending invoices <= 25 by creation date. { query: invpkg.InvoiceQuery{ PendingOnly: true, NumMaxInvoices: numInvoices, CreationDateEnd: time.Unix(25, 0), }, expected: pendingInvoices[:13], }, // Fetch pending invoices >= 26 creation date. { query: invpkg.InvoiceQuery{ PendingOnly: true, NumMaxInvoices: numInvoices, CreationDateStart: time.Unix(26, 0), }, expected: pendingInvoices[13:], }, // Fetch pending invoices with offset and end creation date. { query: invpkg.InvoiceQuery{ IndexOffset: 20, NumMaxInvoices: numInvoices, CreationDateEnd: time.Unix(30, 0), }, // 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: time.Unix(11, 0), }, // 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: time.Unix(11, 0), CreationDateEnd: time.Unix(20, 0), }, expected: invoices[10:20], }, // Fetch pending invoices with start and end creation date. { query: invpkg.InvoiceQuery{ PendingOnly: true, NumMaxInvoices: numInvoices, CreationDateStart: time.Unix(11, 0), CreationDateEnd: time.Unix(20, 0), }, expected: pendingInvoices[5:10], }, // Fetch invoices with start and end creation date in reverse // order. { query: invpkg.InvoiceQuery{ Reversed: true, NumMaxInvoices: numInvoices, CreationDateStart: time.Unix(11, 0), CreationDateEnd: time.Unix(20, 0), }, 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: time.Unix(11, 0), CreationDateEnd: time.Unix(20, 0), }, 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: time.Unix(20, 0), CreationDateEnd: time.Unix(11, 0), }, expected: nil, }, } for i, testCase := range testCases { response, err := db.QueryInvoices(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 := MakeTestDB(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, }, } if _, err := db.AddInvoice(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(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(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 := MakeTestDB(t) require.Nil(t, err) testInvoice, err := randInvoice(1000) require.Nil(t, err) if isAMP { testInvoice.Terms.Features = ampFeatures } payHash := testInvoice.Terms.PaymentPreimage.Hash() _, err = db.AddInvoice(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(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(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 := MakeTestDB(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(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 := MakeTestDB(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 preimage := *invoice.Terms.PaymentPreimage payHash := preimage.Hash() _, err = db.AddInvoice(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( 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(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(invoice2, payHash2) require.Nil(t, err) ref2 := invpkg.InvoiceRefByHashAndAddr( payHash2, invoice2.Terms.PaymentAddr, ) _, err = db.UpdateInvoice( 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( ref, (*invpkg.SetID)(setID2), updateAcceptAMPHtlc(1, amt, setID2, false), ) require.Nil(t, err) dbInvoice, err = db.UpdateInvoice( 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(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(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( 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( ref, (*invpkg.SetID)(setID), getUpdateInvoiceAMPSettle( setID, preimage, models.CircuitKey{HtlcID: 0}, ), ) require.Nil(t, err) freshInvoice, err = db.LookupInvoice(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( 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( ref, (*invpkg.SetID)(setID2), getUpdateInvoiceAMPSettle( setID2, preimage, models.CircuitKey{HtlcID: 1}, models.CircuitKey{HtlcID: 2}, ), ) require.Nil(t, err) freshInvoice, err = db.LookupInvoice(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(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 := MakeTestDB(t) require.NoError(t, err, "unable to make test db") invoice, err := randInvoice(lnwire.MilliSatoshi(100)) require.NoError(t, err) // Add a random invoice indexed by payment hash and payment addr. paymentHash := invoice.Terms.PaymentPreimage.Hash() _, err = db.AddInvoice(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( 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 := MakeTestDB(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 _, err = db.AddInvoice(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( 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(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 := MakeTestDB(t) require.NoError(t, err, "unable to make test db") // Add some invoices to the test db. numInvoices := 3 invoicesToDelete := make([]invpkg.InvoiceDeleteRef, numInvoices) 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(invoice, paymentHash) require.NoError(t, err) // Settle the second invoice. if i == 1 { invoice, err = db.UpdateInvoice( 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(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(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(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(invoicesToDelete)) assertInvoiceCount(3) // Restore the add index. invoicesToDelete[2].AddIndex ^= 13 // Delete should succeed with all the valid references. require.NoError(t, db.DeleteInvoice(invoicesToDelete)) assertInvoiceCount(0) } // 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 := MakeTestDB(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.TLVOnionPayloadOptional, lnwire.MPPOptional, ), lnwire.Features, ) hash := invoice.Terms.PaymentPreimage.Hash() _, err = db.AddInvoice(invoice, hash) require.Error(t, err, feature.NewErrMissingFeatureDep( lnwire.PaymentAddrOptional, )) } // TestEncodeDecodeAmpInvoiceState asserts that the nested TLV // encoding+decoding for the AMPInvoiceState struct works as expected. func TestEncodeDecodeAmpInvoiceState(t *testing.T) { t.Parallel() setID1 := [32]byte{1} setID2 := [32]byte{2} setID3 := [32]byte{3} circuitKey1 := models.CircuitKey{ ChanID: lnwire.NewShortChanIDFromInt(1), HtlcID: 1, } circuitKey2 := models.CircuitKey{ ChanID: lnwire.NewShortChanIDFromInt(2), HtlcID: 2, } circuitKey3 := models.CircuitKey{ ChanID: lnwire.NewShortChanIDFromInt(2), HtlcID: 3, } // Make a sample invoice state map that we'll encode then decode to // assert equality of. ampState := invpkg.AMPInvoiceState{ setID1: invpkg.InvoiceStateAMP{ State: invpkg.HtlcStateSettled, SettleDate: testNow, SettleIndex: 1, InvoiceKeys: map[models.CircuitKey]struct{}{ circuitKey1: {}, circuitKey2: {}, }, AmtPaid: 5, }, setID2: invpkg.InvoiceStateAMP{ State: invpkg.HtlcStateCanceled, SettleDate: testNow, SettleIndex: 2, InvoiceKeys: map[models.CircuitKey]struct{}{ circuitKey1: {}, }, AmtPaid: 6, }, setID3: invpkg.InvoiceStateAMP{ State: invpkg.HtlcStateAccepted, SettleDate: testNow, SettleIndex: 3, InvoiceKeys: map[models.CircuitKey]struct{}{ circuitKey1: {}, circuitKey2: {}, circuitKey3: {}, }, AmtPaid: 7, }, } // We'll now make a sample invoice stream, and use that to encode the // amp state we created above. tlvStream, err := tlv.NewStream( tlv.MakeDynamicRecord( invoiceAmpStateType, &State, ampRecordSize(&State), ampStateEncoder, ampStateDecoder, ), ) require.Nil(t, err) // Next encode the stream into a set of raw bytes. var b bytes.Buffer err = tlvStream.Encode(&b) require.Nil(t, err) // Now create a new blank ampState map, which we'll use to decode the // bytes into. ampState2 := make(invpkg.AMPInvoiceState) // Decode from the raw stream into this blank mpa. tlvStream, err = tlv.NewStream( tlv.MakeDynamicRecord( invoiceAmpStateType, &State2, nil, ampStateEncoder, ampStateDecoder, ), ) require.Nil(t, err) err = tlvStream.Decode(&b) require.Nil(t, err) // The two states should match. require.Equal(t, ampState, ampState2) }