channeldb+routing: htlcs are pruned on settle

This commit is contained in:
Tommy Volk 2022-06-12 04:18:07 +00:00 committed by Olaoluwa Osuntokun
parent 0a1289d606
commit 064e0c6273
6 changed files with 164 additions and 5 deletions

View File

@ -223,6 +223,19 @@ func (p *PaymentControl) InitPayment(paymentHash lntypes.Hash,
return updateErr
}
// DeleteFailedAttempts deletes all failed htlcs for a payment if configured
// by the PaymentControl db.
func (p *PaymentControl) DeleteFailedAttempts(hash lntypes.Hash) error {
if !p.db.keepFailedPaymentAttempts {
const failedHtlcsOnly = true
err := p.db.DeletePayment(hash, failedHtlcsOnly)
if err != nil {
return err
}
}
return nil
}
// paymentIndexTypeHash is a payment index type which indicates that we have
// created an index of payment sequence number to payment hash.
type paymentIndexType uint8

View File

@ -308,7 +308,7 @@ func TestPaymentControlFailsWithoutInFlight(t *testing.T) {
// TestPaymentControlDeleteNonInFlight checks that calling DeletePayments only
// deletes payments from the database that are not in-flight.
func TestPaymentControlDeleteNonInFligt(t *testing.T) {
func TestPaymentControlDeleteNonInFlight(t *testing.T) {
t.Parallel()
db, cleanup, err := MakeTestDB()
@ -528,7 +528,7 @@ func TestPaymentControlDeletePayments(t *testing.T) {
// Register three payments:
// 1. A payment with two failed attempts.
// 2. A Payment with one failed and one settled attempt.
// 2. A payment with one failed and one settled attempt.
// 3. A payment with one failed and one in-flight attempt.
payments := []*payment{
{status: StatusFailed},
@ -585,7 +585,7 @@ func TestPaymentControlDeleteSinglePayment(t *testing.T) {
// according to its final status.
// 1. A payment with two failed attempts.
// 2. Another payment with two failed attempts.
// 3. A Payment with one failed and one settled attempt.
// 3. A payment with one failed and one settled attempt.
// 4. A payment with one failed and one in-flight attempt.
// Initiate payments, which is a slice of payment that is used as
@ -1003,6 +1003,96 @@ func TestPaymentControlMPPRecordValidation(t *testing.T) {
}
}
// TestDeleteFailedAttempts checks that DeleteFailedAttempts properly removes
// failed HTLCs from finished payments.
func TestDeleteFailedAttempts(t *testing.T) {
t.Parallel()
t.Run("keep failed payment attempts", func(t *testing.T) {
testDeleteFailedAttempts(t, true)
})
t.Run("remove failed payment attempts", func(t *testing.T) {
testDeleteFailedAttempts(t, false)
})
}
func testDeleteFailedAttempts(t *testing.T, keepFailedPaymentAttempts bool) {
db, cleanup, err := MakeTestDB()
defer cleanup()
require.NoError(t, err, "unable to init db")
db.keepFailedPaymentAttempts = keepFailedPaymentAttempts
pControl := NewPaymentControl(db)
// Register three payments:
// All payments will have one failed HTLC attempt and one HTLC attempt
// according to its final status.
// 1. A payment with two failed attempts.
// 2. A payment with one failed and one in-flight attempt.
// 3. A payment with one failed and one settled attempt.
// Initiate payments, which is a slice of payment that is used as
// template to create the corresponding test payments in the database.
//
// Note: The payment id and number of htlc attempts of each payment will
// be added to this slice when creating the payments below.
// This allows the slice to be used directly for testing purposes.
payments := []*payment{
{status: StatusFailed},
{status: StatusInFlight},
{status: StatusSucceeded},
}
// Use helper function to register the test payments in the data and
// populate the data to the payments slice.
createTestPayments(t, pControl, payments)
// Check that all payments are there as we added them.
assertPayments(t, db, payments)
// Calling DeleteFailedAttempts on a failed payment should delete all
// HTLCs.
require.NoError(t, pControl.DeleteFailedAttempts(payments[0].id))
// Expect all HTLCs to be deleted if the config is set to delete them.
if !keepFailedPaymentAttempts {
payments[0].htlcs = 0
}
assertPayments(t, db, payments)
// Calling DeleteFailedAttempts on an in-flight payment should return
// an error.
if keepFailedPaymentAttempts {
require.NoError(t, pControl.DeleteFailedAttempts(payments[1].id))
} else {
require.Error(t, pControl.DeleteFailedAttempts(payments[1].id))
}
// Since DeleteFailedAttempts returned an error, we should expect the
// payment to be unchanged.
assertPayments(t, db, payments)
// Cleaning up a successful payment should remove failed htlcs.
require.NoError(t, pControl.DeleteFailedAttempts(payments[2].id))
// Expect all HTLCs except for the settled one to be deleted if the
// config is set to delete them.
if !keepFailedPaymentAttempts {
payments[2].htlcs = 1
}
assertPayments(t, db, payments)
if keepFailedPaymentAttempts {
// DeleteFailedAttempts is ignored, even for non-existent
// payments, if the control tower is configured to keep failed
// HTLCs.
require.NoError(t, pControl.DeleteFailedAttempts(lntypes.ZeroHash))
} else {
// Attempting to cleanup a non-existent payment returns an error.
require.Error(t, pControl.DeleteFailedAttempts(lntypes.ZeroHash))
}
}
// assertPaymentStatus retrieves the status of the payment referred to by hash
// and compares it with the expected state.
func assertPaymentStatus(t *testing.T, p *PaymentControl,

View File

@ -19,6 +19,11 @@ type ControlTower interface {
// hash.
InitPayment(lntypes.Hash, *channeldb.PaymentCreationInfo) error
// DeleteFailedAttempts removes all failed HTLCs from the db. It should
// be called for a given payment whenever all inflight htlcs are
// completed, and the payment has reached a final settled state.
DeleteFailedAttempts(lntypes.Hash) error
// RegisterAttempt atomically records the provided HTLCAttemptInfo.
RegisterAttempt(lntypes.Hash, *channeldb.HTLCAttemptInfo) error
@ -125,6 +130,12 @@ func (p *controlTower) InitPayment(paymentHash lntypes.Hash,
return p.db.InitPayment(paymentHash, info)
}
// DeleteFailedAttempts deletes all failed htlcs if the payment was
// successfully settled.
func (p *controlTower) DeleteFailedAttempts(paymentHash lntypes.Hash) error {
return p.db.DeleteFailedAttempts(paymentHash)
}
// RegisterAttempt atomically records the provided HTLCAttemptInfo to the
// DB.
func (p *controlTower) RegisterAttempt(paymentHash lntypes.Hash,

View File

@ -309,6 +309,32 @@ func (m *mockControlTowerOld) InitPayment(phash lntypes.Hash,
return nil
}
func (m *mockControlTowerOld) DeleteFailedAttempts(phash lntypes.Hash) error {
p, ok := m.payments[phash]
if !ok {
return channeldb.ErrPaymentNotInitiated
}
var inFlight bool
for _, a := range p.attempts {
if a.Settle != nil {
continue
}
if a.Failure != nil {
continue
}
inFlight = true
}
if inFlight {
return channeldb.ErrPaymentInFlight
}
return nil
}
func (m *mockControlTowerOld) RegisterAttempt(phash lntypes.Hash,
a *channeldb.HTLCAttemptInfo) error {
@ -665,6 +691,11 @@ func (m *mockControlTower) InitPayment(phash lntypes.Hash,
return args.Error(0)
}
func (m *mockControlTower) DeleteFailedAttempts(phash lntypes.Hash) error {
args := m.Called(phash)
return args.Error(0)
}
func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash,
a *channeldb.HTLCAttemptInfo) error {

View File

@ -186,9 +186,21 @@ lifecycle:
// Find the first successful shard and return
// the preimage and route.
for _, a := range payment.HTLCs {
if a.Settle != nil {
return a.Settle.Preimage, &a.Route, nil
if a.Settle == nil {
continue
}
err := p.router.cfg.Control.
DeleteFailedAttempts(
p.identifier)
if err != nil {
log.Errorf("Error deleting failed "+
"payment attempts for "+
"payment %v: %v", p.identifier,
err)
}
return a.Settle.Preimage, &a.Route, nil
}
// Payment failed.

View File

@ -3571,6 +3571,7 @@ func TestSendMPPaymentSucceed(t *testing.T) {
}
}
})
controlTower.On("DeleteFailedAttempts", identifier).Return(nil)
// Call the actual method SendPayment on router. This is place inside a
// goroutine so we can set a timeout for the whole test, in case
@ -3782,6 +3783,7 @@ func TestSendMPPaymentSucceedOnExtraShards(t *testing.T) {
return
}
})
controlTower.On("DeleteFailedAttempts", identifier).Return(nil)
// Call the actual method SendPayment on router. This is place inside a
// goroutine so we can set a timeout for the whole test, in case