channeldb+router: record payment failure reason in db

This commit is contained in:
Johan T. Halseth 2019-05-23 20:05:30 +02:00
parent 60e2367973
commit 1b788904f0
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
5 changed files with 106 additions and 23 deletions

@ -56,10 +56,11 @@ type ControlTower interface {
// keeping.
Success(lntypes.Hash, lntypes.Preimage) error
// Fail transitions a payment into the Failed state. After invoking
// this method, InitPayment should return nil on its next call for this
// payment hash, allowing the switch to make a subsequent payment.
Fail(lntypes.Hash) error
// Fail transitions a payment into the Failed state, and records the
// reason the payment failed. After invoking this method, InitPayment
// should return nil on its next call for this payment hash, allowing
// the switch to make a subsequent payment.
Fail(lntypes.Hash, FailureReason) error
// FetchInFlightPayments returns all payments with status InFlight.
FetchInFlightPayments() ([]*InFlightPayment, error)
@ -165,7 +166,9 @@ func (p *paymentControl) InitPayment(paymentHash lntypes.Hash,
return err
}
return nil
// Also delete any lingering failure info now that we are
// re-attempting.
return bucket.Delete(paymentFailInfoKey)
})
if err != nil {
return nil
@ -255,10 +258,13 @@ func (p *paymentControl) Success(paymentHash lntypes.Hash,
}
// Fail transitions a payment into the Failed state. After invoking this
// method, InitPayment should return nil on its next call for this payment
// hash, allowing the switch to make a subsequent payment.
func (p *paymentControl) Fail(paymentHash lntypes.Hash) error {
// Fail transitions a payment into the Failed state, and records the reason the
// payment failed. After invoking this method, InitPayment should return nil on
// its next call for this payment hash, allowing the switch to make a
// subsequent payment.
func (p *paymentControl) Fail(paymentHash lntypes.Hash,
reason FailureReason) error {
var updateErr error
err := p.db.Batch(func(tx *bbolt.Tx) error {
// Reset the update error, to avoid carrying over an error
@ -276,6 +282,13 @@ func (p *paymentControl) Fail(paymentHash lntypes.Hash) error {
return nil
}
// Put the failure reason in the bucket for record keeping.
v := []byte{byte(reason)}
err = bucket.Put(paymentFailInfoKey, v)
if err != nil {
return err
}
// A failed response was received for an InFlight payment, mark
// it as Failed to allow subsequent attempts.
return bucket.Put(paymentStatusKey, StatusFailed.Bytes())

@ -88,10 +88,13 @@ func TestPaymentControlSwitchFail(t *testing.T) {
assertPaymentStatus(t, db, info.PaymentHash, StatusInFlight)
assertPaymentInfo(
t, db, info.PaymentHash, info, nil, lntypes.Preimage{},
nil,
)
// Fail the payment, which should moved it to Failed.
if err := pControl.Fail(info.PaymentHash); err != nil {
failReason := FailureReasonNoRoute
err = pControl.Fail(info.PaymentHash, failReason)
if err != nil {
t.Fatalf("unable to fail payment hash: %v", err)
}
@ -99,6 +102,7 @@ func TestPaymentControlSwitchFail(t *testing.T) {
assertPaymentStatus(t, db, info.PaymentHash, StatusFailed)
assertPaymentInfo(
t, db, info.PaymentHash, info, nil, lntypes.Preimage{},
&failReason,
)
// Sends the htlc again, which should succeed since the prior payment
@ -111,6 +115,7 @@ func TestPaymentControlSwitchFail(t *testing.T) {
assertPaymentStatus(t, db, info.PaymentHash, StatusInFlight)
assertPaymentInfo(
t, db, info.PaymentHash, info, nil, lntypes.Preimage{},
nil,
)
// Record a new attempt.
@ -122,6 +127,7 @@ func TestPaymentControlSwitchFail(t *testing.T) {
assertPaymentStatus(t, db, info.PaymentHash, StatusInFlight)
assertPaymentInfo(
t, db, info.PaymentHash, info, attempt, lntypes.Preimage{},
nil,
)
// Verifies that status was changed to StatusCompleted.
@ -130,7 +136,7 @@ func TestPaymentControlSwitchFail(t *testing.T) {
}
assertPaymentStatus(t, db, info.PaymentHash, StatusCompleted)
assertPaymentInfo(t, db, info.PaymentHash, info, attempt, preimg)
assertPaymentInfo(t, db, info.PaymentHash, info, attempt, preimg, nil)
// Attempt a final payment, which should now fail since the prior
// payment succeed.
@ -167,6 +173,7 @@ func TestPaymentControlSwitchDoubleSend(t *testing.T) {
assertPaymentStatus(t, db, info.PaymentHash, StatusInFlight)
assertPaymentInfo(
t, db, info.PaymentHash, info, nil, lntypes.Preimage{},
nil,
)
// Try to initiate double sending of htlc message with the same
@ -186,6 +193,7 @@ func TestPaymentControlSwitchDoubleSend(t *testing.T) {
assertPaymentStatus(t, db, info.PaymentHash, StatusInFlight)
assertPaymentInfo(
t, db, info.PaymentHash, info, attempt, lntypes.Preimage{},
nil,
)
// Sends base htlc message which initiate StatusInFlight.
@ -200,7 +208,7 @@ func TestPaymentControlSwitchDoubleSend(t *testing.T) {
t.Fatalf("error shouldn't have been received, got: %v", err)
}
assertPaymentStatus(t, db, info.PaymentHash, StatusCompleted)
assertPaymentInfo(t, db, info.PaymentHash, info, attempt, preimg)
assertPaymentInfo(t, db, info.PaymentHash, info, attempt, preimg, nil)
err = pControl.InitPayment(info.PaymentHash, info)
if err != ErrAlreadyPaid {
@ -232,7 +240,10 @@ func TestPaymentControlSuccessesWithoutInFlight(t *testing.T) {
}
assertPaymentStatus(t, db, info.PaymentHash, StatusGrounded)
assertPaymentInfo(t, db, info.PaymentHash, nil, nil, lntypes.Preimage{})
assertPaymentInfo(
t, db, info.PaymentHash, nil, nil, lntypes.Preimage{},
nil,
)
}
// TestPaymentControlFailsWithoutInFlight checks that a strict payment
@ -253,13 +264,15 @@ func TestPaymentControlFailsWithoutInFlight(t *testing.T) {
}
// Calling Fail should return an error.
err = pControl.Fail(info.PaymentHash)
err = pControl.Fail(info.PaymentHash, FailureReasonNoRoute)
if err != ErrPaymentNotInitiated {
t.Fatalf("expected ErrPaymentNotInitiated, got %v", err)
}
assertPaymentStatus(t, db, info.PaymentHash, StatusGrounded)
assertPaymentInfo(t, db, info.PaymentHash, nil, nil, lntypes.Preimage{})
assertPaymentInfo(
t, db, info.PaymentHash, nil, nil, lntypes.Preimage{}, nil,
)
}
func assertPaymentStatus(t *testing.T, db *DB,
@ -364,8 +377,29 @@ func checkSettleInfo(bucket *bbolt.Bucket, preimg lntypes.Preimage) error {
return nil
}
func checkFailInfo(bucket *bbolt.Bucket, failReason *FailureReason) error {
b := bucket.Get(paymentFailInfoKey)
switch {
case b == nil && failReason == nil:
return nil
case b == nil:
return fmt.Errorf("expected fail info not found")
case failReason == nil:
return fmt.Errorf("unexpected fail info found")
}
failReason2 := FailureReason(b[0])
if *failReason != failReason2 {
return fmt.Errorf("Failure infos don't match: %v vs %v",
*failReason, failReason2)
}
return nil
}
func assertPaymentInfo(t *testing.T, db *DB, hash lntypes.Hash,
c *PaymentCreationInfo, a *PaymentAttemptInfo, s lntypes.Preimage) {
c *PaymentCreationInfo, a *PaymentAttemptInfo, s lntypes.Preimage,
f *FailureReason) {
t.Helper()
@ -398,6 +432,10 @@ func assertPaymentInfo(t *testing.T, db *DB, hash lntypes.Hash,
if err := checkSettleInfo(bucket, s); err != nil {
return err
}
if err := checkFailInfo(bucket, f); err != nil {
return err
}
return nil
})
if err != nil {

@ -81,6 +81,10 @@ var (
// store the settle info of the payment.
paymentSettleInfoKey = []byte("payment-settle-info")
// paymentFailInfoKey is a key used in the payment's sub-bucket to
// store information about the reason a payment failed.
paymentFailInfoKey = []byte("payment-fail-info")
// paymentBucket is the name of the bucket within the database that
// stores all data related to payments.
//
@ -94,6 +98,21 @@ var (
paymentStatusBucket = []byte("payment-status")
)
// FailureReason encodes the reason a payment ultimately failed.
type FailureReason byte
const (
// FailureReasonTimeout indicates that the payment did timeout before a
// successful payment attempt was made.
FailureReasonTimeout FailureReason = 0
// FailureReasonNoRoute indicates no successful route to the
// destination was found during path finding.
FailureReasonNoRoute FailureReason = 1
// TODO(halseth): cancel state.
)
// PaymentStatus represent current status of payment
type PaymentStatus byte
@ -113,8 +132,6 @@ const (
// StatusFailed is the status where a payment has been initiated and a
// failure result has come back.
StatusFailed PaymentStatus = 3
// TODO(halseth): timeout/cancel state?
)
// Bytes returns status as slice of bytes.
@ -454,6 +471,12 @@ type Payment struct {
//
// NOTE: Can be nil if payment is not settled.
PaymentPreimage *lntypes.Preimage
// Failure is a failure reason code indicating the reason the payment
// failed. It is only non-nil for failed payments.
//
// NOTE: Can be nil if payment is not failed.
Failure *FailureReason
}
// FetchPayments returns all sent payments found in the DB.
@ -570,6 +593,13 @@ func fetchPayment(bucket *bbolt.Bucket) (*Payment, error) {
p.PaymentPreimage = &preimg
}
// Get failure reason if available.
b = bucket.Get(paymentFailInfoKey)
if b != nil {
reason := FailureReason(b[0])
p.Failure = &reason
}
return p, nil
}

@ -167,6 +167,7 @@ type successArgs struct {
}
type failArgs struct {
reason channeldb.FailureReason
}
type mockControlTower struct {
@ -253,13 +254,14 @@ func (m *mockControlTower) Success(phash lntypes.Hash,
return nil
}
func (m *mockControlTower) Fail(phash lntypes.Hash) error {
func (m *mockControlTower) Fail(phash lntypes.Hash,
reason channeldb.FailureReason) error {
m.Lock()
defer m.Unlock()
if m.fail != nil {
m.fail <- failArgs{}
m.fail <- failArgs{reason}
}
delete(m.inflights, phash)

@ -177,7 +177,7 @@ func (p *paymentLifecycle) createNewPaymentAttempt() (lnwire.ShortChannelID,
// Mark the payment as failed because of the
// timeout.
err := p.router.cfg.Control.Fail(
p.payment.PaymentHash,
p.payment.PaymentHash, channeldb.FailureReasonTimeout,
)
if err != nil {
return lnwire.ShortChannelID{}, nil, err
@ -208,7 +208,7 @@ func (p *paymentLifecycle) createNewPaymentAttempt() (lnwire.ShortChannelID,
// any of the routes we've found, then mark the payment
// as permanently failed.
saveErr := p.router.cfg.Control.Fail(
p.payment.PaymentHash,
p.payment.PaymentHash, channeldb.FailureReasonNoRoute,
)
if saveErr != nil {
return lnwire.ShortChannelID{}, nil, saveErr
@ -338,7 +338,7 @@ func (p *paymentLifecycle) handleSendError(sendErr error) error {
// TODO(halseth): make payment codes for the actual reason we
// don't continue path finding.
err := p.router.cfg.Control.Fail(
p.payment.PaymentHash,
p.payment.PaymentHash, channeldb.FailureReasonNoRoute,
)
if err != nil {
return err