From f86e68a1a234c4456a29a3901b37ade6872e7654 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 20 Feb 2020 18:08:01 +0100 Subject: [PATCH] channeldb+routing: store full htlc failure reason This commit extends the htlc fail info with the full failure reason that was received over the wire. In a later commit, this info will also be exposed on the rpc interface. Furthermore it serves as a building block to make SendToRoute reliable across restarts. --- channeldb/mp_payment.go | 75 ++++++++++++++++++++++++++++++- channeldb/payment_control_test.go | 8 +++- routing/payment_lifecycle.go | 61 ++++++++++++++++++++++--- 3 files changed, 134 insertions(+), 10 deletions(-) diff --git a/channeldb/mp_payment.go b/channeldb/mp_payment.go index 7c5d97aee..d46e59091 100644 --- a/channeldb/mp_payment.go +++ b/channeldb/mp_payment.go @@ -1,11 +1,14 @@ package channeldb import ( + "bytes" "io" "time" "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) @@ -57,11 +60,45 @@ type HTLCSettleInfo struct { SettleTime time.Time } +// HTLCFailReason is the reason an htlc failed. +type HTLCFailReason byte + +const ( + // HTLCFailUnknown is recorded for htlcs that failed with an unknown + // reason. + HTLCFailUnknown HTLCFailReason = 0 + + // HTLCFailUnknown is recorded for htlcs that had a failure message that + // couldn't be decrypted. + HTLCFailUnreadable HTLCFailReason = 1 + + // HTLCFailInternal is recorded for htlcs that failed because of an + // internal error. + HTLCFailInternal HTLCFailReason = 2 + + // HTLCFailMessage is recorded for htlcs that failed with a network + // failure message. + HTLCFailMessage HTLCFailReason = 3 +) + // HTLCFailInfo encapsulates the information that augments an HTLCAttempt in the // event that the HTLC fails. type HTLCFailInfo struct { // FailTime is the time at which this HTLC was failed. FailTime time.Time + + // Message is the wire message that failed this HTLC. This field will be + // populated when the failure reason is HTLCFailMessage. + Message lnwire.FailureMessage + + // Reason is the failure reason for this HTLC. + Reason HTLCFailReason + + // The position in the path of the intermediate or final node that + // generated the failure message. Position zero is the sender node. This + // field will be populated when the failure reason is either + // HTLCFailMessage or HTLCFailUnknown. + FailureSourceIndex uint32 } // MPPayment is a wrapper around a payment's PaymentCreationInfo and @@ -130,7 +167,20 @@ func serializeHTLCFailInfo(w io.Writer, f *HTLCFailInfo) error { return err } - return nil + // Write failure. If there is no failure message, write an empty + // byte slice. + var messageBytes bytes.Buffer + if f.Message != nil { + err := lnwire.EncodeFailureMessage(&messageBytes, f.Message, 0) + if err != nil { + return err + } + } + if err := wire.WriteVarBytes(w, 0, messageBytes.Bytes()); err != nil { + return err + } + + return WriteElements(w, byte(f.Reason), f.FailureSourceIndex) } // deserializeHTLCFailInfo deserializes the details of a failed htlc including @@ -143,6 +193,29 @@ func deserializeHTLCFailInfo(r io.Reader) (*HTLCFailInfo, error) { return nil, err } + // Read failure. + failureBytes, err := wire.ReadVarBytes( + r, 0, lnwire.FailureMessageLength, "failure", + ) + if err != nil { + return nil, err + } + if len(failureBytes) > 0 { + f.Message, err = lnwire.DecodeFailureMessage( + bytes.NewReader(failureBytes), 0, + ) + if err != nil { + return nil, err + } + } + + var reason byte + err = ReadElements(r, &reason, &f.FailureSourceIndex) + if err != nil { + return nil, err + } + f.Reason = HTLCFailReason(reason) + return f, nil } diff --git a/channeldb/payment_control_test.go b/channeldb/payment_control_test.go index 7856cc1ac..272de68c9 100644 --- a/channeldb/payment_control_test.go +++ b/channeldb/payment_control_test.go @@ -126,7 +126,9 @@ func TestPaymentControlSwitchFail(t *testing.T) { } err = pControl.FailAttempt( - info.PaymentHash, 2, &HTLCFailInfo{}, + info.PaymentHash, 2, &HTLCFailInfo{ + Reason: HTLCFailUnreadable, + }, ) if err != nil { t.Fatal(err) @@ -362,7 +364,9 @@ func TestPaymentControlDeleteNonInFligt(t *testing.T) { // Fail the payment attempt. err := pControl.FailAttempt( info.PaymentHash, attempt.AttemptID, - &HTLCFailInfo{}, + &HTLCFailInfo{ + Reason: HTLCFailUnreadable, + }, ) if err != nil { t.Fatalf("unable to fail htlc: %v", err) diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index 565262edd..96441d179 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -60,7 +60,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) { if sendErr != nil { // TODO(joostjager): Distinguish unexpected // internal errors from real send errors. - err = p.failAttempt() + err = p.failAttempt(sendErr) if err != nil { return [32]byte{}, nil, err } @@ -117,7 +117,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) { "the Switch, retrying.", p.attempt.AttemptID, p.payment.PaymentHash) - err = p.failAttempt() + err = p.failAttempt(err) if err != nil { return [32]byte{}, nil, err } @@ -158,7 +158,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) { log.Errorf("Attempt to send payment %x failed: %v", p.payment.PaymentHash, result.Error) - err = p.failAttempt() + err = p.failAttempt(result.Error) if err != nil { return [32]byte{}, nil, err } @@ -447,13 +447,60 @@ func (p *paymentLifecycle) handleSendError(sendErr error) error { } // failAttempt calls control tower to fail the current payment attempt. -func (p *paymentLifecycle) failAttempt() error { - failInfo := &channeldb.HTLCFailInfo{ - FailTime: p.router.cfg.Clock.Now(), - } +func (p *paymentLifecycle) failAttempt(sendError error) error { + failInfo := marshallError( + sendError, + p.router.cfg.Clock.Now(), + ) return p.router.cfg.Control.FailAttempt( p.payment.PaymentHash, p.attempt.AttemptID, failInfo, ) } + +// marshallError marshall an error as received from the switch to a structure +// that is suitable for database storage. +func marshallError(sendError error, time time.Time) *channeldb.HTLCFailInfo { + response := &channeldb.HTLCFailInfo{ + FailTime: time, + } + + switch sendError { + + case htlcswitch.ErrPaymentIDNotFound: + response.Reason = channeldb.HTLCFailInternal + return response + + case htlcswitch.ErrUnreadableFailureMessage: + response.Reason = channeldb.HTLCFailUnreadable + return response + } + + rtErr, ok := sendError.(htlcswitch.ClearTextError) + if !ok { + response.Reason = channeldb.HTLCFailInternal + return response + } + + message := rtErr.WireMessage() + if message != nil { + response.Reason = channeldb.HTLCFailMessage + response.Message = message + } else { + response.Reason = channeldb.HTLCFailUnknown + } + + // If the ClearTextError received is a ForwardingError, the error + // originated from a node along the route, not locally on our outgoing + // link. We set failureSourceIdx to the index of the node where the + // failure occurred. If the error is not a ForwardingError, the failure + // occurred at our node, so we leave the index as 0 to indicate that + // we failed locally. + fErr, ok := rtErr.(*htlcswitch.ForwardingError) + if ok { + response.FailureSourceIndex = uint32(fErr.FailureSourceIdx) + } + + return response +}