htlcswitch: add linkError field to htlcpacket

This commit adds a linkError field to track the value of failures
which occur at our node. This field is set when local payments or
multi hop htlcs fail in the switch or on our outgoing link. This
addition is required for the addition of a htlc notifier which will
notify these failures in handleDownstreamPacket.

The passing of link error to failAddPacket removes the need for an
additional error field, because the link error's failure detail will
contain any additional metadata. In the places where the failure detail
does not cover all the metadata that was previously supplied by addr
err, the error is logged before calling failAddPacket so that this
change does not reduce the amount of information we log.
This commit is contained in:
carla 2020-02-06 19:35:17 +02:00
parent 9390d3bbfd
commit 74e0d545fe
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
6 changed files with 102 additions and 44 deletions

@ -76,7 +76,7 @@ func (l *LinkError) Error() string {
return l.msg.Error() return l.msg.Error()
} }
return fmt.Sprintf("%v: %v", l.msg.Error(), l.FailureDetail) return l.FailureDetail.FailureString()
} }
// ForwardingError wraps an lnwire.FailureMessage in a struct that also // ForwardingError wraps an lnwire.FailureMessage in a struct that also

@ -42,6 +42,18 @@ const (
// to forward a htlc through our node which arrives and leaves on the // to forward a htlc through our node which arrives and leaves on the
// same channel. // same channel.
OutgoingFailureCircularRoute OutgoingFailureCircularRoute
// OutgoingFailureIncompleteForward is returned when we cancel an incomplete
// forward.
OutgoingFailureIncompleteForward
// OutgoingFailureDownstreamHtlcAdd is returned when we fail to add a
// downstream htlc to our outgoing link.
OutgoingFailureDownstreamHtlcAdd
// OutgoingFailureForwardsDisabled is returned when the switch is
// configured to disallow forwards.
OutgoingFailureForwardsDisabled
) )
// FailureString returns the string representation of a failure detail. // FailureString returns the string representation of a failure detail.
@ -70,6 +82,15 @@ func (fd OutgoingFailure) FailureString() string {
case OutgoingFailureCircularRoute: case OutgoingFailureCircularRoute:
return "same incoming and outgoing channel" return "same incoming and outgoing channel"
case OutgoingFailureIncompleteForward:
return "failed after detecting incomplete forward"
case OutgoingFailureDownstreamHtlcAdd:
return "could not add downstream htlc"
case OutgoingFailureForwardsDisabled:
return "node configured to disallow forwards"
default: default:
return "unknown failure detail" return "unknown failure detail"
} }

@ -1314,6 +1314,10 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket, isReProcess bool) {
reason lnwire.OpaqueReason reason lnwire.OpaqueReason
) )
// Create a temporary channel failure which we
// will send back to our peer if this is a
// forward, or report to the user if the failed
// payment was locally initiated.
failure := l.createFailureWithUpdate( failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage { func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure( return lnwire.NewTemporaryChannelFailure(
@ -1322,28 +1326,43 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket, isReProcess bool) {
}, },
) )
// Encrypt the error back to the source unless // If the payment was locally initiated (which
// the payment was generated locally. // is indicated by a nil obfuscator), we do
// not need to encrypt it back to the sender.
if pkt.obfuscator == nil { if pkt.obfuscator == nil {
var b bytes.Buffer var b bytes.Buffer
err := lnwire.EncodeFailure(&b, failure, 0) err := lnwire.EncodeFailure(&b, failure, 0)
if err != nil { if err != nil {
l.log.Errorf("unable to encode failure: %v", err) l.log.Errorf("unable to "+
"encode failure: %v", err)
l.mailBox.AckPacket(pkt.inKey()) l.mailBox.AckPacket(pkt.inKey())
return return
} }
reason = lnwire.OpaqueReason(b.Bytes()) reason = lnwire.OpaqueReason(b.Bytes())
localFailure = true localFailure = true
} else { } else {
// If the packet is part of a forward,
// (identified by a non-nil obfuscator)
// we need to encrypt the error back to
// the source.
var err error var err error
reason, err = pkt.obfuscator.EncryptFirstHop(failure) reason, err = pkt.obfuscator.EncryptFirstHop(failure)
if err != nil { if err != nil {
l.log.Errorf("unable to obfuscate error: %v", err) l.log.Errorf("unable to "+
"obfuscate error: %v", err)
l.mailBox.AckPacket(pkt.inKey()) l.mailBox.AckPacket(pkt.inKey())
return return
} }
} }
// Create a link error containing the temporary
// channel failure and a detail which indicates
// the we failed to add the htlc.
linkError := NewDetailedLinkError(
failure,
OutgoingFailureDownstreamHtlcAdd,
)
failPkt := &htlcPacket{ failPkt := &htlcPacket{
incomingChanID: pkt.incomingChanID, incomingChanID: pkt.incomingChanID,
incomingHTLCID: pkt.incomingHTLCID, incomingHTLCID: pkt.incomingHTLCID,
@ -1351,6 +1370,7 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket, isReProcess bool) {
sourceRef: pkt.sourceRef, sourceRef: pkt.sourceRef,
hasSource: true, hasSource: true,
localFailure: localFailure, localFailure: localFailure,
linkFailure: linkError,
htlc: &lnwire.UpdateFailHTLC{ htlc: &lnwire.UpdateFailHTLC{
Reason: reason, Reason: reason,
}, },
@ -2461,7 +2481,9 @@ func (l *channelLink) processRemoteSettleFails(fwdPkg *channeldb.FwdPkg,
} }
// Fetch the reason the HTLC was canceled so we can // Fetch the reason the HTLC was canceled so we can
// continue to propagate it. // continue to propagate it. This failure originated
// from another node, so the linkFailure field is not
// set on the packet.
failPacket := &htlcPacket{ failPacket := &htlcPacket{
outgoingChanID: l.ShortChanID(), outgoingChanID: l.ShortChanID(),
outgoingHTLCID: pd.ParentIndex, outgoingHTLCID: pd.ParentIndex,

@ -54,6 +54,11 @@ type htlcPacket struct {
// encrypted with any shared secret. // encrypted with any shared secret.
localFailure bool localFailure bool
// linkFailure is non-nil for htlcs that fail at our node. This may
// occur for our own payments which fail on the outgoing link,
// or for forwards which fail in the switch or on the outgoing link.
linkFailure *LinkError
// convertedError is set to true if this is an HTLC fail that was // convertedError is set to true if this is an HTLC fail that was
// created using an UpdateFailMalformedHTLC from the remote party. If // created using an UpdateFailMalformedHTLC from the remote party. If
// this is true, then when forwarding this failure packet, we'll need // this is true, then when forwarding this failure packet, we'll need

@ -45,11 +45,6 @@ var (
// through the switch and is locked into another commitment txn. // through the switch and is locked into another commitment txn.
ErrDuplicateAdd = errors.New("duplicate add HTLC detected") ErrDuplicateAdd = errors.New("duplicate add HTLC detected")
// ErrIncompleteForward is used when an htlc was already forwarded
// through the switch, but did not get locked into another commitment
// txn.
ErrIncompleteForward = errors.New("incomplete forward detected")
// ErrUnknownErrorDecryptor signals that we were unable to locate the // ErrUnknownErrorDecryptor signals that we were unable to locate the
// error decryptor for this payment. This is likely due to restarting // error decryptor for this payment. This is likely due to restarting
// the daemon. // the daemon.
@ -496,9 +491,12 @@ func (s *Switch) forward(packet *htlcPacket) error {
} else { } else {
failure = lnwire.NewTemporaryChannelFailure(update) failure = lnwire.NewTemporaryChannelFailure(update)
} }
addErr := ErrIncompleteForward
return s.failAddPacket(packet, failure, addErr) linkError := NewDetailedLinkError(
failure, OutgoingFailureIncompleteForward,
)
return s.failAddPacket(packet, linkError)
} }
packet.circuit = circuit packet.circuit = circuit
@ -647,14 +645,14 @@ func (s *Switch) ForwardPackets(linkQuit chan struct{},
} else { } else {
failure = lnwire.NewTemporaryChannelFailure(update) failure = lnwire.NewTemporaryChannelFailure(update)
} }
linkError := NewDetailedLinkError(
failure, OutgoingFailureIncompleteForward,
)
for _, packet := range failedPackets { for _, packet := range failedPackets {
addErr := errors.New("failing packet after " +
"detecting incomplete forward")
// We don't handle the error here since this method // We don't handle the error here since this method
// always returns an error. // always returns an error.
s.failAddPacket(packet, failure, addErr) _ = s.failAddPacket(packet, linkError)
} }
} }
@ -979,10 +977,12 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
// Check if the node is set to reject all onward HTLCs and also make // Check if the node is set to reject all onward HTLCs and also make
// sure that HTLC is not from the source node. // sure that HTLC is not from the source node.
if s.cfg.RejectHTLC && packet.incomingChanID != hop.Source { if s.cfg.RejectHTLC && packet.incomingChanID != hop.Source {
failure := &lnwire.FailChannelDisabled{} failure := NewDetailedLinkError(
addErr := fmt.Errorf("unable to forward any htlcs") &lnwire.FailChannelDisabled{},
OutgoingFailureForwardsDisabled,
)
return s.failAddPacket(packet, failure, addErr) return s.failAddPacket(packet, failure)
} }
if packet.incomingChanID == hop.Source { if packet.incomingChanID == hop.Source {
@ -1002,9 +1002,7 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
s.cfg.AllowCircularRoute, htlc.PaymentHash, s.cfg.AllowCircularRoute, htlc.PaymentHash,
) )
if linkErr != nil { if linkErr != nil {
return s.failAddPacket( return s.failAddPacket(packet, linkErr)
packet, linkErr.WireMessage(), linkErr,
)
} }
s.indexMtx.RLock() s.indexMtx.RLock()
@ -1012,14 +1010,17 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
if err != nil { if err != nil {
s.indexMtx.RUnlock() s.indexMtx.RUnlock()
log.Debugf("unable to find link with "+
"destination %v", packet.outgoingChanID)
// If packet was forwarded from another channel link // If packet was forwarded from another channel link
// than we should notify this link that some error // than we should notify this link that some error
// occurred. // occurred.
failure := &lnwire.FailUnknownNextPeer{} linkError := NewLinkError(
addErr := fmt.Errorf("unable to find link with "+ &lnwire.FailUnknownNextPeer{},
"destination %v", packet.outgoingChanID) )
return s.failAddPacket(packet, failure, addErr) return s.failAddPacket(packet, linkError)
} }
targetPeerKey := targetLink.Peer().PubKey() targetPeerKey := targetLink.Peer().PubKey()
interfaceLinks, _ := s.getLinks(targetPeerKey) interfaceLinks, _ := s.getLinks(targetPeerKey)
@ -1088,12 +1089,12 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
})) }))
} }
addErr := fmt.Errorf("incoming HTLC(%x) violated "+ log.Tracef("incoming HTLC(%x) violated "+
"target outgoing link (id=%v) policy: %v", "target outgoing link (id=%v) policy: %v",
htlc.PaymentHash[:], packet.outgoingChanID, htlc.PaymentHash[:], packet.outgoingChanID,
linkErr) linkErr)
return s.failAddPacket(packet, linkErr.WireMessage(), addErr) return s.failAddPacket(packet, linkErr)
} }
// Send the packet to the destination channel link which // Send the packet to the destination channel link which
@ -1225,13 +1226,11 @@ func checkCircularForward(incoming, outgoing lnwire.ShortChannelID,
// failAddPacket encrypts a fail packet back to an add packet's source. // failAddPacket encrypts a fail packet back to an add packet's source.
// The ciphertext will be derived from the failure message proivded by context. // The ciphertext will be derived from the failure message proivded by context.
// This method returns the failErr if all other steps complete successfully. // This method returns the failErr if all other steps complete successfully.
func (s *Switch) failAddPacket(packet *htlcPacket, func (s *Switch) failAddPacket(packet *htlcPacket, failure *LinkError) error {
failure lnwire.FailureMessage, failErr error) error {
// Encrypt the failure so that the sender will be able to read the error // Encrypt the failure so that the sender will be able to read the error
// message. Since we failed this packet, we use EncryptFirstHop to // message. Since we failed this packet, we use EncryptFirstHop to
// obfuscate the failure for their eyes only. // obfuscate the failure for their eyes only.
reason, err := packet.obfuscator.EncryptFirstHop(failure) reason, err := packet.obfuscator.EncryptFirstHop(failure.WireMessage())
if err != nil { if err != nil {
err := fmt.Errorf("unable to obfuscate "+ err := fmt.Errorf("unable to obfuscate "+
"error: %v", err) "error: %v", err)
@ -1239,13 +1238,14 @@ func (s *Switch) failAddPacket(packet *htlcPacket,
return err return err
} }
log.Error(failErr) log.Error(failure.Error())
failPkt := &htlcPacket{ failPkt := &htlcPacket{
sourceRef: packet.sourceRef, sourceRef: packet.sourceRef,
incomingChanID: packet.incomingChanID, incomingChanID: packet.incomingChanID,
incomingHTLCID: packet.incomingHTLCID, incomingHTLCID: packet.incomingHTLCID,
circuit: packet.circuit, circuit: packet.circuit,
linkFailure: failure,
htlc: &lnwire.UpdateFailHTLC{ htlc: &lnwire.UpdateFailHTLC{
Reason: reason, Reason: reason,
}, },
@ -1261,7 +1261,7 @@ func (s *Switch) failAddPacket(packet *htlcPacket,
return err return err
} }
return failErr return failure
} }
// closeCircuit accepts a settle or fail htlc and the associated htlc packet and // closeCircuit accepts a settle or fail htlc and the associated htlc packet and
@ -1869,8 +1869,11 @@ func (s *Switch) reforwardSettleFails(fwdPkgs []*channeldb.FwdPkg) {
// commitment state, so we'll forward this to the switch so the // commitment state, so we'll forward this to the switch so the
// backwards undo can continue. // backwards undo can continue.
case lnwallet.Fail: case lnwallet.Fail:
// Fetch the reason the HTLC was canceled so we can // Fetch the reason the HTLC was canceled so
// continue to propagate it. // we can continue to propagate it. This
// failure originated from another node, so
// the linkFailure field is not set on this
// packet.
failPacket := &htlcPacket{ failPacket := &htlcPacket{
outgoingChanID: fwdPkg.Source, outgoingChanID: fwdPkg.Source,
outgoingHTLCID: pd.ParentIndex, outgoingHTLCID: pd.ParentIndex,

@ -211,10 +211,13 @@ func TestSwitchSendPending(t *testing.T) {
// Send the ADD packet, this should not be forwarded out to the link // Send the ADD packet, this should not be forwarded out to the link
// since there are no eligible links. // since there are no eligible links.
err = s.forward(packet) err = s.forward(packet)
expErr := fmt.Sprintf("unable to find link with destination %v", linkErr, ok := err.(*LinkError)
aliceChanID) if !ok {
if err != nil && err.Error() != expErr { t.Fatalf("expected link error, got: %T", err)
t.Fatalf("expected forward failure: %v", err) }
if linkErr.WireMessage().Code() != lnwire.CodeUnknownNextPeer {
t.Fatalf("expected fail unknown next peer, got: %T",
linkErr.WireMessage().Code())
} }
// No message should be sent, since the packet was failed. // No message should be sent, since the packet was failed.
@ -1070,9 +1073,13 @@ func TestSwitchForwardFailAfterHalfAdd(t *testing.T) {
// Resend the failed htlc, it should be returned to alice since the // Resend the failed htlc, it should be returned to alice since the
// switch will detect that it has been half added previously. // switch will detect that it has been half added previously.
err = s2.forward(ogPacket) err = s2.forward(ogPacket)
if err != ErrIncompleteForward { linkErr, ok := err.(*LinkError)
t.Fatal("unexpected error when reforwarding a "+ if !ok {
"failed packet", err) t.Fatalf("expected link error, got: %T", err)
}
if linkErr.FailureDetail != OutgoingFailureIncompleteForward {
t.Fatalf("expected incomplete forward, got: %v",
linkErr.FailureDetail)
} }
// After detecting an incomplete forward, the fail packet should have // After detecting an incomplete forward, the fail packet should have