From 2d378b3280199e923a29e37d14a1355a64186ab8 Mon Sep 17 00:00:00 2001 From: Andrey Samokhvalov Date: Thu, 29 Jun 2017 16:40:45 +0300 Subject: [PATCH] htlcswitch+router: add onion error obfuscation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Within the network, it's important that when an HTLC forwarding failure occurs, the recipient is notified in a timely manner in order to ensure that errors are graceful and not unknown. For that reason with accordance to BOLT №4 onion failure obfuscation have been added. --- htlcswitch/circuit.go | 8 +- htlcswitch/failure.go | 87 +++++++++++++ htlcswitch/iterator.go | 64 +++++++--- htlcswitch/link.go | 257 ++++++++++++++++++++++++++++---------- htlcswitch/link_test.go | 49 ++++---- htlcswitch/mock.go | 57 ++++++++- htlcswitch/packet.go | 37 ++++-- htlcswitch/switch.go | 92 ++++++++++---- htlcswitch/switch_test.go | 42 ++++--- htlcswitch/test_utils.go | 95 ++++++++++---- lnd_test.go | 30 +++-- routing/router.go | 28 +++-- routing/router_test.go | 5 +- 13 files changed, 639 insertions(+), 212 deletions(-) create mode 100644 htlcswitch/failure.go diff --git a/htlcswitch/circuit.go b/htlcswitch/circuit.go index 1aa67d36..bcebc7a1 100644 --- a/htlcswitch/circuit.go +++ b/htlcswitch/circuit.go @@ -38,17 +38,23 @@ type paymentCircuit struct { // request back. Dest lnwire.ShortChannelID + // Obfuscator is used to obfuscate the onion failure before sending it + // back to the originator of the payment. + Obfuscator Obfuscator + // RefCount is used to count the circuits with the same circuit key. RefCount int } // newPaymentCircuit creates new payment circuit instance. -func newPaymentCircuit(src, dest lnwire.ShortChannelID, key circuitKey) *paymentCircuit { +func newPaymentCircuit(src, dest lnwire.ShortChannelID, key circuitKey, + obfuscator Obfuscator) *paymentCircuit { return &paymentCircuit{ Src: src, Dest: dest, PaymentHash: key, RefCount: 1, + Obfuscator: obfuscator, } } diff --git a/htlcswitch/failure.go b/htlcswitch/failure.go new file mode 100644 index 00000000..82e3be45 --- /dev/null +++ b/htlcswitch/failure.go @@ -0,0 +1,87 @@ +package htlcswitch + +import ( + "bytes" + + "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/lnwire" +) + +// Deobfuscator entity which is used to de-obfuscate the onion opaque reason and +// extract failure. +type Deobfuscator interface { + // Deobfuscate function decodes the onion error failure. + Deobfuscate(lnwire.OpaqueReason) (lnwire.FailureMessage, error) +} + +// Obfuscator entity which is used to do the initial and backward onion +// failure obfuscation. +type Obfuscator interface { + // InitialObfuscate is used to convert the failure into opaque + // reason. + InitialObfuscate(lnwire.FailureMessage) (lnwire.OpaqueReason, error) + + // BackwardObfuscate is used to make the processing over onion error + // when it moves backward to the htlc sender. + BackwardObfuscate(lnwire.OpaqueReason) lnwire.OpaqueReason +} + +// FailureObfuscator is used to obfuscate the onion failure. +type FailureObfuscator struct { + *sphinx.OnionObfuscator +} + +// InitialObfuscate is used by the failure sender to decode the failure and +// make the initial failure obfuscation with addition of the failure data hmac. +// +// NOTE: Part of the Obfuscator interface. +func (o *FailureObfuscator) InitialObfuscate(failure lnwire.FailureMessage) ( + lnwire.OpaqueReason, error) { + var b bytes.Buffer + if err := lnwire.EncodeFailure(&b, failure, 0); err != nil { + return nil, err + } + + // Make the initial obfuscation with appending hmac. + return o.OnionObfuscator.Obfuscate(true, b.Bytes()), nil +} + +// BackwardObfuscate is used by the forwarding nodes in order to obfuscate the +// already obfuscated onion failure blob with the stream which have been +// generated with our shared secret. The reason we re-encrypt the message on the +// backwards path is to ensure that the error is computationally +// indistinguishable from any other error seen. +// +// NOTE: Part of the Obfuscator interface. +func (o *FailureObfuscator) BackwardObfuscate( + reason lnwire.OpaqueReason) lnwire.OpaqueReason { + return o.OnionObfuscator.Obfuscate(false, reason) +} + +// A compile time check to ensure FailureObfuscator implements the +// Obfuscator interface. +var _ Obfuscator = (*FailureObfuscator)(nil) + +// FailureDeobfuscator wraps the sphinx data obfuscator and adds awareness of +// the lnwire onion failure messages to it. +type FailureDeobfuscator struct { + *sphinx.OnionDeobfuscator +} + +// Deobfuscate decodes the obfuscated onion failure. +// +// NOTE: Part of the Obfuscator interface. +func (o *FailureDeobfuscator) Deobfuscate(reason lnwire.OpaqueReason) (lnwire.FailureMessage, + error) { + _, failureData, err := o.OnionDeobfuscator.Deobfuscate(reason) + if err != nil { + return nil, err + } + + r := bytes.NewReader(failureData) + return lnwire.DecodeFailure(r, 0) +} + +// A compile time check to ensure FailureDeobfuscator implements the +// Deobfuscator interface. +var _ Deobfuscator = (*FailureDeobfuscator)(nil) diff --git a/htlcswitch/iterator.go b/htlcswitch/iterator.go index 7dd780da..628f0e34 100644 --- a/htlcswitch/iterator.go +++ b/htlcswitch/iterator.go @@ -4,7 +4,6 @@ import ( "encoding/binary" "io" - "github.com/go-errors/errors" "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" "github.com/roasbeef/btcutil" @@ -139,7 +138,7 @@ func (r *sphinxHopIterator) ForwardingInstructions() ForwardingInfo { } } -// SphinxDecoder is responsible for keeping all sphinx dependent parts inside +// OnionProcessor is responsible for keeping all sphinx dependent parts inside // and expose only decoding function. With such approach we give freedom for // subsystems which wants to decode sphinx path to not be dependable from // sphinx at all. @@ -148,25 +147,23 @@ func (r *sphinxHopIterator) ForwardingInstructions() ForwardingInfo { // maintain the hop iterator abstraction. Without it the structures which using // the hop iterator should contain sphinx router which makes their creations in // tests dependent from the sphinx internal parts. -type SphinxDecoder struct { +type OnionProcessor struct { router *sphinx.Router } -// NewSphinxDecoder creates new instance of decoder. -func NewSphinxDecoder(router *sphinx.Router) *SphinxDecoder { - return &SphinxDecoder{router} +// NewOnionProcessor creates new instance of decoder. +func NewOnionProcessor(router *sphinx.Router) *OnionProcessor { + return &OnionProcessor{router} } -// Decode attempts to decode a valid sphinx packet from the passed io.Reader +// DecodeHopIterator attempts to decode a valid sphinx packet from the passed io.Reader // instance using the rHash as the associated data when checking the relevant // MACs during the decoding process. -func (p *SphinxDecoder) Decode(r io.Reader, rHash []byte) (HopIterator, error) { - // Before adding the new HTLC to the state machine, parse the onion - // object in order to obtain the routing information. +func (p *OnionProcessor) DecodeHopIterator(r io.Reader, rHash []byte) (HopIterator, + lnwire.FailCode) { onionPkt := &sphinx.OnionPacket{} if err := onionPkt.Decode(r); err != nil { - return nil, errors.Errorf("unable to decode onion pkt: %v", - err) + return nil, lnwire.CodeTemporaryChannelFailure } // Attempt to process the Sphinx packet. We include the payment hash of @@ -176,12 +173,49 @@ func (p *SphinxDecoder) Decode(r io.Reader, rHash []byte) (HopIterator, error) { // hash twice, thereby losing their money entirely. sphinxPacket, err := p.router.ProcessOnionPacket(onionPkt, rHash) if err != nil { - return nil, errors.Errorf("unable to process onion pkt: "+ - "%v", err) + switch err { + case sphinx.ErrInvalidOnionVersion: + return nil, lnwire.CodeInvalidOnionVersion + case sphinx.ErrInvalidOnionHMAC: + return nil, lnwire.CodeInvalidOnionHmac + case sphinx.ErrInvalidOnionKey: + return nil, lnwire.CodeInvalidOnionKey + default: + return nil, lnwire.CodeTemporaryChannelFailure + } } return &sphinxHopIterator{ nextPacket: sphinxPacket.NextPacket, processedPacket: sphinxPacket, - }, nil + }, lnwire.CodeNone +} + +// DecodeOnionObfuscator takes the onion blob as input extract the shared secret +// and return the entity which is able to obfuscate failure data. +func (p *OnionProcessor) DecodeOnionObfuscator(r io.Reader) (Obfuscator, + lnwire.FailCode) { + onionPkt := &sphinx.OnionPacket{} + if err := onionPkt.Decode(r); err != nil { + return nil, lnwire.CodeTemporaryChannelFailure + } + + onionObfuscator, err := sphinx.NewOnionObfuscator(p.router, + onionPkt.EphemeralKey) + if err != nil { + switch err { + case sphinx.ErrInvalidOnionVersion: + return nil, lnwire.CodeInvalidOnionVersion + case sphinx.ErrInvalidOnionHMAC: + return nil, lnwire.CodeInvalidOnionHmac + case sphinx.ErrInvalidOnionKey: + return nil, lnwire.CodeInvalidOnionKey + default: + return nil, lnwire.CodeTemporaryChannelFailure + } + } + + return &FailureObfuscator{ + OnionObfuscator: onionObfuscator, + }, lnwire.CodeNone } diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 6efc7097..554dfa10 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -9,6 +9,9 @@ import ( "io" + "crypto/sha256" + + "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/roasbeef/btcd/btcec" @@ -80,10 +83,18 @@ type ChannelLinkConfig struct { // contained in the forwarding blob within each HTLC. Switch *Switch - // DecodeOnion function responsible for decoding htlc Sphinx onion + // DecodeHopIterator function is responsible for decoding htlc Sphinx onion // blob, and creating hop iterator which will give us next destination // of htlc. - DecodeOnion func(r io.Reader, meta []byte) (HopIterator, error) + DecodeHopIterator func(r io.Reader, rHash []byte) (HopIterator, lnwire.FailCode) + + // DecodeOnionObfuscator function is responsible for decoding htlc Sphinx + // onion blob, and creating onion failure obfuscator. + DecodeOnionObfuscator func(r io.Reader) (Obfuscator, lnwire.FailCode) + + // GetLastChannelUpdate retrieve the topology info about the channel in + // order to create the channel update. + GetLastChannelUpdate func() (*lnwire.ChannelUpdate, error) // Peer is a lightning network node with which we have the channel link // opened. @@ -406,32 +417,57 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket) { // commitment chains. htlc.ChanID = l.ChanID() index, err := l.channel.AddHTLC(htlc) - if err == lnwallet.ErrMaxHTLCNumber { - log.Infof("Downstream htlc add update with "+ - "payment hash(%x) have been added to "+ - "reprocessing queue, batch: %v", - htlc.PaymentHash[:], - l.batchCounter) - l.overflowQueue.consume(pkt) - return - } else if err != nil { - failPacket := newFailPacket( - l.ShortChanID(), - &lnwire.UpdateFailHTLC{ - Reason: []byte{byte(0)}, - }, - htlc.PaymentHash, - htlc.Amount, - ) + if err != nil { + switch err { + case lnwallet.ErrMaxHTLCNumber: + log.Infof("Downstream htlc add update with "+ + "payment hash(%x) have been added to "+ + "reprocessing queue, batch: %v", + htlc.PaymentHash[:], + l.batchCounter) + l.overflowQueue.consume(pkt) + return - // The HTLC was unable to be added to the state - // machine, as a result, we'll signal the switch to - // cancel the pending payment. - go l.cfg.Switch.forward(failPacket) + default: + // The HTLC was unable to be added to the state + // machine, as a result, we'll signal the switch to + // cancel the pending payment. + var ( + isObfuscated bool + reason lnwire.OpaqueReason + ) - log.Errorf("unable to handle downstream add HTLC: %v", - err) - return + failure := lnwire.NewTemporaryChannelFailure(nil) + onionReader := bytes.NewReader(htlc.OnionBlob[:]) + obfuscator, failCode := l.cfg.DecodeOnionObfuscator(onionReader) + if failCode != lnwire.CodeNone { + var b bytes.Buffer + err := lnwire.EncodeFailure(&b, failure, 0) + if err != nil { + log.Errorf("unable to encode failure: %v", err) + return + } + reason = lnwire.OpaqueReason(b.Bytes()) + isObfuscated = false + } else { + reason, err = obfuscator.InitialObfuscate(failure) + if err != nil { + log.Errorf("unable to obfuscate error: %v", err) + return + } + isObfuscated = true + } + + go l.cfg.Switch.forward(newFailPacket( + l.ShortChanID(), + &lnwire.UpdateFailHTLC{ + Reason: reason, + }, htlc.PaymentHash, htlc.Amount, isObfuscated, + )) + + log.Infof("Unable to handle downstream add HTLC: %v", err) + return + } } log.Tracef("Received downstream htlc: payment_hash=%x, "+ @@ -439,7 +475,6 @@ func (l *channelLink) handleDownStreamPkt(pkt *htlcPacket) { htlc.PaymentHash[:], index, l.batchCounter+1) htlc.ID = index - l.cfg.Peer.SendMessage(htlc) case *lnwire.UpdateFufillHTLC: @@ -516,9 +551,6 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { log.Tracef("Receive upstream htlc with payment hash(%x), "+ "assigning index: %v", msg.PaymentHash[:], index) - // TODO(roasbeef): perform sanity checks on per-hop payload - // * time-lock is sane, fee, chain, etc - // Store the onion blob which encapsulate the htlc route and // use in on stage of HTLC inclusion to retrieve the next hop // and propagate the HTLC along the remaining route. @@ -535,6 +567,46 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // TODO(roasbeef): add preimage to DB in order to swipe // repeated r-values + + // If remote side have been unable to parse the onion blob we have sent + // to it, than we should transform the malformed notification to the the + // usual htlc fail message. + case *lnwire.UpdateFailMalformedHTLC: + idx := msg.ID + if err := l.channel.ReceiveFailHTLC(idx); err != nil { + l.fail("unable to handle upstream fail HTLC: %v", err) + return + } + + var failure lnwire.FailureMessage + switch msg.FailureCode { + case lnwire.CodeInvalidOnionVersion: + failure = &lnwire.FailInvalidOnionVersion{ + OnionSHA256: msg.ShaOnionBlob, + } + case lnwire.CodeInvalidOnionHmac: + failure = &lnwire.FailInvalidOnionHmac{ + OnionSHA256: msg.ShaOnionBlob, + } + + case lnwire.CodeInvalidOnionKey: + failure = &lnwire.FailInvalidOnionKey{ + OnionSHA256: msg.ShaOnionBlob, + } + default: + log.Errorf("unable to understand code of received " + + "malformed error") + return + } + + var b bytes.Buffer + if err := lnwire.EncodeFailure(&b, failure, 0); err != nil { + log.Errorf("unable to encode malformed error: %v", err) + return + } + + l.cancelReasons[idx] = lnwire.OpaqueReason(b.Bytes()) + case *lnwire.UpdateFailHTLC: idx := msg.ID if err := l.channel.ReceiveFailHTLC(idx); err != nil { @@ -847,7 +919,7 @@ func (l *channelLink) processLockedInHtlcs( packetsToForward = append(packetsToForward, settlePacket) l.overflowQueue.release() - // A failure message for a previously forwarded HTLC has been + // A failureCode message for a previously forwarded HTLC has been // received. As a result a new slot will be freed up in our // commitment state, so we'll forward this to the switch so the // backwards undo can continue. @@ -861,7 +933,7 @@ func (l *channelLink) processLockedInHtlcs( ChanID: l.ChanID(), } failPacket := newFailPacket(l.ShortChanID(), failUpdate, - pd.RHash, pd.Amount) + pd.RHash, pd.Amount, false) // Add the packet to the batch to be forwarded, and // notify the overflow queue that a spare spot has been @@ -880,11 +952,22 @@ func (l *channelLink) processLockedInHtlcs( onionBlob := l.clearedOnionBlobs[pd.Index] delete(l.clearedOnionBlobs, pd.Index) + // Retrieve onion obfuscator from onion blob in order to produce + // initial obfuscation of the onion failureCode. onionReader := bytes.NewReader(onionBlob[:]) + obfuscator, failureCode := l.cfg.DecodeOnionObfuscator(onionReader) + if failureCode != lnwire.CodeNone { + log.Error("unable to decode onion obfuscator") + // If we unable to process the onion blob than we should send + // the malformed htlc error to payment sender. + l.sendMalformedHTLCError(pd.RHash, failureCode, onionBlob[:]) + needUpdate = true + continue + } // Before adding the new htlc to the state machine, // parse the onion object in order to obtain the - // routing information with DecodeOnion function which + // routing information with DecodeHopIterator function which // process the Sphinx packet. // // We include the payment hash of the htlc as it's @@ -893,20 +976,18 @@ func (l *channelLink) processLockedInHtlcs( // attacks. In the case of a replay, an attacker is // *forced* to use the same payment hash twice, thereby // losing their money entirely. - chanIterator, err := l.cfg.DecodeOnion(onionReader, - pd.RHash[:]) - if err != nil { - // If we're unable to parse the Sphinx packet, - // then we'll cancel the htlc. - log.Errorf("unable to get the next hop: %v", err) - reason := []byte{byte(lnwire.SphinxParseError)} - l.sendHTLCError(pd.RHash, reason) + onionReader = bytes.NewReader(onionBlob[:]) + chanIterator, failureCode := l.cfg.DecodeHopIterator(onionReader, pd.RHash[:]) + if failureCode != lnwire.CodeNone { + log.Error("unable to decode onion hop iterator") + // If we unable to process the onion blob than we should send + // the malformed htlc error to payment sender. + l.sendMalformedHTLCError(pd.RHash, failureCode, onionBlob[:]) needUpdate = true continue } fwdInfo := chanIterator.ForwardingInstructions() - switch fwdInfo.NextHop { case exitHop: // We're the designated payment destination. @@ -918,8 +999,8 @@ func (l *channelLink) processLockedInHtlcs( if err != nil { log.Errorf("unable to query to locate:"+ " %v", err) - reason := []byte{byte(lnwire.UnknownPaymentHash)} - l.sendHTLCError(pd.RHash, reason) + failure := lnwire.FailUnknownPaymentHash{} + l.sendHTLCError(pd.RHash, failure, obfuscator) needUpdate = true continue } @@ -940,8 +1021,8 @@ func (l *channelLink) processLockedInHtlcs( invoice.Terms.Value, fwdInfo.AmountToForward) - reason := []byte{byte(lnwire.IncorrectValue)} - l.sendHTLCError(pd.RHash, reason) + failure := lnwire.FailIncorrectPaymentAmount{} + l.sendHTLCError(pd.RHash, failure, obfuscator) needUpdate = true continue } @@ -954,8 +1035,8 @@ func (l *channelLink) processLockedInHtlcs( pd.RHash[:], l.cfg.FwrdingPolicy.TimeLockDelta, fwdInfo.OutgoingCTLV) - reason := []byte{byte(lnwire.UpstreamTimeout)} - l.sendHTLCError(pd.RHash, reason) + failure := lnwire.NewFinalIncorrectCltvExpiry(fwdInfo.OutgoingCTLV) + l.sendHTLCError(pd.RHash, failure, obfuscator) needUpdate = true continue } @@ -970,9 +1051,8 @@ func (l *channelLink) processLockedInHtlcs( log.Errorf("rejecting htlc due to incorrect "+ "amount: expected %v, received %v", invoice.Terms.Value, pd.Amount) - - reason := []byte{byte(lnwire.IncorrectValue)} - l.sendHTLCError(pd.RHash, reason) + failure := lnwire.FailIncorrectPaymentAmount{} + l.sendHTLCError(pd.RHash, failure, obfuscator) needUpdate = true continue } @@ -1016,8 +1096,16 @@ func (l *channelLink) processLockedInHtlcs( pd.RHash[:], l.cfg.FwrdingPolicy.MinHTLC, pd.Amount) - reason := []byte{byte(lnwire.IncorrectValue)} - l.sendHTLCError(pd.RHash, reason) + var failure lnwire.FailureMessage + update, err := l.cfg.GetLastChannelUpdate() + if err != nil { + failure = lnwire.NewTemporaryChannelFailure(nil) + } else { + failure = lnwire.NewAmountBelowMinimum( + pd.Amount, *update) + } + + l.sendHTLCError(pd.RHash, failure, obfuscator) needUpdate = true continue } @@ -1046,9 +1134,16 @@ func (l *channelLink) processLockedInHtlcs( btcutil.Amount(pd.Amount-fwdInfo.AmountToForward), btcutil.Amount(expectedFee)) - // TODO(andrew.shvv): send proper back error - reason := []byte{byte(lnwire.IncorrectValue)} - l.sendHTLCError(pd.RHash, reason) + var failure lnwire.FailureMessage + update, err := l.cfg.GetLastChannelUpdate() + if err != nil { + failure = lnwire.NewTemporaryChannelFailure(nil) + } else { + failure = lnwire.NewFeeInsufficient(pd.Amount, + *update) + } + + l.sendHTLCError(pd.RHash, failure, obfuscator) needUpdate = true continue } @@ -1062,16 +1157,22 @@ func (l *channelLink) processLockedInHtlcs( // with the HTLC. timeDelta := l.cfg.FwrdingPolicy.TimeLockDelta if pd.Timeout-timeDelta != fwdInfo.OutgoingCTLV { - // TODO(andrew.shvv): send proper back error log.Errorf("Incoming htlc(%x) has "+ "incorrect time-lock value: expected "+ "%v blocks, got %v blocks", pd.RHash[:], pd.Timeout-timeDelta, fwdInfo.OutgoingCTLV) - // TODO(andrew.shvv): send proper back error - reason := []byte{byte(lnwire.UpstreamTimeout)} - l.sendHTLCError(pd.RHash, reason) + update, err := l.cfg.GetLastChannelUpdate() + if err != nil { + l.fail("unable to create channel update "+ + "while handling the error: %v", err) + return nil + } + + failure := lnwire.NewIncorrectCltvExpiry( + pd.Timeout, *update) + l.sendHTLCError(pd.RHash, failure, obfuscator) needUpdate = true continue } @@ -1094,14 +1195,15 @@ func (l *channelLink) processLockedInHtlcs( if err != nil { log.Errorf("unable to encode the "+ "remaining route %v", err) - reason := []byte{byte(lnwire.UnknownError)} - l.sendHTLCError(pd.RHash, reason) + + failure := lnwire.NewTemporaryChannelFailure(nil) + l.sendHTLCError(pd.RHash, failure, obfuscator) needUpdate = true continue } updatePacket := newAddPacket(l.ShortChanID(), - fwdInfo.NextHop, addMsg) + fwdInfo.NextHop, addMsg, obfuscator) packetsToForward = append(packetsToForward, updatePacket) } } @@ -1122,8 +1224,13 @@ func (l *channelLink) processLockedInHtlcs( // sendHTLCError functions cancels htlc and send cancel message back to the // peer from which htlc was received. -func (l *channelLink) sendHTLCError(rHash [32]byte, - reason lnwire.OpaqueReason) { +func (l *channelLink) sendHTLCError(rHash [32]byte, failure lnwire.FailureMessage, + obfuscator Obfuscator) { + reason, err := obfuscator.InitialObfuscate(failure) + if err != nil { + log.Errorf("unable to obfuscate error: %v", err) + return + } index, err := l.channel.FailHTLC(rHash) if err != nil { @@ -1138,10 +1245,28 @@ func (l *channelLink) sendHTLCError(rHash [32]byte, }) } +// sendMalformedHTLCError helper function which sends the malformed htlc update +// to the payment sender. +func (l *channelLink) sendMalformedHTLCError(rHash [32]byte, code lnwire.FailCode, + onionBlob []byte) { + index, err := l.channel.FailHTLC(rHash) + if err != nil { + log.Errorf("unable cancel htlc: %v", err) + return + } + + l.cfg.Peer.SendMessage(&lnwire.UpdateFailMalformedHTLC{ + ChanID: l.ChanID(), + ID: index, + ShaOnionBlob: sha256.Sum256(onionBlob), + FailureCode: code, + }) +} + // fail helper function which is used to encapsulate the action neccessary // for proper disconnect. func (l *channelLink) fail(format string, a ...interface{}) { reason := errors.Errorf(format, a...) log.Error(reason) l.cfg.Peer.Disconnect(reason) -} \ No newline at end of file +} diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index 34c4d758..de31b050 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -376,10 +376,10 @@ func TestExitNodeTimelockPayloadMismatch(t *testing.T) { n.bobServer.PubKey(), hops, amount, htlcAmt, htlcExpiry) if err == nil { t.Fatalf("payment should have failed but didn't") - } else if err.Error() != lnwire.UpstreamTimeout.String() { + } else if err.Error() != lnwire.CodeFinalIncorrectCltvExpiry.String() { // TODO(roasbeef): use proper error after error propagation is // in - t.Fatalf("incorrect error, expected insufficient value, "+ + t.Fatalf("incorrect error, expected incorrect cltv expiry, "+ "instead have: %v", err) } } @@ -413,7 +413,7 @@ func TestExitNodeAmountPayloadMismatch(t *testing.T) { n.bobServer.PubKey(), hops, amount, htlcAmt, htlcExpiry) if err == nil { t.Fatalf("payment should have failed but didn't") - } else if err.Error() != lnwire.IncorrectValue.String() { + } else if err.Error() != lnwire.CodeIncorrectPaymentAmount.String() { // TODO(roasbeef): use proper error after error propagation is // in t.Fatalf("incorrect error, expected insufficient value, "+ @@ -455,10 +455,8 @@ func TestLinkForwardTimelockPolicyMismatch(t *testing.T) { // should be rejected due to a policy violation. if err == nil { t.Fatalf("payment should have failed but didn't") - } else if err.Error() != lnwire.UpstreamTimeout.String() { - // TODO(roasbeef): use proper error after error propagation is - // in - t.Fatalf("incorrect error, expected insufficient value, "+ + } else if err.Error() != lnwire.CodeIncorrectCltvExpiry.String() { + t.Fatalf("incorrect error, expected incorrect cltv expiry, "+ "instead have: %v", err) } } @@ -498,10 +496,10 @@ func TestLinkForwardFeePolicyMismatch(t *testing.T) { // should be rejected due to a policy violation. if err == nil { t.Fatalf("payment should have failed but didn't") - } else if err.Error() != lnwire.IncorrectValue.String() { + } else if err.Error() != lnwire.CodeFeeInsufficient.String() { // TODO(roasbeef): use proper error after error propagation is // in - t.Fatalf("incorrect error, expected insufficient value, "+ + t.Fatalf("incorrect error, expected fee insufficient, "+ "instead have: %v", err) } } @@ -541,10 +539,10 @@ func TestLinkForwardMinHTLCPolicyMismatch(t *testing.T) { // should be rejected due to a policy violation (below min HTLC). if err == nil { t.Fatalf("payment should have failed but didn't") - } else if err.Error() != lnwire.IncorrectValue.String() { + } else if err.Error() != lnwire.CodeAmountBelowMinimum.String() { // TODO(roasbeef): use proper error after error propagation is // in - t.Fatalf("incorrect error, expected insufficient value, "+ + t.Fatalf("incorrect error, expected amount below minimum, "+ "instead have: %v", err) } } @@ -633,7 +631,7 @@ func TestChannelLinkMultiHopInsufficientPayment(t *testing.T) { btcutil.SatoshiPerBitcoin*5, ) if err := n.start(); err != nil { - t.Fatalf("can't start three hop network: %v", err) + t.Fatalf("unable to start three hop network: %v", err) } defer n.stop() @@ -656,7 +654,7 @@ func TestChannelLinkMultiHopInsufficientPayment(t *testing.T) { n.bobServer.PubKey(), hops, amount, htlcAmt, totalTimelock) if err == nil { t.Fatal("error haven't been received") - } else if err.Error() != errors.New(lnwire.InsufficientCapacity).Error() { + } else if err.Error() != errors.New(lnwire.CodeTemporaryChannelFailure).Error() { t.Fatalf("wrong error have been received: %v", err) } @@ -702,7 +700,7 @@ func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) { btcutil.SatoshiPerBitcoin*5, ) if err := n.start(); err != nil { - t.Fatalf("can't start three hop network: %v", err) + t.Fatalf("unable to start three hop network: %v", err) } defer n.stop() @@ -733,13 +731,14 @@ func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) { // Check who is last in the route and add invoice to server registry. if err := n.carolServer.registry.AddInvoice(invoice); err != nil { - t.Fatalf("can't add invoice in carol registry: %v", err) + t.Fatalf("unable to add invoice in carol registry: %v", err) } // Send payment and expose err channel. - _, err = n.aliceServer.htlcSwitch.SendHTLC(n.bobServer.PubKey(), htlc) - if err == nil { - t.Fatal("error wasn't received") + _, err = n.aliceServer.htlcSwitch.SendHTLC(n.bobServer.PubKey(), htlc, + newMockDeobfuscator()) + if err.Error() != lnwire.CodeUnknownPaymentHash.String() { + t.Fatal("error haven't been received") } // Wait for Alice to receive the revocation. @@ -802,7 +801,7 @@ func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) { amount, htlcAmt, totalTimelock) if err == nil { t.Fatal("error haven't been received") - } else if err.Error() != errors.New(lnwire.UnknownDestination).Error() { + } else if err.Error() != lnwire.CodeUnknownNextPeer.String() { t.Fatalf("wrong error have been received: %v", err) } @@ -848,14 +847,14 @@ func TestChannelLinkMultiHopDecodeError(t *testing.T) { btcutil.SatoshiPerBitcoin*5, ) if err := n.start(); err != nil { - t.Fatalf("can't start three hop network: %v", err) + t.Fatalf("unable to start three hop network: %v", err) } defer n.stop() // Replace decode function with another which throws an error. - n.carolChannelLink.cfg.DecodeOnion = func(r io.Reader, meta []byte) ( - HopIterator, error) { - return nil, errors.New("some sphinx decode error") + n.carolChannelLink.cfg.DecodeOnionObfuscator = func( + r io.Reader) (Obfuscator, lnwire.FailCode) { + return nil, lnwire.CodeInvalidOnionVersion } carolBandwidthBefore := n.carolChannelLink.Bandwidth() @@ -871,7 +870,7 @@ func TestChannelLinkMultiHopDecodeError(t *testing.T) { n.bobServer.PubKey(), hops, amount, htlcAmt, totalTimelock) if err == nil { t.Fatal("error haven't been received") - } else if err.Error() != errors.New(lnwire.SphinxParseError).Error() { + } else if err.Error() != lnwire.CodeInvalidOnionVersion.String() { t.Fatalf("wrong error have been received: %v", err) } @@ -994,7 +993,7 @@ func TestChannelLinkSingleHopMessageOrdering(t *testing.T) { }) if err := n.start(); err != nil { - t.Fatalf("can't start three hop network: %v", err) + t.Fatalf("unable to start three hop network: %v", err) } defer n.stop() diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index e7cd6688..f2ab707c 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -9,6 +9,8 @@ import ( "io" "sync/atomic" + "bytes" + "github.com/btcsuite/fastsha256" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/chainntnfs" @@ -147,15 +149,60 @@ func (f *ForwardingInfo) encode(w io.Writer) error { var _ HopIterator = (*mockHopIterator)(nil) +// mockObfuscator mock implementation of the failure obfuscator which only +// encodes the failure and do not makes any onion obfuscation. +type mockObfuscator struct{} + +func newMockObfuscator() Obfuscator { + return &mockObfuscator{} +} + +func (o *mockObfuscator) InitialObfuscate(failure lnwire.FailureMessage) ( + lnwire.OpaqueReason, error) { + + var b bytes.Buffer + if err := lnwire.EncodeFailure(&b, failure, 0); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +func (o *mockObfuscator) BackwardObfuscate(reason lnwire.OpaqueReason) lnwire.OpaqueReason { + return reason + +} + +// mockDeobfuscator mock implementation of the failure deobfuscator which +// only decodes the failure do not makes any onion obfuscation. +type mockDeobfuscator struct{} + +func newMockDeobfuscator() Deobfuscator { + return &mockDeobfuscator{} +} + +func (o *mockDeobfuscator) Deobfuscate(reason lnwire.OpaqueReason) (lnwire.FailureMessage, + error) { + r := bytes.NewReader(reason) + failure, err := lnwire.DecodeFailure(r, 0) + if err != nil { + return nil, err + } + return failure, nil +} + +var _ Deobfuscator = (*mockDeobfuscator)(nil) + // mockIteratorDecoder test version of hop iterator decoder which decodes the // encoded array of hops. type mockIteratorDecoder struct{} -func (p *mockIteratorDecoder) Decode(r io.Reader, meta []byte) (HopIterator, error) { +func (p *mockIteratorDecoder) DecodeHopIterator(r io.Reader, meta []byte) ( + HopIterator, lnwire.FailCode) { + var b [4]byte _, err := r.Read(b[:]) if err != nil { - return nil, err + return nil, lnwire.CodeTemporaryChannelFailure } hopLength := binary.BigEndian.Uint32(b[:]) @@ -163,13 +210,13 @@ func (p *mockIteratorDecoder) Decode(r io.Reader, meta []byte) (HopIterator, err for i := uint32(0); i < hopLength; i++ { f := &ForwardingInfo{} if err := f.decode(r); err != nil { - return nil, err + return nil, lnwire.CodeTemporaryChannelFailure } hops[i] = *f } - return newMockHopIterator(hops...), nil + return newMockHopIterator(hops...), lnwire.CodeNone } func (f *ForwardingInfo) decode(r io.Reader) error { @@ -223,6 +270,8 @@ func (s *mockServer) readHandler(message lnwire.Message) error { targetChan = msg.ChanID case *lnwire.UpdateFailHTLC: targetChan = msg.ChanID + case *lnwire.UpdateFailMalformedHTLC: + targetChan = msg.ChanID case *lnwire.RevokeAndAck: targetChan = msg.ChanID case *lnwire.CommitSig: diff --git a/htlcswitch/packet.go b/htlcswitch/packet.go index ba39e073..68302bcb 100644 --- a/htlcswitch/packet.go +++ b/htlcswitch/packet.go @@ -29,12 +29,27 @@ type htlcPacket struct { src lnwire.ShortChannelID // amount is the value of the HTLC that is being created or modified. - // // TODO(andrew.shvv) should be removed after introducing sphinx payment. amount btcutil.Amount // htlc lnwire message type of which depends on switch request type. htlc lnwire.Message + + // obfuscator is entity which is needed to make the obfuscation of the + // onion failure, it is carried inside the packet from channel + // link to the switch because we have to create onion error inside the + // switch to, but we unable to restore obfuscator from the onion, because + // on stage of forwarding onion inside payment belongs to the remote node. + // TODO(andrew.shvv) revisit after refactoring the way of returning errors + // inside the htlcswitch packet. + obfuscator Obfuscator + + // isObfuscated is used in case if switch sent the packet to the link, + // but error have occurred locally, in this case we shouldn't obfuscate + // it again. + // TODO(andrew.shvv) revisit after refactoring the way of returning errors + // inside the htlcswitch packet. + isObfuscated bool } // newInitPacket creates htlc switch add packet which encapsulates the add htlc @@ -49,12 +64,13 @@ func newInitPacket(destNode [33]byte, htlc *lnwire.UpdateAddHTLC) *htlcPacket { // newAddPacket creates htlc switch add packet which encapsulates the add htlc // request and additional information for proper forwarding over htlc switch. func newAddPacket(src, dest lnwire.ShortChannelID, - htlc *lnwire.UpdateAddHTLC) *htlcPacket { + htlc *lnwire.UpdateAddHTLC, obfuscator Obfuscator) *htlcPacket { return &htlcPacket{ - dest: dest, - src: src, - htlc: htlc, + dest: dest, + src: src, + htlc: htlc, + obfuscator: obfuscator, } } @@ -77,11 +93,12 @@ func newSettlePacket(src lnwire.ShortChannelID, htlc *lnwire.UpdateFufillHTLC, // add request if something wrong happened on the path to the final // destination. func newFailPacket(src lnwire.ShortChannelID, htlc *lnwire.UpdateFailHTLC, - payHash [sha256.Size]byte, amount btcutil.Amount) *htlcPacket { + payHash [sha256.Size]byte, amount btcutil.Amount, isObfuscated bool) *htlcPacket { return &htlcPacket{ - src: src, - payHash: payHash, - htlc: htlc, - amount: amount, + src: src, + payHash: payHash, + htlc: htlc, + amount: amount, + isObfuscated: isObfuscated, } } diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go index ad927f05..ffbdaf46 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "github.com/davecgh/go-spew/spew" + "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet" @@ -34,6 +35,11 @@ type pendingPayment struct { preimage chan [sha256.Size]byte err chan error + + // deobfuscator is an serializable entity which is used if we received + // an error, it deobfuscates the onion failure blob, and extracts the + // exact error from it. + deobfuscator Deobfuscator } // plexPacket encapsulates switch packet and adds error channel to receive @@ -162,18 +168,17 @@ func New(cfg Config) *Switch { // SendHTLC is used by other subsystems which aren't belong to htlc switch // package in order to send the htlc update. -func (s *Switch) SendHTLC(nextNode [33]byte, update lnwire.Message) ( - [sha256.Size]byte, error) { - - htlc := update.(*lnwire.UpdateAddHTLC) +func (s *Switch) SendHTLC(nextNode [33]byte, htlc *lnwire.UpdateAddHTLC, + deobfuscator Deobfuscator) ([sha256.Size]byte, error) { // Create payment and add to the map of payment in order later to be // able to retrieve it and return response to the user. payment := &pendingPayment{ - err: make(chan error, 1), - preimage: make(chan [sha256.Size]byte, 1), - paymentHash: htlc.PaymentHash, - amount: htlc.Amount, + err: make(chan error, 1), + preimage: make(chan [sha256.Size]byte, 1), + paymentHash: htlc.PaymentHash, + amount: htlc.Amount, + deobfuscator: deobfuscator, } s.pendingMutex.Lock() @@ -259,7 +264,7 @@ func (s *Switch) handleLocalDispatch(payment *pendingPayment, packet *htlcPacket if err != nil { log.Errorf("unable to find links by "+ "destination %v", err) - return errors.New(lnwire.UnknownDestination) + return errors.New(lnwire.CodeUnknownNextPeer) } // Try to find destination channel link with appropriate @@ -278,7 +283,7 @@ func (s *Switch) handleLocalDispatch(payment *pendingPayment, packet *htlcPacket if destination == nil { log.Errorf("unable to find appropriate channel link "+ "insufficient capacity, need %v", htlc.Amount) - return errors.New(lnwire.InsufficientCapacity) + return errors.New(lnwire.CodeTemporaryChannelFailure) } // Send the packet to the destination channel link which @@ -300,11 +305,15 @@ func (s *Switch) handleLocalDispatch(payment *pendingPayment, packet *htlcPacket case *lnwire.UpdateFailHTLC: // Retrieving the fail code from byte representation of error. var userErr error - if code, err := htlc.Reason.ToFailCode(); err != nil { - userErr = errors.Errorf("can't decode fail code id"+ - "(%v): %v", htlc.ID, err) + + failure, err := payment.deobfuscator.Deobfuscate(htlc.Reason) + if err != nil { + userErr = errors.Errorf("unable to de-obfuscate "+ + "onion failure, htlc with hash(%v): %v", payment.paymentHash[:], + err) + log.Error(userErr) } else { - userErr = errors.New(code) + userErr = errors.New(failure.Code()) } // Notify user that his payment was discarded. @@ -342,15 +351,23 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error { // If packet was forwarded from another channel link // than we should notify this link that some error // occurred. - reason := []byte{byte(lnwire.UnknownDestination)} + failure := lnwire.FailUnknownNextPeer{} + reason, err := packet.obfuscator.InitialObfuscate(failure) + if err != nil { + err := errors.Errorf("unable to obfuscate "+ + "error: %v", err) + log.Error(err) + return err + } + go source.HandleSwitchPacket(newFailPacket( packet.src, &lnwire.UpdateFailHTLC{ Reason: reason, }, - htlc.PaymentHash, 0, + htlc.PaymentHash, 0, true, )) - err := errors.Errorf("unable to find link with "+ + err = errors.Errorf("unable to find link with "+ "destination %v", packet.dest) log.Error(err) return err @@ -371,20 +388,28 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error { // over has insufficient capacity, then we'll cancel the htlc // as the payment cannot succeed. if destination == nil { - // If packet was forwarded from another channel link - // than we should notify this link that some error - // occurred. - reason := []byte{byte(lnwire.InsufficientCapacity)} + // If packet was forwarded from another + // channel link than we should notify this + // link that some error occurred. + failure := lnwire.NewTemporaryChannelFailure(nil) + reason, err := packet.obfuscator.InitialObfuscate(failure) + if err != nil { + err := errors.Errorf("unable to obfuscate "+ + "error: %v", err) + log.Error(err) + return err + } + go source.HandleSwitchPacket(newFailPacket( packet.src, &lnwire.UpdateFailHTLC{ Reason: reason, }, htlc.PaymentHash, - 0, + 0, true, )) - err := errors.Errorf("unable to find appropriate "+ + err = errors.Errorf("unable to find appropriate "+ "channel link insufficient capacity, need "+ "%v", htlc.Amount) log.Error(err) @@ -398,17 +423,25 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error { source.ShortChanID(), destination.ShortChanID(), htlc.PaymentHash, + packet.obfuscator, )); err != nil { - reason := []byte{byte(lnwire.UnknownError)} + failure := lnwire.NewTemporaryChannelFailure(nil) + reason, err := packet.obfuscator.InitialObfuscate(failure) + if err != nil { + err := errors.Errorf("unable to obfuscate "+ + "error: %v", err) + log.Error(err) + return err + } + go source.HandleSwitchPacket(newFailPacket( packet.src, &lnwire.UpdateFailHTLC{ Reason: reason, }, - htlc.PaymentHash, - 0, + htlc.PaymentHash, 0, true, )) - err := errors.Errorf("unable to add circuit: "+ + err = errors.Errorf("unable to add circuit: "+ "%v", err) log.Error(err) return err @@ -433,6 +466,11 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error { return err } + // If this is failure than we need to obfuscate the error. + if htlc, ok := htlc.(*lnwire.UpdateFailHTLC); ok && !packet.isObfuscated { + htlc.Reason = circuit.Obfuscator.BackwardObfuscate(htlc.Reason) + } + // Propagating settle/fail htlc back to src of add htlc packet. source, err := s.getLinkByShortID(circuit.Src) if err != nil { diff --git a/htlcswitch/switch_test.go b/htlcswitch/switch_test.go index 6fe06234..cbcf1351 100644 --- a/htlcswitch/switch_test.go +++ b/htlcswitch/switch_test.go @@ -59,7 +59,7 @@ func TestSwitchForward(t *testing.T) { &lnwire.UpdateAddHTLC{ PaymentHash: rhash, Amount: 1, - }, + }, newMockObfuscator(), ) // Handle the request and checks that bob channel link received it. @@ -137,7 +137,7 @@ func TestSwitchCancel(t *testing.T) { &lnwire.UpdateAddHTLC{ PaymentHash: rhash, Amount: 1, - }, + }, newMockObfuscator(), ) // Handle the request and checks that bob channel link received it. @@ -162,7 +162,7 @@ func TestSwitchCancel(t *testing.T) { request = newFailPacket( bobChannelLink.ShortChanID(), &lnwire.UpdateFailHTLC{}, - rhash, 1) + rhash, 1, true) // Handle the request and checks that payment circuit works properly. if err := s.forward(request); err != nil { @@ -213,7 +213,7 @@ func TestSwitchAddSamePayment(t *testing.T) { &lnwire.UpdateAddHTLC{ PaymentHash: rhash, Amount: 1, - }, + }, newMockObfuscator(), ) // Handle the request and checks that bob channel link received it. @@ -247,7 +247,7 @@ func TestSwitchAddSamePayment(t *testing.T) { request = newFailPacket( bobChannelLink.ShortChanID(), &lnwire.UpdateFailHTLC{}, - rhash, 1) + rhash, 1, true) // Handle the request and checks that payment circuit works properly. if err := s.forward(request); err != nil { @@ -308,14 +308,16 @@ func TestSwitchSendPayment(t *testing.T) { // Handle the request and checks that bob channel link received it. errChan := make(chan error) go func() { - _, err := s.SendHTLC(aliceChannelLink.Peer().PubKey(), update) + _, err := s.SendHTLC(aliceChannelLink.Peer().PubKey(), update, + newMockDeobfuscator()) errChan <- err }() go func() { // Send the payment with the same payment hash and same // amount and check that it will be propagated successfully - _, err := s.SendHTLC(aliceChannelLink.Peer().PubKey(), update) + _, err := s.SendHTLC(aliceChannelLink.Peer().PubKey(), update, + newMockDeobfuscator()) errChan <- err }() @@ -345,18 +347,22 @@ func TestSwitchSendPayment(t *testing.T) { t.Fatal("wrong amount of circuits") } - // Create fail request pretending that bob channel link handled the add - // htlc request with error and sent the htlc fail request back. This - // request should be forwarder back to alice channel link. - packet := newFailPacket( - aliceChannelLink.ShortChanID(), + // Create fail request pretending that bob channel link handled + // the add htlc request with error and sent the htlc fail request + // back. This request should be forwarded back to alice channel link. + obfuscator := newMockObfuscator() + failure := lnwire.FailIncorrectPaymentAmount{} + reason, err := obfuscator.InitialObfuscate(failure) + if err != nil { + t.Fatalf("unable obfuscate failure: %v", err) + } + + packet := newFailPacket(aliceChannelLink.ShortChanID(), &lnwire.UpdateFailHTLC{ - Reason: []byte{byte(lnwire.IncorrectValue)}, + Reason: reason, ID: 1, }, - rhash, - 1, - ) + rhash, 1, true) if err := s.forward(packet); err != nil { t.Fatalf("can't forward htlc packet: %v", err) @@ -364,7 +370,7 @@ func TestSwitchSendPayment(t *testing.T) { select { case err := <-errChan: - if err.Error() != errors.New(lnwire.IncorrectValue).Error() { + if err.Error() != errors.New(lnwire.CodeIncorrectPaymentAmount).Error() { t.Fatal("err wasn't received") } case <-time.After(time.Second): @@ -379,7 +385,7 @@ func TestSwitchSendPayment(t *testing.T) { select { case err := <-errChan: - if err.Error() != errors.New(lnwire.IncorrectValue).Error() { + if err.Error() != errors.New(lnwire.CodeIncorrectPaymentAmount).Error() { t.Fatal("err wasn't received") } case <-time.After(time.Second): diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index 6d402425..09c7cb05 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -10,6 +10,10 @@ import ( "io/ioutil" "os" + "io" + + "math/big" + "github.com/btcsuite/fastsha256" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/channeldb" @@ -26,8 +30,34 @@ var ( alicePrivKey = []byte("alice priv key") bobPrivKey = []byte("bob priv key") carolPrivKey = []byte("carol priv key") + + testPrivKey = []byte{ + 0x81, 0xb6, 0x37, 0xd8, 0xfc, 0xd2, 0xc6, 0xda, + 0x63, 0x59, 0xe6, 0x96, 0x31, 0x13, 0xa1, 0x17, + 0xd, 0xe7, 0x95, 0xe4, 0xb7, 0x25, 0xb8, 0x4d, + 0x1e, 0xb, 0x4c, 0xfd, 0x9e, 0xc5, 0x8c, 0xe9, + } + + _, testPubKey = btcec.PrivKeyFromBytes(btcec.S256(), testPrivKey) + testSig = &btcec.Signature{ + R: new(big.Int), + S: new(big.Int), + } + + _, _ = testSig.R.SetString("6372440660162918006277497454296753625158993"+ + "5445068131219452686511677818569431", 10) + _, _ = testSig.S.SetString("1880105606924982582529128710493133386286603"+ + "3135609736119018462340006816851118", 10) ) +// mockGetChanUpdateMessage helper function which returns topology update +// of the channel +func mockGetChanUpdateMessage() (*lnwire.ChannelUpdate, error) { + return &lnwire.ChannelUpdate{ + Signature: testSig, + }, nil +} + // generateRandomBytes returns securely generated random bytes. // It will return an error if the system's secure random // number generator fails to function correctly, in which @@ -423,7 +453,8 @@ func (n *threeHopNetwork) makePayment(sendingPeer, receivingPeer Peer, // Send payment and expose err channel. errChan := make(chan error) go func() { - _, err := sender.htlcSwitch.SendHTLC(firstHopPub, htlc) + _, err := sender.htlcSwitch.SendHTLC(firstHopPub, htlc, + newMockDeobfuscator()) errChan <- err }() @@ -523,14 +554,19 @@ func newThreeHopNetwork(t *testing.T, aliceToBob, BaseFee: btcutil.Amount(1), TimeLockDelta: 1, } - + obfuscator := newMockObfuscator() aliceChannelLink := NewChannelLink( ChannelLinkConfig{ - FwrdingPolicy: globalPolicy, - Peer: bobServer, - Switch: aliceServer.htlcSwitch, - DecodeOnion: decoder.Decode, - Registry: aliceServer.registry, + FwrdingPolicy: globalPolicy, + Peer: bobServer, + Switch: aliceServer.htlcSwitch, + DecodeHopIterator: decoder.DecodeHopIterator, + DecodeOnionObfuscator: func(io.Reader) (Obfuscator, + lnwire.FailCode) { + return obfuscator, lnwire.CodeNone + }, + GetLastChannelUpdate: mockGetChanUpdateMessage, + Registry: aliceServer.registry, }, aliceChannel, ) @@ -540,11 +576,16 @@ func newThreeHopNetwork(t *testing.T, aliceToBob, firstBobChannelLink := NewChannelLink( ChannelLinkConfig{ - FwrdingPolicy: globalPolicy, - Peer: aliceServer, - Switch: bobServer.htlcSwitch, - DecodeOnion: decoder.Decode, - Registry: bobServer.registry, + FwrdingPolicy: globalPolicy, + Peer: aliceServer, + Switch: bobServer.htlcSwitch, + DecodeHopIterator: decoder.DecodeHopIterator, + DecodeOnionObfuscator: func(io.Reader) (Obfuscator, + lnwire.FailCode) { + return obfuscator, lnwire.CodeNone + }, + GetLastChannelUpdate: mockGetChanUpdateMessage, + Registry: bobServer.registry, }, firstBobChannel, ) @@ -554,11 +595,16 @@ func newThreeHopNetwork(t *testing.T, aliceToBob, secondBobChannelLink := NewChannelLink( ChannelLinkConfig{ - FwrdingPolicy: globalPolicy, - Peer: carolServer, - Switch: bobServer.htlcSwitch, - DecodeOnion: decoder.Decode, - Registry: bobServer.registry, + FwrdingPolicy: globalPolicy, + Peer: carolServer, + Switch: bobServer.htlcSwitch, + DecodeHopIterator: decoder.DecodeHopIterator, + DecodeOnionObfuscator: func(io.Reader) (Obfuscator, + lnwire.FailCode) { + return obfuscator, lnwire.CodeNone + }, + GetLastChannelUpdate: mockGetChanUpdateMessage, + Registry: bobServer.registry, }, secondBobChannel, ) @@ -568,11 +614,16 @@ func newThreeHopNetwork(t *testing.T, aliceToBob, carolChannelLink := NewChannelLink( ChannelLinkConfig{ - FwrdingPolicy: globalPolicy, - Peer: bobServer, - Switch: carolServer.htlcSwitch, - DecodeOnion: decoder.Decode, - Registry: carolServer.registry, + FwrdingPolicy: globalPolicy, + Peer: bobServer, + Switch: carolServer.htlcSwitch, + DecodeHopIterator: decoder.DecodeHopIterator, + DecodeOnionObfuscator: func(io.Reader) (Obfuscator, + lnwire.FailCode) { + return obfuscator, lnwire.CodeNone + }, + GetLastChannelUpdate: mockGetChanUpdateMessage, + Registry: carolServer.registry, }, carolChannel, ) diff --git a/lnd_test.go b/lnd_test.go index 0e678c83..1c3a886d 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -2030,10 +2030,11 @@ out: } else if resp.PaymentError == "" { t.Fatalf("payment should have been rejected due to invalid " + "payment hash") - } else if !strings.Contains(resp.PaymentError, "preimage") { + } else if !strings.Contains(resp.PaymentError, + lnwire.CodeUnknownPaymentHash.String()) { // TODO(roasbeef): make into proper gRPC error code - t.Fatalf("payment should have failed due to unknown preimage, "+ - "instead failed due to : %v", err) + t.Fatalf("payment should have failed due to unknown payment hash, "+ + "instead failed due to: %v", resp.PaymentError) } // The balances of all parties should be the same as initially since @@ -2066,9 +2067,10 @@ out: } else if resp.PaymentError == "" { t.Fatalf("payment should have been rejected due to wrong " + "HTLC amount") - } else if !strings.Contains(resp.PaymentError, "htlc value") { + } else if !strings.Contains(resp.PaymentError, + lnwire.CodeIncorrectPaymentAmount.String()) { t.Fatalf("payment should have failed due to wrong amount, "+ - "instead failed due to: %v", err) + "instead failed due to: %v", resp.PaymentError) } // The balances of all parties should be the same as initially since @@ -2129,9 +2131,10 @@ out: } else if resp.PaymentError == "" { t.Fatalf("payment should fail due to insufficient "+ "capacity: %v", err) - } else if !strings.Contains(resp.PaymentError, "capacity") { + } else if !strings.Contains(resp.PaymentError, + lnwire.CodeTemporaryChannelFailure.String()) { t.Fatalf("payment should fail due to insufficient capacity, "+ - "instead: %v", err) + "instead: %v", resp.PaymentError) } // For our final test, we'll ensure that if a target link isn't @@ -2158,9 +2161,10 @@ out: t.Fatalf("payment stream has been closed: %v", err) } else if resp.PaymentError == "" { t.Fatalf("payment should have failed") - } else if !strings.Contains(resp.PaymentError, "hop unknown") { + } else if !strings.Contains(resp.PaymentError, + lnwire.CodeUnknownNextPeer.String()) { t.Fatalf("payment should fail due to unknown hop, instead: %v", - err) + resp.PaymentError) } // Finally, immediately close the channel. This function will also @@ -2575,17 +2579,19 @@ func testAsyncPayments(net *networkHarness, t *harnessTest) { if err := alicePayStream.Send(sendReq); err != nil { t.Fatalf("unable to send payment: "+ - "%v", err) + "stream has been closed: %v", err) } } + // We should receive one insufficient capacity error, because we are + // sending on one invoice bigger. errorReceived := false for i := 0; i < numInvoices; i++ { if resp, err := alicePayStream.Recv(); err != nil { t.Fatalf("payment stream have been closed: %v", err) } else if resp.PaymentError != "" { if strings.Contains(resp.PaymentError, - "Insufficient") { + lnwire.CodeTemporaryChannelFailure.String()) { if errorReceived { t.Fatalf("redundant payment "+ "error: %v", resp.PaymentError) @@ -2595,7 +2601,7 @@ func testAsyncPayments(net *networkHarness, t *harnessTest) { continue } - t.Fatalf("unable to send payment: %v", err) + t.Fatalf("unable to send payment: %v", resp.PaymentError) } } diff --git a/routing/router.go b/routing/router.go index 25e2b7a1..01f07452 100644 --- a/routing/router.go +++ b/routing/router.go @@ -17,6 +17,8 @@ import ( "github.com/roasbeef/btcd/wire" "github.com/roasbeef/btcutil" + "crypto/sha256" + "github.com/go-errors/errors" "github.com/lightningnetwork/lightning-onion" ) @@ -111,8 +113,8 @@ type Config struct { // forward a fully encoded payment to the first hop in the route // denoted by its public key. A non-nil error is to be returned if the // payment was unsuccessful. - SendToSwitch func(firstHop *btcec.PublicKey, - htlcAdd *lnwire.UpdateAddHTLC) ([32]byte, error) + SendToSwitch func(firstHop *btcec.PublicKey, htlcAdd *lnwire.UpdateAddHTLC, + circuit *sphinx.Circuit) ([sha256.Size]byte, error) } // routeTuple is an entry within the ChannelRouter's route cache. We cache @@ -851,7 +853,8 @@ func (r *ChannelRouter) FindRoutes(target *btcec.PublicKey, amt btcutil.Amount) // the onion route specified by the passed layer 3 route. The blob returned // from this function can immediately be included within an HTLC add packet to // be sent to the first hop within the route. -func generateSphinxPacket(route *Route, paymentHash []byte) ([]byte, error) { +func generateSphinxPacket(route *Route, paymentHash []byte) ([]byte, + *sphinx.Circuit, error) { // First obtain all the public keys along the route which are contained // in each hop. nodes := make([]*btcec.PublicKey, len(route.Hops)) @@ -877,7 +880,7 @@ func generateSphinxPacket(route *Route, paymentHash []byte) ([]byte, error) { sessionKey, err := btcec.NewPrivateKey(btcec.S256()) if err != nil { - return nil, err + return nil, nil, err } // Next generate the onion routing packet which allows us to perform @@ -885,14 +888,14 @@ func generateSphinxPacket(route *Route, paymentHash []byte) ([]byte, error) { sphinxPacket, err := sphinx.NewOnionPacket(nodes, sessionKey, hopPayloads, paymentHash) if err != nil { - return nil, err + return nil, nil, err } // Finally, encode Sphinx packet using it's wire representation to be // included within the HTLC add packet. var onionBlob bytes.Buffer if err := sphinxPacket.Encode(&onionBlob); err != nil { - return nil, err + return nil, nil, err } log.Tracef("Generated sphinx packet: %v", @@ -904,7 +907,10 @@ func generateSphinxPacket(route *Route, paymentHash []byte) ([]byte, error) { }), ) - return onionBlob.Bytes(), nil + return onionBlob.Bytes(), &sphinx.Circuit{ + SessionKey: sessionKey, + PaymentPath: nodes, + }, nil } // LightningPayment describes a payment to be sent through the network to the @@ -989,7 +995,8 @@ func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, *Route // Generate the raw encoded sphinx packet to be included along // with the htlcAdd message that we send directly to the // switch. - sphinxPacket, err := generateSphinxPacket(route, payment.PaymentHash[:]) + onionBlob, circuit, err := generateSphinxPacket(route, + payment.PaymentHash[:]) if err != nil { return preImage, nil, err } @@ -1002,13 +1009,14 @@ func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, *Route Expiry: route.TotalTimeLock, PaymentHash: payment.PaymentHash, } - copy(htlcAdd.OnionBlob[:], sphinxPacket) + copy(htlcAdd.OnionBlob[:], onionBlob) // Attempt to send this payment through the network to complete // the payment. If this attempt fails, then we'll continue on // to the next available route. firstHop := route.Hops[0].Channel.Node.PubKey - preImage, sendError = r.cfg.SendToSwitch(firstHop, htlcAdd) + preImage, sendError = r.cfg.SendToSwitch(firstHop, htlcAdd, + circuit) if sendError != nil { log.Errorf("Attempt to send payment %x failed: %v", payment.PaymentHash, sendError) diff --git a/routing/router_test.go b/routing/router_test.go index 392040a0..c47a5a5d 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -10,6 +10,7 @@ import ( "github.com/roasbeef/btcd/wire" "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" "github.com/roasbeef/btcd/btcec" "github.com/roasbeef/btcutil" @@ -80,7 +81,7 @@ func createTestCtx(startingHeight uint32, testGraph ...string) (*testCtx, func() Chain: chain, ChainView: chainView, SendToSwitch: func(_ *btcec.PublicKey, - _ *lnwire.UpdateAddHTLC) ([32]byte, error) { + _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { return [32]byte{}, nil }, }) @@ -175,7 +176,7 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) { // first hop. This should force the router to instead take the // available two hop path (through satoshi). ctx.router.cfg.SendToSwitch = func(n *btcec.PublicKey, - _ *lnwire.UpdateAddHTLC) ([32]byte, error) { + _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { if ctx.aliases["luoji"].IsEqual(n) { return [32]byte{}, errors.New("send error")