htlcswitch: use LinkError for internal errors

Update the ChannelLink interface to specifically
return the LinkError struct. This error implements
the ClearTextError interface, so will be picked
up as a routing realted error by the router.

With LinkErrors implemented, the switch now
returns a LinkError for all failures on our
incoming/outgoing link and ForwardingError when
the failure occurs down the line.
This commit is contained in:
carla 2020-01-14 15:07:42 +02:00
parent b5a2d75465
commit f430fd50c5
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
8 changed files with 185 additions and 79 deletions

@ -33,6 +33,9 @@ type LinkError struct {
// know the failure type for failures which occur at our own
// node.
msg lnwire.FailureMessage
// FailureDetail enriches the wire error with additional information.
FailureDetail
}
// NewLinkError returns a LinkError with the failure message provided.
@ -42,6 +45,17 @@ func NewLinkError(msg lnwire.FailureMessage) *LinkError {
return &LinkError{msg: msg}
}
// NewDetailedLinkError returns a link error that enriches a wire message with
// a failure detail.
func NewDetailedLinkError(msg lnwire.FailureMessage,
detail FailureDetail) *LinkError {
return &LinkError{
msg: msg,
FailureDetail: detail,
}
}
// WireMessage extracts a valid wire failure message from an internal
// error which may contain additional metadata (which should not be
// exposed to the network). This value should never be nil for LinkErrors,
@ -56,9 +70,15 @@ func (l *LinkError) WireMessage() lnwire.FailureMessage {
//
// Note this is part of the ClearTextError interface.
func (l *LinkError) Error() string {
// If the link error has no failure detail, return the wire message's
// error.
if l.FailureDetail == FailureDetailNone {
return l.msg.Error()
}
return fmt.Sprintf("%v: %v", l.msg.Error(), l.FailureDetail)
}
// ForwardingError wraps an lnwire.FailureMessage in a struct that also
// includes the source of the error.
type ForwardingError struct {

@ -0,0 +1,58 @@
package htlcswitch
// FailureDetail is an enum which is used to enrich failures with
// additional information.
type FailureDetail int
const (
// FailureDetailNone is returned when the wire message contains
// sufficient information.
FailureDetailNone = iota
// FailureDetailOnionDecode indicates that we could not decode an
// onion error.
FailureDetailOnionDecode
// FailureDetailLinkNotEligible indicates that a routing attempt was
// made over a link that is not eligible for routing.
FailureDetailLinkNotEligible
// FailureDetailOnChainTimeout indicates that a payment had to be timed
// out on chain before it got past the first hop by us or the remote
// party.
FailureDetailOnChainTimeout
// FailureDetailHTLCExceedsMax is returned when a htlc exceeds our
// policy's maximum htlc amount.
FailureDetailHTLCExceedsMax
// FailureDetailInsufficientBalance is returned when we cannot route a
// htlc due to insufficient outgoing capacity.
FailureDetailInsufficientBalance
)
// String returns the string representation of a failure detail.
func (fd FailureDetail) String() string {
switch fd {
case FailureDetailNone:
return "no failure detail"
case FailureDetailOnionDecode:
return "could not decode onion"
case FailureDetailLinkNotEligible:
return "link not eligible"
case FailureDetailOnChainTimeout:
return "payment was resolved on-chain, then canceled back"
case FailureDetailHTLCExceedsMax:
return "htlc exceeds maximum policy amount"
case FailureDetailInsufficientBalance:
return "insufficient bandwidth to route htlc"
default:
return "unknown failure detail"
}
}

@ -102,20 +102,21 @@ type ChannelLink interface {
// CheckHtlcForward should return a nil error if the passed HTLC details
// satisfy the current forwarding policy fo the target link. Otherwise,
// a valid protocol failure message should be returned in order to
// signal to the source of the HTLC, the policy consistency issue.
// a LinkError with a valid protocol failure message should be returned
// in order to signal to the source of the HTLC, the policy consistency
// issue.
CheckHtlcForward(payHash [32]byte, incomingAmt lnwire.MilliSatoshi,
amtToForward lnwire.MilliSatoshi,
incomingTimeout, outgoingTimeout uint32,
heightNow uint32) lnwire.FailureMessage
heightNow uint32) *LinkError
// CheckHtlcTransit should return a nil error if the passed HTLC details
// satisfy the current channel policy. Otherwise, a valid protocol
// failure message should be returned in order to signal the violation.
// This call is intended to be used for locally initiated payments for
// which there is no corresponding incoming htlc.
// satisfy the current channel policy. Otherwise, a LinkError with a
// valid protocol failure message should be returned in order to signal
// the violation. This call is intended to be used for locally initiated
// payments for which there is no corresponding incoming htlc.
CheckHtlcTransit(payHash [32]byte, amt lnwire.MilliSatoshi,
timeout uint32, heightNow uint32) lnwire.FailureMessage
timeout uint32, heightNow uint32) *LinkError
// Bandwidth returns the amount of milli-satoshis which current link
// might pass through channel link. The value returned from this method

@ -2135,16 +2135,17 @@ func (l *channelLink) UpdateForwardingPolicy(newPolicy ForwardingPolicy) {
l.cfg.FwrdingPolicy = newPolicy
}
// CheckHtlcForward should return a nil error if the passed HTLC details satisfy
// the current forwarding policy fo the target link. Otherwise, a valid
// protocol failure message should be returned in order to signal to the source
// of the HTLC, the policy consistency issue.
// CheckHtlcForward should return a nil error if the passed HTLC details
// satisfy the current forwarding policy fo the target link. Otherwise,
// a LinkError with a valid protocol failure message should be returned
// in order to signal to the source of the HTLC, the policy consistency
// issue.
//
// NOTE: Part of the ChannelLink interface.
func (l *channelLink) CheckHtlcForward(payHash [32]byte,
incomingHtlcAmt, amtToForward lnwire.MilliSatoshi,
incomingTimeout, outgoingTimeout uint32,
heightNow uint32) lnwire.FailureMessage {
heightNow uint32) *LinkError {
l.RLock()
policy := l.cfg.FwrdingPolicy
@ -2176,14 +2177,14 @@ func (l *channelLink) CheckHtlcForward(payHash [32]byte,
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up to date data.
return l.createFailureWithUpdate(
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewFeeInsufficient(
amtToForward, *upd,
)
},
)
return NewLinkError(failure)
}
// Finally, we'll ensure that the time-lock on the outgoing HTLC meets
@ -2198,26 +2199,27 @@ func (l *channelLink) CheckHtlcForward(payHash [32]byte,
// Grab the latest routing policy so the sending node is up to
// date with our current policy.
return l.createFailureWithUpdate(
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewIncorrectCltvExpiry(
incomingTimeout, *upd,
)
},
)
return NewLinkError(failure)
}
return nil
}
// CheckHtlcTransit should return a nil error if the passed HTLC details satisfy the
// current channel policy. Otherwise, a valid protocol failure message should
// be returned in order to signal the violation. This call is intended to be
// used for locally initiated payments for which there is no corresponding
// incoming htlc.
// CheckHtlcTransit should return a nil error if the passed HTLC details
// satisfy the current channel policy. Otherwise, a LinkError with a
// valid protocol failure message should be returned in order to signal
// the violation. This call is intended to be used for locally initiated
// payments for which there is no corresponding incoming htlc.
func (l *channelLink) CheckHtlcTransit(payHash [32]byte,
amt lnwire.MilliSatoshi, timeout uint32,
heightNow uint32) lnwire.FailureMessage {
heightNow uint32) *LinkError {
l.RLock()
policy := l.cfg.FwrdingPolicy
@ -2232,7 +2234,7 @@ func (l *channelLink) CheckHtlcTransit(payHash [32]byte,
// the channel's amount and time lock constraints.
func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
payHash [32]byte, amt lnwire.MilliSatoshi, timeout uint32,
heightNow uint32) lnwire.FailureMessage {
heightNow uint32) *LinkError {
// As our first sanity check, we'll ensure that the passed HTLC isn't
// too small for the next hop. If so, then we'll cancel the HTLC
@ -2244,13 +2246,14 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up to date data.
return l.createFailureWithUpdate(
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewAmountBelowMinimum(
amt, *upd,
)
},
)
return NewLinkError(failure)
}
// Next, ensure that the passed HTLC isn't too large. If so, we'll
@ -2261,11 +2264,12 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up-to-date data.
return l.createFailureWithUpdate(
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(upd)
},
)
return NewDetailedLinkError(failure, FailureDetailHTLCExceedsMax)
}
// We want to avoid offering an HTLC which will expire in the near
@ -2275,12 +2279,12 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
l.log.Errorf("htlc(%x) has an expiry that's too soon: "+
"outgoing_expiry=%v, best_height=%v", payHash[:],
timeout, heightNow)
return l.createFailureWithUpdate(
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewExpiryTooSoon(*upd)
},
)
return NewLinkError(failure)
}
// Check absolute max delta.
@ -2289,16 +2293,19 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
"the future: got %v, but maximum is %v", payHash[:],
timeout-heightNow, l.cfg.MaxOutgoingCltvExpiry)
return &lnwire.FailExpiryTooFar{}
return NewLinkError(&lnwire.FailExpiryTooFar{})
}
// Check to see if there is enough balance in this channel.
if amt > l.Bandwidth() {
return l.createFailureWithUpdate(
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(upd)
},
)
return NewDetailedLinkError(
failure, FailureDetailInsufficientBalance,
)
}
return nil

@ -5541,7 +5541,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("below minhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 100, 50,
200, 150, 0)
if _, ok := result.(*lnwire.FailAmountBelowMinimum); !ok {
if _, ok := result.WireMessage().(*lnwire.FailAmountBelowMinimum); !ok {
t.Fatalf("expected FailAmountBelowMinimum failure code")
}
})
@ -5549,7 +5549,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("above maxhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1200,
200, 150, 0)
if _, ok := result.(*lnwire.FailTemporaryChannelFailure); !ok {
if _, ok := result.WireMessage().(*lnwire.FailTemporaryChannelFailure); !ok {
t.Fatalf("expected FailTemporaryChannelFailure failure code")
}
})
@ -5557,7 +5557,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("insufficient fee", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1005, 1000,
200, 150, 0)
if _, ok := result.(*lnwire.FailFeeInsufficient); !ok {
if _, ok := result.WireMessage().(*lnwire.FailFeeInsufficient); !ok {
t.Fatalf("expected FailFeeInsufficient failure code")
}
})
@ -5565,7 +5565,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("expiry too soon", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 150, 190)
if _, ok := result.(*lnwire.FailExpiryTooSoon); !ok {
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooSoon); !ok {
t.Fatalf("expected FailExpiryTooSoon failure code")
}
})
@ -5573,7 +5573,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("incorrect cltv expiry", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 190, 0)
if _, ok := result.(*lnwire.FailIncorrectCltvExpiry); !ok {
if _, ok := result.WireMessage().(*lnwire.FailIncorrectCltvExpiry); !ok {
t.Fatalf("expected FailIncorrectCltvExpiry failure code")
}
@ -5583,7 +5583,7 @@ func TestCheckHtlcForward(t *testing.T) {
// Check that expiry isn't too far in the future.
result := link.CheckHtlcForward(hash, 1500, 1000,
10200, 10100, 0)
if _, ok := result.(*lnwire.FailExpiryTooFar); !ok {
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooFar); !ok {
t.Fatalf("expected FailExpiryTooFar failure code")
}
})

@ -645,9 +645,9 @@ type mockChannelLink struct {
htlcID uint64
checkHtlcTransitResult lnwire.FailureMessage
checkHtlcTransitResult *LinkError
checkHtlcForwardResult lnwire.FailureMessage
checkHtlcForwardResult *LinkError
}
// completeCircuit is a helper method for adding the finalized payment circuit
@ -707,14 +707,14 @@ func (f *mockChannelLink) HandleChannelUpdate(lnwire.Message) {
func (f *mockChannelLink) UpdateForwardingPolicy(_ ForwardingPolicy) {
}
func (f *mockChannelLink) CheckHtlcForward([32]byte, lnwire.MilliSatoshi,
lnwire.MilliSatoshi, uint32, uint32, uint32) lnwire.FailureMessage {
lnwire.MilliSatoshi, uint32, uint32, uint32) *LinkError {
return f.checkHtlcForwardResult
}
func (f *mockChannelLink) CheckHtlcTransit(payHash [32]byte,
amt lnwire.MilliSatoshi, timeout uint32,
heightNow uint32) lnwire.FailureMessage {
heightNow uint32) *LinkError {
return f.checkHtlcTransitResult
}

@ -756,20 +756,19 @@ func (s *Switch) handleLocalDispatch(pkt *htlcPacket) error {
s.indexMtx.RUnlock()
if err != nil {
log.Errorf("Link %v not found", pkt.outgoingChanID)
return NewForwardingError(
&lnwire.FailUnknownNextPeer{}, 0, "",
)
return NewLinkError(&lnwire.FailUnknownNextPeer{})
}
if !link.EligibleToForward() {
err := fmt.Errorf("Link %v is not available to forward",
log.Errorf("Link %v is not available to forward",
pkt.outgoingChanID)
log.Error(err)
// The update does not need to be populated as the error
// will be returned back to the router.
htlcErr := lnwire.NewTemporaryChannelFailure(nil)
return NewForwardingError(htlcErr, 0, err.Error())
return NewDetailedLinkError(
lnwire.NewTemporaryChannelFailure(nil),
FailureDetailLinkNotEligible,
)
}
// Ensure that the htlc satisfies the outgoing channel policy.
@ -782,8 +781,7 @@ func (s *Switch) handleLocalDispatch(pkt *htlcPacket) error {
if htlcErr != nil {
log.Errorf("Link %v policy for local forward not "+
"satisfied", pkt.outgoingChanID)
return NewForwardingError(htlcErr, 0, "")
return htlcErr
}
return link.HandleSwitchPacket(pkt)
@ -907,35 +905,43 @@ func (s *Switch) parseFailedPayment(deobfuscator ErrorDecrypter,
// decrypt the error, simply decode it them report back to the
// user.
case unencrypted:
var userErr string
r := bytes.NewReader(htlc.Reason)
failureMsg, err := lnwire.DecodeFailure(r, 0)
if err != nil {
userErr = fmt.Sprintf("unable to decode onion "+
"failure (hash=%v, pid=%d): %v",
paymentHash, paymentID, err)
log.Error(userErr)
// If we could not decode the failure reason, return a link
// error indicating that we failed to decode the onion.
linkError := NewDetailedLinkError(
// As this didn't even clear the link, we don't
// need to apply an update here since it goes
// directly to the router.
lnwire.NewTemporaryChannelFailure(nil),
FailureDetailOnionDecode,
)
// As this didn't even clear the link, we don't need to
// apply an update here since it goes directly to the
// router.
failureMsg = lnwire.NewTemporaryChannelFailure(nil)
log.Errorf("%v: (hash=%v, pid=%d): %v",
linkError.FailureDetail, paymentHash, paymentID,
err)
return linkError
}
return NewForwardingError(failureMsg, 0, userErr)
// If we successfully decoded the failure reason, return it.
return NewLinkError(failureMsg)
// A payment had to be timed out on chain before it got past
// the first hop. In this case, we'll report a permanent
// channel failure as this means us, or the remote party had to
// go on chain.
case isResolution && htlc.Reason == nil:
userErr := fmt.Sprintf("payment was resolved "+
"on-chain, then canceled back (hash=%v, pid=%d)",
linkError := NewDetailedLinkError(
&lnwire.FailPermanentChannelFailure{},
FailureDetailOnChainTimeout,
)
log.Info("%v: hash=%v, pid=%d", linkError.FailureDetail,
paymentHash, paymentID)
return NewForwardingError(
&lnwire.FailPermanentChannelFailure{}, 0, userErr,
)
return linkError
// A regular multi-hop payment error that we'll need to
// decrypt.
@ -1002,18 +1008,21 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
// selection process. This way we can return the error for
// precise link that the sender selected, while optimistically
// trying all links to utilize our available bandwidth.
linkErrs := make(map[lnwire.ShortChannelID]lnwire.FailureMessage)
linkErrs := make(map[lnwire.ShortChannelID]*LinkError)
// Try to find destination channel link with appropriate
// bandwidth.
var destination ChannelLink
for _, link := range interfaceLinks {
var failure lnwire.FailureMessage
var failure *LinkError
// We'll skip any links that aren't yet eligible for
// forwarding.
if !link.EligibleToForward() {
failure = &lnwire.FailUnknownNextPeer{}
failure = NewDetailedLinkError(
&lnwire.FailUnknownNextPeer{},
FailureDetailLinkNotEligible,
)
} else {
// We'll ensure that the HTLC satisfies the
// current forwarding conditions of this target
@ -1048,7 +1057,9 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
// If we can't find the error of the source,
// then we'll return an unknown next peer,
// though this should never happen.
linkErr = &lnwire.FailUnknownNextPeer{}
linkErr = NewLinkError(
&lnwire.FailUnknownNextPeer{},
)
log.Warnf("unable to find err source for "+
"outgoing_link=%v, errors=%v",
packet.outgoingChanID, newLogClosure(func() string {
@ -1061,7 +1072,7 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
htlc.PaymentHash[:], packet.outgoingChanID,
linkErr)
return s.failAddPacket(packet, linkErr, addErr)
return s.failAddPacket(packet, linkErr.WireMessage(), addErr)
}
// Send the packet to the destination channel link which

@ -1290,7 +1290,7 @@ func TestSwitchForwardCircuitPersistence(t *testing.T) {
type multiHopFwdTest struct {
name string
eligible1, eligible2 bool
failure1, failure2 lnwire.FailureMessage
failure1, failure2 *LinkError
expectedReply lnwire.FailCode
}
@ -1310,7 +1310,9 @@ func TestSkipIneligibleLinksMultiHopForward(t *testing.T) {
{
name: "policy fail",
eligible1: true,
failure1: lnwire.NewFinalIncorrectCltvExpiry(0),
failure1: NewLinkError(
lnwire.NewFinalIncorrectCltvExpiry(0),
),
expectedReply: lnwire.CodeFinalIncorrectCltvExpiry,
},
@ -1327,9 +1329,14 @@ func TestSkipIneligibleLinksMultiHopForward(t *testing.T) {
{
name: "non-strict policy fail",
eligible1: true,
failure1: lnwire.NewTemporaryChannelFailure(nil),
failure1: NewDetailedLinkError(
lnwire.NewTemporaryChannelFailure(nil),
FailureDetailInsufficientBalance,
),
eligible2: true,
failure2: lnwire.NewFinalIncorrectCltvExpiry(0),
failure2: NewLinkError(
lnwire.NewFinalIncorrectCltvExpiry(0),
),
expectedReply: lnwire.CodeTemporaryChannelFailure,
},
}
@ -1482,7 +1489,9 @@ func testSkipLinkLocalForward(t *testing.T, eligible bool,
aliceChannelLink := newMockChannelLink(
s, chanID1, aliceChanID, alicePeer, eligible,
)
aliceChannelLink.checkHtlcTransitResult = policyResult
aliceChannelLink.checkHtlcTransitResult = NewLinkError(
policyResult,
)
if err := s.AddLink(aliceChannelLink); err != nil {
t.Fatalf("unable to add alice link: %v", err)
}