lnd.xprv/channeldb/invoice_test.go
2021-03-03 16:26:43 -08:00

1399 lines
37 KiB
Go

package channeldb
import (
"crypto/rand"
"fmt"
"math"
"testing"
"time"
"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)
testNow = time.Unix(1, 0)
)
func randInvoice(value lnwire.MilliSatoshi) (*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 := &Invoice{
CreationDate: testNow,
Terms: ContractTerm{
Expiry: 4000,
PaymentPreimage: &pre,
PaymentAddr: payAddr,
Value: value,
Features: emptyFeatures,
},
Htlcs: map[CircuitKey]*InvoiceHTLC{},
}
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 [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 *Invoice, settleIndex uint64) {
invoice.SettleDate = testNow
invoice.AmtPaid = invoice.Terms.Value
invoice.State = ContractSettled
invoice.Htlcs[CircuitKey{}] = &InvoiceHTLC{
Amt: invoice.Terms.Value,
AcceptTime: testNow,
ResolveTime: testNow,
State: 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 := []ContractState{
ContractOpen, ContractSettled, ContractCanceled, ContractAccepted,
}
for _, state := range contractStates {
invoice := Invoice{
State: state,
}
// We expect that an invoice is pending if it's either in ContractOpen
// or ContractAccepted state.
pending := (state == ContractOpen || state == ContractAccepted)
if invoice.IsPending() != pending {
t.Fatalf("expected pending: %v, got: %v, invoice: %v",
pending, invoice.IsPending(), invoice)
}
}
}
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, cleanUp, err := MakeTestDB()
defer cleanUp()
if err != nil {
t.Fatalf("unable to make test db: %v", err)
}
// Create a fake invoice which we'll use several times in the tests
// below.
fakeInvoice, err := randInvoice(10000)
if err != nil {
t.Fatalf("unable to create invoice: %v", err)
}
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 InvoiceRef
)
switch {
case test.queryPayHash && test.queryPayAddr:
payHash = invPayHash
payAddr = &fakeInvoice.Terms.PaymentAddr
ref = InvoiceRefByHashAndAddr(payHash, *payAddr)
case test.queryPayHash:
payHash = invPayHash
ref = 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 {
if err != ErrInvoiceNotFound {
t.Fatalf("invoice should not exist: %v", err)
}
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, getUpdateInvoice(payAmt))
if err != nil {
t.Fatalf("unable to settle invoice: %v", err)
}
dbInvoice2, err := db.LookupInvoice(ref)
if err != nil {
t.Fatalf("unable to fetch invoice: %v", err)
}
if dbInvoice2.State != 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.
if _, err := db.AddInvoice(fakeInvoice, payHash); err != ErrDuplicateInvoice {
t.Fatalf("invoice insertion should fail due to duplication, "+
"instead %v", err)
}
// Attempt to look up a non-existent invoice, this should also fail but
// with a "not found" error.
var fakeHash [32]byte
fakeRef := InvoiceRefByHash(fakeHash)
_, err = db.LookupInvoice(fakeRef)
if err != ErrInvoiceNotFound {
t.Fatalf("lookup should have failed, instead %v", err)
}
// Add 10 random invoices.
const numInvoices = 10
amt := lnwire.NewMSatFromSatoshis(1000)
invoices := make([]*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 := InvoiceQuery{
IndexOffset: 0,
NumMaxInvoices: math.MaxUint64,
PendingOnly: false,
}
response, err := db.QueryInvoices(query)
if err != nil {
t.Fatalf("invoice query failed: %v", err)
}
// 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, cleanUp, err := MakeTestDB()
defer cleanUp()
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, 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, cleanUp, err := MakeTestDB()
defer cleanUp()
require.NoError(t, err)
// Create two invoices with the same _blank_ payment addr.
invoice1, err := randInvoice(1000)
require.NoError(t, err)
invoice1.Terms.PaymentAddr = BlankPayAddr
invoice2, err := randInvoice(20000)
require.NoError(t, err)
invoice2.Terms.PaymentAddr = 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 := InvoiceRefByHashAndAddr(inv1Hash, BlankPayAddr)
dbInv1, err := db.LookupInvoice(ref1)
require.NoError(t, err)
require.Equal(t, invoice1, &dbInv1)
ref2 := InvoiceRefByHashAndAddr(inv2Hash, BlankPayAddr)
dbInv2, err := db.LookupInvoice(ref2)
require.NoError(t, err)
require.Equal(t, invoice2, &dbInv2)
}
// TestInvRefEquivocation asserts that retrieving or updating an invoice using
// an equivocating InvoiceRef results in ErrInvRefEquivocation.
func TestInvRefEquivocation(t *testing.T) {
db, cleanUp, err := MakeTestDB()
defer cleanUp()
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 := InvoiceRefByHashAndAddr(inv2Hash, invoice1.Terms.PaymentAddr)
_, err = db.LookupInvoice(ref)
require.Error(t, err, ErrInvRefEquivocation)
// The same error should be returned when updating an equivocating
// reference.
nop := func(_ *Invoice) (*InvoiceUpdateDesc, error) {
return nil, nil
}
_, err = db.UpdateInvoice(ref, nop)
require.Error(t, err, ErrInvRefEquivocation)
}
// TestInvoiceCancelSingleHtlc tests that a single htlc can be canceled on the
// invoice.
func TestInvoiceCancelSingleHtlc(t *testing.T) {
t.Parallel()
db, cleanUp, err := MakeTestDB()
defer cleanUp()
if err != nil {
t.Fatalf("unable to make test db: %v", err)
}
preimage := lntypes.Preimage{1}
paymentHash := preimage.Hash()
testInvoice := &Invoice{
Htlcs: map[CircuitKey]*InvoiceHTLC{},
Terms: 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 := CircuitKey{ChanID: lnwire.NewShortChanIDFromInt(1), HtlcID: 4}
htlc := HtlcAcceptDesc{
Amt: 500,
CustomRecords: make(record.CustomSet),
}
ref := InvoiceRefByHash(paymentHash)
invoice, err := db.UpdateInvoice(ref,
func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
return &InvoiceUpdateDesc{
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
key: &htlc,
},
}, nil
})
if err != nil {
t.Fatalf("unable to add invoice htlc: %v", err)
}
if len(invoice.Htlcs) != 1 {
t.Fatalf("expected the htlc to be added")
}
if invoice.Htlcs[key].State != HtlcStateAccepted {
t.Fatalf("expected htlc in state accepted")
}
// Cancel the htlc again.
invoice, err = db.UpdateInvoice(ref,
func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
return &InvoiceUpdateDesc{
CancelHtlcs: map[CircuitKey]struct{}{
key: {},
},
}, nil
})
if err != nil {
t.Fatalf("unable to cancel htlc: %v", err)
}
if len(invoice.Htlcs) != 1 {
t.Fatalf("expected the htlc to be present")
}
if invoice.Htlcs[key].State != HtlcStateCanceled {
t.Fatalf("expected htlc in state canceled")
}
}
// 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, cleanUp, err := MakeTestDB(OptionClock(testClock))
defer cleanUp()
if err != nil {
t.Fatalf("unable to make test db: %v", err)
}
_, 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([]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 []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 []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 := InvoiceRefByHash(paymentHash)
_, err := db.UpdateInvoice(
ref, 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 []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),
)
}
}
}
// TestScanInvoices tests that ScanInvoices scans trough all stored invoices
// correctly.
func TestScanInvoices(t *testing.T) {
t.Parallel()
db, cleanup, err := MakeTestDB()
defer cleanup()
if err != nil {
t.Fatalf("unable to make test db: %v", err)
}
var invoices map[lntypes.Hash]*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]*Invoice)
callCount = 0
resetCount++
}
scanFunc := func(paymentHash lntypes.Hash, invoice *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]*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, cleanUp, err := MakeTestDB(OptionClock(testClock))
defer cleanUp()
if err != nil {
t.Fatalf("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)
if err != nil {
t.Fatalf("unable to create invoice: %v", err)
}
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 := InvoiceRefByHash(payHash)
dbInvoice, err := db.UpdateInvoice(ref, getUpdateInvoice(amt))
if err != nil {
t.Fatalf("unable to settle invoice: %v", err)
}
// 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 = ContractSettled
invoice.AmtPaid = amt
invoice.SettleDate = dbInvoice.SettleDate
invoice.Htlcs = map[CircuitKey]*InvoiceHTLC{
{}: {
Amt: amt,
AcceptTime: time.Unix(1, 0),
ResolveTime: time.Unix(1, 0),
State: 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, getUpdateInvoice(amt))
if err != ErrInvoiceAlreadySettled {
t.Fatalf("expected 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, cleanUp, err := MakeTestDB(OptionClock(testClock))
defer cleanUp()
if err != nil {
t.Fatalf("unable to make test db: %v", err)
}
// 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 []Invoice
var pendingInvoices []Invoice
for i := 1; i <= numInvoices; i++ {
amt := lnwire.MilliSatoshi(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)
}
// We'll only settle half of all invoices created.
if i%2 == 0 {
ref := InvoiceRefByHash(paymentHash)
_, err := db.UpdateInvoice(ref, 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 InvoiceQuery
expected []Invoice
}{
// Fetch all invoices with a single query.
{
query: InvoiceQuery{
NumMaxInvoices: numInvoices,
},
expected: invoices,
},
// Fetch all invoices with a single query, reversed.
{
query: InvoiceQuery{
Reversed: true,
NumMaxInvoices: numInvoices,
},
expected: invoices,
},
// Fetch the first 25 invoices.
{
query: InvoiceQuery{
NumMaxInvoices: numInvoices / 2,
},
expected: invoices[:numInvoices/2],
},
// Fetch the first 10 invoices, but this time iterating
// backwards.
{
query: InvoiceQuery{
IndexOffset: 11,
Reversed: true,
NumMaxInvoices: numInvoices,
},
expected: invoices[:10],
},
// Fetch the last 40 invoices.
{
query: InvoiceQuery{
IndexOffset: 10,
NumMaxInvoices: numInvoices,
},
expected: invoices[10:],
},
// Fetch all but the first invoice.
{
query: 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: InvoiceQuery{
IndexOffset: 3,
Reversed: true,
NumMaxInvoices: 1,
},
expected: invoices[1:2],
},
// Same as above, at index 2.
{
query: 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: InvoiceQuery{
IndexOffset: 1,
Reversed: true,
NumMaxInvoices: 1,
},
expected: nil,
},
// Same as above, but don't restrict the number of invoices to
// 1.
{
query: 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: 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: InvoiceQuery{
IndexOffset: numInvoices + 1,
Reversed: true,
NumMaxInvoices: 1,
},
expected: invoices[numInvoices-1:],
},
// Same as above, at offset numInvoices.
{
query: 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: InvoiceQuery{
NumMaxInvoices: 1,
},
expected: invoices[:1],
},
// Same as above, at offset 1.
{
query: InvoiceQuery{
IndexOffset: 1,
NumMaxInvoices: 1,
},
expected: invoices[1:2],
},
// Same as above, at offset 2.
{
query: InvoiceQuery{
IndexOffset: 2,
NumMaxInvoices: 1,
},
expected: invoices[2:3],
},
// Same as above, at offset numInvoices-1. Expect the last
// invoice to be returned.
{
query: 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: InvoiceQuery{
IndexOffset: numInvoices,
NumMaxInvoices: 1,
},
expected: nil,
},
// Fetch all pending invoices with a single query.
{
query: InvoiceQuery{
PendingOnly: true,
NumMaxInvoices: numInvoices,
},
expected: pendingInvoices,
},
// Fetch the first 12 pending invoices.
{
query: InvoiceQuery{
PendingOnly: true,
NumMaxInvoices: numInvoices / 4,
},
expected: pendingInvoices[:len(pendingInvoices)/2],
},
// Fetch the first 5 pending invoices, but this time iterating
// backwards.
{
query: 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: 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: InvoiceQuery{
IndexOffset: numInvoices * 2,
PendingOnly: false,
Reversed: true,
NumMaxInvoices: numInvoices,
},
expected: invoices,
},
}
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) InvoiceUpdateCallback {
return func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
if invoice.State == ContractSettled {
return nil, ErrInvoiceAlreadySettled
}
noRecords := make(record.CustomSet)
update := &InvoiceUpdateDesc{
State: &InvoiceStateUpdateDesc{
Preimage: invoice.Terms.PaymentPreimage,
NewState: ContractSettled,
},
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
{}: {
Amt: amt,
CustomRecords: noRecords,
},
},
}
return update, nil
}
}
// TestCustomRecords tests that custom records are properly recorded in the
// invoice database.
func TestCustomRecords(t *testing.T) {
t.Parallel()
db, cleanUp, err := MakeTestDB()
defer cleanUp()
if err != nil {
t.Fatalf("unable to make test db: %v", err)
}
preimage := lntypes.Preimage{1}
paymentHash := preimage.Hash()
testInvoice := &Invoice{
Htlcs: map[CircuitKey]*InvoiceHTLC{},
Terms: 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 := CircuitKey{ChanID: lnwire.NewShortChanIDFromInt(1), HtlcID: 4}
records := record.CustomSet{
100000: []byte{},
100001: []byte{1, 2},
}
ref := InvoiceRefByHash(paymentHash)
_, err = db.UpdateInvoice(ref,
func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
return &InvoiceUpdateDesc{
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
key: {
Amt: 500,
CustomRecords: records,
},
},
}, nil
},
)
if err != nil {
t.Fatalf("unable to add invoice htlc: %v", err)
}
// Retrieve the invoice from that database and verify that the custom
// records are present.
dbInvoice, err := db.LookupInvoice(ref)
if err != nil {
t.Fatalf("unable to lookup invoice: %v", err)
}
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, cleanUp, err := MakeTestDB()
defer cleanUp()
require.Nil(t, err)
testInvoice, err := randInvoice(1000)
require.Nil(t, err)
payHash := testInvoice.Terms.PaymentPreimage.Hash()
_, err = db.AddInvoice(testInvoice, payHash)
require.Nil(t, err)
// Accept an htlc with custom records on this invoice.
key := CircuitKey{ChanID: lnwire.NewShortChanIDFromInt(1), HtlcID: 4}
records := make(map[uint64][]byte)
var ampData *InvoiceHtlcAMPData
if isAMP {
amp := record.NewAMP([32]byte{1}, [32]byte{2}, 3)
preimage := &lntypes.Preimage{4}
ampData = &InvoiceHtlcAMPData{
Record: *amp,
Hash: preimage.Hash(),
Preimage: preimage,
}
}
ref := InvoiceRefByHash(payHash)
_, err = db.UpdateInvoice(ref,
func(invoice *Invoice) (*InvoiceUpdateDesc, error) {
return &InvoiceUpdateDesc{
AddHtlcs: map[CircuitKey]*HtlcAcceptDesc{
key: {
Amt: 500,
AMP: ampData,
CustomRecords: records,
},
},
}, nil
},
)
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}
// An InvoiceRef by hash should return the provided hash and a nil
// payment addr.
refByHash := InvoiceRefByHash(payHash)
require.Equal(t, payHash, refByHash.PayHash())
require.Equal(t, (*[32]byte)(nil), refByHash.PayAddr())
// An InvoiceRef by hash and addr should return the payment hash and
// payment addr passed to the constructor.
refByHashAndAddr := InvoiceRefByHashAndAddr(payHash, payAddr)
require.Equal(t, payHash, refByHashAndAddr.PayHash())
require.Equal(t, &payAddr, refByHashAndAddr.PayAddr())
}
// 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 := &Invoice{
Htlcs: make(map[CircuitKey]*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[CircuitKey]*InvoiceHTLC)
expSet1 := make(map[CircuitKey]*InvoiceHTLC)
expSet2 := make(map[CircuitKey]*InvoiceHTLC)
checkHTLCSets := func() {
require.Equal(t, expSetNil, inv.HTLCSet(nil))
require.Equal(t, expSet1, inv.HTLCSet(setID1))
require.Equal(t, expSet2, inv.HTLCSet(setID2))
}
// 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 HtlcState
}{
{nil, HtlcStateAccepted},
{nil, HtlcStateAccepted},
{setID1, HtlcStateAccepted},
{setID1, HtlcStateAccepted},
{setID2, HtlcStateAccepted},
{setID2, HtlcStateAccepted},
{nil, HtlcStateCanceled},
{setID1, HtlcStateCanceled},
{setID2, HtlcStateCanceled},
{nil, HtlcStateSettled},
{setID1, HtlcStateSettled},
{setID2, HtlcStateSettled},
}
for i, h := range htlcs {
var ampData *InvoiceHtlcAMPData
if h.setID != nil {
ampData = &InvoiceHtlcAMPData{
Record: *record.NewAMP([32]byte{0}, *h.setID, 0),
}
}
// Add the HTLC to the invoice's set of HTLCs.
key := CircuitKey{HtlcID: uint64(i)}
htlc := &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 == 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()
}
}
// 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, cleanup, err := MakeTestDB()
defer cleanup()
require.NoError(t, err, "unable to make test db")
// Add some invoices to the test db.
numInvoices := 3
invoicesToDelete := make([]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(
InvoiceRefByHash(paymentHash),
getUpdateInvoice(invoice.Terms.Value),
)
require.NoError(t, err, "unable to settle invoice")
}
// store the delete ref for later.
invoicesToDelete[i] = 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 := 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 one byte of one of the references' payment address and attempt
// to delete.
invoicesToDelete[1].PayAddr[5] ^= 7
require.Error(t, db.DeleteInvoice(invoicesToDelete))
assertInvoiceCount(3)
// Restore the payment address.
invoicesToDelete[1].PayAddr[5] ^= 7
// 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)
}