Merge pull request #4493 from bhandras/invoice_gc

invoices: garbage collect settled/canceled invoices
This commit is contained in:
András Bánki-Horváth
2020-08-24 08:34:58 +02:00
committed by GitHub
8 changed files with 524 additions and 171 deletions

View File

@@ -622,9 +622,9 @@ func TestInvoiceAddTimeSeries(t *testing.T) {
}
}
// Tests that FetchAllInvoicesWithPaymentHash returns all invoices with their
// corresponding payment hashes.
func TestFetchAllInvoicesWithPaymentHash(t *testing.T) {
// TestScanInvoices tests that ScanInvoices scans trough all stored invoices
// correctly.
func TestScanInvoices(t *testing.T) {
t.Parallel()
db, cleanup, err := MakeTestDB()
@@ -633,97 +633,54 @@ func TestFetchAllInvoicesWithPaymentHash(t *testing.T) {
t.Fatalf("unable to make test db: %v", err)
}
// With an empty DB we expect to return no error and an empty list.
empty, err := db.FetchAllInvoicesWithPaymentHash(false)
if err != nil {
t.Fatalf("failed to call FetchAllInvoicesWithPaymentHash on empty 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++
}
if len(empty) != 0 {
t.Fatalf("expected empty list as a result, got: %v", empty)
scanFunc := func(paymentHash lntypes.Hash, invoice *Invoice) error {
invoices[paymentHash] = invoice
callCount++
return nil
}
states := []ContractState{
ContractOpen, ContractSettled, ContractCanceled, ContractAccepted,
}
// 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 := len(states) * 2
testPendingInvoices := make(map[lntypes.Hash]*Invoice)
testAllInvoices := make(map[lntypes.Hash]*Invoice)
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))
if err != nil {
t.Fatalf("unable to create invoice: %v", err)
}
require.NoError(t, err)
// Set the contract state of the next invoice such that there's an equal
// number for all possbile states.
invoice.State = states[i%len(states)]
paymentHash := invoice.Terms.PaymentPreimage.Hash()
testInvoices[paymentHash] = invoice
if invoice.IsPending() {
testPendingInvoices[paymentHash] = invoice
}
testAllInvoices[paymentHash] = invoice
if _, err := db.AddInvoice(invoice, paymentHash); err != nil {
t.Fatalf("unable to add invoice: %v", err)
}
}
pendingInvoices, err := db.FetchAllInvoicesWithPaymentHash(true)
if err != nil {
t.Fatalf("can't fetch invoices with payment hash: %v", err)
}
if len(testPendingInvoices) != len(pendingInvoices) {
t.Fatalf("expected %v pending invoices, got: %v",
len(testPendingInvoices), len(pendingInvoices))
}
allInvoices, err := db.FetchAllInvoicesWithPaymentHash(false)
if err != nil {
t.Fatalf("can't fetch invoices with payment hash: %v", err)
}
if len(testAllInvoices) != len(allInvoices) {
t.Fatalf("expected %v invoices, got: %v",
len(testAllInvoices), len(allInvoices))
}
for i := range pendingInvoices {
expected, ok := testPendingInvoices[pendingInvoices[i].PaymentHash]
if !ok {
t.Fatalf("coulnd't find invoice with hash: %v",
pendingInvoices[i].PaymentHash)
}
// Zero out add index to not confuse require.Equal.
pendingInvoices[i].Invoice.AddIndex = 0
expected.AddIndex = 0
require.Equal(t, *expected, pendingInvoices[i].Invoice)
}
for i := range allInvoices {
expected, ok := testAllInvoices[allInvoices[i].PaymentHash]
if !ok {
t.Fatalf("coulnd't find invoice with hash: %v",
allInvoices[i].PaymentHash)
}
// Zero out add index to not confuse require.Equal.
allInvoices[i].Invoice.AddIndex = 0
expected.AddIndex = 0
require.Equal(t, *expected, allInvoices[i].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
@@ -1194,3 +1151,96 @@ func TestInvoiceRef(t *testing.T) {
require.Equal(t, payHash, refByHashAndAddr.PayHash())
require.Equal(t, &payAddr, refByHashAndAddr.PayAddr())
}
// 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)
}

View File

@@ -723,28 +723,21 @@ func fetchInvoiceNumByRef(invoiceIndex, payAddrIndex kvdb.RBucket,
}
}
// InvoiceWithPaymentHash is used to store an invoice and its corresponding
// payment hash. This struct is only used to store results of
// ChannelDB.FetchAllInvoicesWithPaymentHash() call.
type InvoiceWithPaymentHash struct {
// Invoice holds the invoice as selected from the invoices bucket.
Invoice Invoice
// ScanInvoices scans trough all invoices and calls the passed scanFunc for
// for each invoice with its respective payment hash. Additionally a reset()
// closure is passed which is used to reset/initialize partial results and also
// to signal if the kvdb.View transaction has been retried.
func (d *DB) ScanInvoices(
scanFunc func(lntypes.Hash, *Invoice) error, reset func()) error {
// PaymentHash is the payment hash for the Invoice.
PaymentHash lntypes.Hash
}
return kvdb.View(d, func(tx kvdb.RTx) error {
// Reset partial results. As transaction commit success is not
// guaranteed when using etcd, we need to be prepared to redo
// the whole view transaction. In order to be able to do that
// we need a way to reset existing results. This is also done
// upon first run for initialization.
reset()
// FetchAllInvoicesWithPaymentHash returns all invoices and their payment hashes
// currently stored within the database. If the pendingOnly param is true, then
// only open or accepted invoices and their payment hashes will be returned,
// skipping all invoices that are fully settled or canceled. Note that the
// returned array is not ordered by add index.
func (d *DB) FetchAllInvoicesWithPaymentHash(pendingOnly bool) (
[]InvoiceWithPaymentHash, error) {
var result []InvoiceWithPaymentHash
err := kvdb.View(d, func(tx kvdb.RTx) error {
invoices := tx.ReadBucket(invoiceBucket)
if invoices == nil {
return ErrNoInvoicesCreated
@@ -775,26 +768,12 @@ func (d *DB) FetchAllInvoicesWithPaymentHash(pendingOnly bool) (
return err
}
if pendingOnly && !invoice.IsPending() {
return nil
}
var paymentHash lntypes.Hash
copy(paymentHash[:], k)
invoiceWithPaymentHash := InvoiceWithPaymentHash{
Invoice: invoice,
}
copy(invoiceWithPaymentHash.PaymentHash[:], k)
result = append(result, invoiceWithPaymentHash)
return nil
return scanFunc(paymentHash, &invoice)
})
})
if err != nil {
return nil, err
}
return result, nil
}
// InvoiceQuery represents a query to the invoice database. The query allows a
@@ -1761,3 +1740,134 @@ func setSettleMetaFields(settleIndex kvdb.RwBucket, invoiceNum []byte,
return nil
}
// InvoiceDeleteRef holds a refererence to an invoice to be deleted.
type InvoiceDeleteRef struct {
// PayHash is the payment hash of the target invoice. All invoices are
// currently indexed by payment hash.
PayHash lntypes.Hash
// PayAddr is the payment addr of the target invoice. Newer invoices
// (0.11 and up) are indexed by payment address in addition to payment
// hash, but pre 0.8 invoices do not have one at all.
PayAddr *[32]byte
// AddIndex is the add index of the invoice.
AddIndex uint64
// SettleIndex is the settle index of the invoice.
SettleIndex uint64
}
// DeleteInvoice attempts to delete the passed invoices from the database in
// one transaction. The passed delete references hold all keys required to
// delete the invoices without also needing to deserialze them.
func (d *DB) DeleteInvoice(invoicesToDelete []InvoiceDeleteRef) error {
err := kvdb.Update(d, func(tx kvdb.RwTx) error {
invoices := tx.ReadWriteBucket(invoiceBucket)
if invoices == nil {
return ErrNoInvoicesCreated
}
invoiceIndex := invoices.NestedReadWriteBucket(
invoiceIndexBucket,
)
if invoiceIndex == nil {
return ErrNoInvoicesCreated
}
invoiceAddIndex := invoices.NestedReadWriteBucket(
addIndexBucket,
)
if invoiceAddIndex == nil {
return ErrNoInvoicesCreated
}
// settleIndex can be nil, as the bucket is created lazily
// when the first invoice is settled.
settleIndex := invoices.NestedReadWriteBucket(settleIndexBucket)
payAddrIndex := tx.ReadWriteBucket(payAddrIndexBucket)
for _, ref := range invoicesToDelete {
// Fetch the invoice key for using it to check for
// consistency and also to delete from the invoice index.
invoiceKey := invoiceIndex.Get(ref.PayHash[:])
if invoiceKey == nil {
return ErrInvoiceNotFound
}
err := invoiceIndex.Delete(ref.PayHash[:])
if err != nil {
return err
}
// Delete payment address index reference if there's a
// valid payment address passed.
if ref.PayAddr != nil {
// To ensure consistency check that the already
// fetched invoice key matches the one in the
// payment address index.
key := payAddrIndex.Get(ref.PayAddr[:])
if !bytes.Equal(key, invoiceKey) {
return fmt.Errorf("unknown invoice")
}
// Delete from the payment address index.
err := payAddrIndex.Delete(ref.PayAddr[:])
if err != nil {
return err
}
}
var addIndexKey [8]byte
byteOrder.PutUint64(addIndexKey[:], ref.AddIndex)
// To ensure consistency check that the key stored in
// the add index also matches the previously fetched
// invoice key.
key := invoiceAddIndex.Get(addIndexKey[:])
if !bytes.Equal(key, invoiceKey) {
return fmt.Errorf("unknown invoice")
}
// Remove from the add index.
err = invoiceAddIndex.Delete(addIndexKey[:])
if err != nil {
return err
}
// Remove from the settle index if available and
// if the invoice is settled.
if settleIndex != nil && ref.SettleIndex > 0 {
var settleIndexKey [8]byte
byteOrder.PutUint64(
settleIndexKey[:], ref.SettleIndex,
)
// To ensure consistency check that the already
// fetched invoice key matches the one in the
// settle index
key := settleIndex.Get(settleIndexKey[:])
if !bytes.Equal(key, invoiceKey) {
return fmt.Errorf("unknown invoice")
}
err = settleIndex.Delete(settleIndexKey[:])
if err != nil {
return err
}
}
// Finally remove the serialized invoice from the
// invoice bucket.
err = invoices.Delete(invoiceKey)
if err != nil {
return err
}
}
return nil
})
return err
}