diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index c21a02459..cb4becbff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -1556,7 +1556,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu case DirectedHtlc(OUT, add) => add }.filter { case add => - Try(Sphinx.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket)) + Sphinx.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket) .map(_.nextPacket.isLastPacket) .getOrElse(true) // we also fail htlcs which onion we can't decode (message won't be precise) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala index 7f2ae24bb..b2b420a66 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelExceptions.scala @@ -3,7 +3,7 @@ package fr.acinq.eclair.channel import fr.acinq.bitcoin.{BinaryData, Transaction} import fr.acinq.eclair.UInt64 import fr.acinq.eclair.payment.Origin -import fr.acinq.eclair.wire.ChannelUpdate +import fr.acinq.eclair.wire.{ChannelUpdate, UpdateAddHtlc} /** * Created by PM on 11/04/2017. @@ -44,6 +44,7 @@ case class TooManyAcceptedHtlcs (override val channelId: BinaryDa case class InsufficientFunds (override val channelId: BinaryData, amountMsat: Long, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"insufficient funds: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis fees=$feesSatoshis") case class InvalidHtlcPreimage (override val channelId: BinaryData, id: Long) extends ChannelException(channelId, s"invalid htlc preimage for htlc id=$id") case class UnknownHtlcId (override val channelId: BinaryData, id: Long) extends ChannelException(channelId, s"unknown htlc id=$id") +case class CannotExtractSharedSecret (override val channelId: BinaryData, htlc: UpdateAddHtlc) extends ChannelException(channelId, s"can't extract shared secret: paymentHash=${htlc.paymentHash} onion=${htlc.onionRoutingPacket}") case class FundeeCannotSendUpdateFee (override val channelId: BinaryData) extends ChannelException(channelId, s"only the funder should send update_fee messages") case class CannotAffordFees (override val channelId: BinaryData, missingSatoshis: Long, reserveSatoshis: Long, feesSatoshis: Long) extends ChannelException(channelId, s"can't pay the fee: missingSatoshis=$missingSatoshis reserveSatoshis=$reserveSatoshis feesSatoshis=$feesSatoshis") case class CannotSignWithoutChanges (override val channelId: BinaryData) extends ChannelException(channelId, "cannot sign when there are no changes") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 27673464e..d0ed70f60 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -10,6 +10,8 @@ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, UInt64} +import scala.util.{Failure, Success} + // @formatter:off case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) { def all: List[UpdateMessage] = proposed ++ signed ++ acked @@ -211,14 +213,17 @@ object Commitments { throw UnknownHtlcId(commitments.channelId, cmd.id) case Some(htlc) => // we need the shared secret to build the error packet - val sharedSecret = Sphinx.parsePacket(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket).sharedSecret - val reason = cmd.reason match { - case Left(forwarded) => Sphinx.forwardErrorPacket(forwarded, sharedSecret) - case Right(failure) => Sphinx.createErrorPacket(sharedSecret, failure) + Sphinx.parsePacket(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket).map(_.sharedSecret) match { + case Success(sharedSecret) => + val reason = cmd.reason match { + case Left(forwarded) => Sphinx.forwardErrorPacket(forwarded, sharedSecret) + case Right(failure) => Sphinx.createErrorPacket(sharedSecret, failure) + } + val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason) + val commitments1 = addLocalProposal(commitments, fail) + (commitments1, fail) + case Failure(_) => throw new CannotExtractSharedSecret(commitments.channelId, htlc) } - val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason) - val commitments1 = addLocalProposal(commitments, fail) - (commitments1, fail) case None => throw UnknownHtlcId(commitments.channelId, cmd.id) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index f527b37dd..61358d645 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -13,6 +13,7 @@ import org.spongycastle.crypto.params.KeyParameter import scodec.bits.BitVector import scala.annotation.tailrec +import scala.util.{Failure, Success, Try} /** * Created by fabrice on 13/01/17. @@ -163,7 +164,7 @@ object Sphinx extends Logging { * - shared secret is the secret we share with the node that sent the packet. We need it to propagate failure * messages upstream. */ - def parsePacket(privateKey: PrivateKey, associatedData: BinaryData, rawPacket: BinaryData): ParsedPacket = { + def parsePacket(privateKey: PrivateKey, associatedData: BinaryData, rawPacket: BinaryData): Try[ParsedPacket] = Try { require(rawPacket.length == PacketLength, s"onion packet length is ${rawPacket.length}, it should be ${PacketLength}") val packet = Packet.read(rawPacket) val sharedSecret = computeSharedSecret(PublicKey(packet.publicKey), privateKey) @@ -183,10 +184,11 @@ object Sphinx extends Logging { } @tailrec - def extractSharedSecrets(packet: BinaryData, privateKey: PrivateKey, associatedData: BinaryData, acc: Seq[BinaryData] = Nil): Seq[BinaryData] = { + private def extractSharedSecrets(packet: BinaryData, privateKey: PrivateKey, associatedData: BinaryData, acc: Seq[BinaryData] = Nil): Try[Seq[BinaryData]] = { parsePacket(privateKey, associatedData, packet) match { - case ParsedPacket(_, nextPacket, sharedSecret) if nextPacket.isLastPacket => acc :+ sharedSecret - case ParsedPacket(_, nextPacket, sharedSecret) => extractSharedSecrets(nextPacket.serialize, privateKey, associatedData, acc :+ sharedSecret) + case Success(ParsedPacket(_, nextPacket, sharedSecret)) if nextPacket.isLastPacket => Success(acc :+ sharedSecret) + case Success(ParsedPacket(_, nextPacket, sharedSecret)) => extractSharedSecrets(nextPacket.serialize, privateKey, associatedData, acc :+ sharedSecret) + case Failure(t) => Failure(t) } } @@ -205,7 +207,7 @@ object Sphinx extends Logging { * @param routingInfoFiller optional routing info filler, needed only when you're constructing the last packet * @return the next packet */ - def makeNextPacket(payload: BinaryData, associatedData: BinaryData, ephemerealPublicKey: BinaryData, sharedSecret: BinaryData, packet: Packet, routingInfoFiller: BinaryData = BinaryData.empty): Packet = { + private def makeNextPacket(payload: BinaryData, associatedData: BinaryData, ephemerealPublicKey: BinaryData, sharedSecret: BinaryData, packet: Packet, routingInfoFiller: BinaryData = BinaryData.empty): Packet = { require(payload.length == PayloadLength) val nextRoutingInfo = { @@ -298,7 +300,7 @@ object Sphinx extends Logging { * @param packet error packet * @return the failure message that is embedded in the error packet */ - def extractFailureMessage(packet: BinaryData): FailureMessage = { + private def extractFailureMessage(packet: BinaryData): FailureMessage = { require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength") val (mac, payload) = packet.splitAt(Sphinx.MacLength) val len = Protocol.uint16(payload, ByteOrder.BIG_ENDIAN) @@ -327,7 +329,7 @@ object Sphinx extends Logging { * @param packet error packet * @return true if the packet's mac is valid, which means that it has been properly de-obfuscated */ - def checkMac(sharedSecret: BinaryData, packet: BinaryData): Boolean = { + private def checkMac(sharedSecret: BinaryData, packet: BinaryData): Boolean = { val (mac, payload) = packet.splitAt(Sphinx.MacLength) val um = Sphinx.generateKey("um", sharedSecret) BinaryData(mac) == Sphinx.mac(um, payload) @@ -339,17 +341,20 @@ object Sphinx extends Logging { * * @param packet error packet * @param sharedSecrets nodes shared secrets - * @return Some(secret, failure message) if the origin of the packet could be identified and the packet de-obfuscated, none otherwise + * @return Success(secret, failure message) if the origin of the packet could be identified and the packet de-obfuscated, Failure otherwise */ - @tailrec - def parseErrorPacket(packet: BinaryData, sharedSecrets: Seq[(BinaryData, PublicKey)]): Option[ErrorPacket] = { + def parseErrorPacket(packet: BinaryData, sharedSecrets: Seq[(BinaryData, PublicKey)]): Try[ErrorPacket] = Try { require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength") - sharedSecrets match { - case Nil => None + + @tailrec + def loop(packet: BinaryData, sharedSecrets: Seq[(BinaryData, PublicKey)]): ErrorPacket = sharedSecrets match { + case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets") case (secret, pubkey) :: tail => val packet1 = forwardErrorPacket(packet, secret) - if (checkMac(secret, packet1)) Some(ErrorPacket(pubkey, extractFailureMessage(packet1))) else parseErrorPacket(packet1, tail) + if (checkMac(secret, packet1)) ErrorPacket(pubkey, extractFailureMessage(packet1)) else loop(packet1, tail) } + + loop(packet, sharedSecrets) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 93ec8c3d4..661619254 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -11,6 +11,8 @@ import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ import scodec.Attempt +import scala.util.{Failure, Success} + // @formatter:off case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String) case class SendPayment(amountMsat: Long, paymentHash: BinaryData, targetNodeId: PublicKey, minFinalCltvExpiry: Long = PaymentLifecycle.defaultMinFinalCltvExpiry, maxAttempts: Int = 5) @@ -74,25 +76,28 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => Sphinx.parseErrorPacket(fail.reason, sharedSecrets) match { - case None => - log.warning(s"cannot parse returned error ${fail.reason}") - s ! PaymentFailed(c.paymentHash, failures = failures :+ UnreadableRemoteFailure(hops)) - stop(FSM.Normal) - case Some(e@ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => + case Failure(t) => + log.warning(s"cannot parse returned error: ${t.getMessage}") + // in that case we don't know which node is sending garbage, let's try to blacklist all nodes except the one we are directly connected to and the destination node + val blacklist = hops.map(_.nextNodeId).drop(1).dropRight(1) + log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}") + router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes ++ blacklist, ignoreChannels) + goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(hops)) + case Success(e@ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") s ! PaymentFailed(c.paymentHash, failures = failures :+ RemoteFailure(hops, e)) stop(FSM.Normal) - case Some(e@ErrorPacket(nodeId, failureMessage)) if failures.size + 1 >= c.maxAttempts => + case Success(e@ErrorPacket(nodeId, failureMessage)) if failures.size + 1 >= c.maxAttempts => log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)") log.warning(s"too many failed attempts, failing the payment") s ! PaymentFailed(c.paymentHash, failures = failures :+ RemoteFailure(hops, e)) stop(FSM.Normal) - case Some(e@ErrorPacket(nodeId, failureMessage: Node)) => + case Success(e@ErrorPacket(nodeId, failureMessage: Node)) => log.info(s"received an error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") // let's try to route around this node router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes + nodeId, ignoreChannels) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) - case Some(e@ErrorPacket(nodeId, failureMessage: Update)) => + case Success(e@ErrorPacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") if (Announcements.checkSig(failureMessage.update, nodeId)) { // note that we check the sig, but we don't make sure that this update was for the exact channel we required @@ -119,7 +124,7 @@ class PaymentLifecycle(sourceNodeId: PublicKey, router: ActorRef, register: Acto router ! RouteRequest(sourceNodeId, c.targetNodeId, ignoreNodes + nodeId, ignoreChannels) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) - case Some(e@ErrorPacket(nodeId, failureMessage)) => + case Success(e@ErrorPacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") // let's try again without the channel outgoing from nodeId val faultyChannel = hops.find(_.nodeId == nodeId).map(_.lastUpdate.shortChannelId) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 2658ffd8e..231ef802c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -57,10 +57,8 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR context become main(channelUpdates + (channelUpdate.shortChannelId -> channelUpdate)) case ForwardAdd(add) => - Try(Sphinx.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket)) - .map { - case Sphinx.ParsedPacket(payload, nextPacket, sharedSecret) => (LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(payload.data)), nextPacket, sharedSecret) - } match { + Sphinx.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket) + .map(parsedPacket => (LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(parsedPacket.payload.data)), parsedPacket.nextPacket, parsedPacket.sharedSecret)) match { case Success((Attempt.Successful(DecodeResult(perHopPayload, _)), nextPacket, _)) if nextPacket.isLastPacket => log.info(s"looks like we are the final recipient of htlc #${add.id}") perHopPayload match { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index dc0b7a283..8596a2281 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -7,6 +7,8 @@ import org.junit.runner.RunWith import org.scalatest.FunSuite import org.scalatest.junit.JUnitRunner +import scala.util.Success + /** * Created by fabrice on 10/01/17. */ @@ -64,11 +66,11 @@ class SphinxSpec extends FunSuite { val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, payloads, associatedData) assert(onion.serialize == BinaryData("0x0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a716a996c7845c93d90e4ecbb9bde4ece2f69425c99e4bc820e44485455f135edc0d10f7d61ab590531cf08000179a333a347f8b4072f216400406bdf3bf038659793d4a1fd7b246979e3150a0a4cb052c9ec69acf0f48c3d39cd55675fe717cb7d80ce721caad69320c3a469a202f1e468c67eaf7a7cd8226d0fd32f7b48084dca885d56047694762b67021713ca673929c163ec36e04e40ca8e1c6d17569419d3039d9a1ec866abe044a9ad635778b961fc0776dc832b3a451bd5d35072d2269cf9b040f6b7a7dad84fb114ed413b1426cb96ceaf83825665ed5a1d002c1687f92465b49ed4c7f0218ff8c6c7dd7221d589c65b3b9aaa71a41484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172307c7268724c3618e6817abd793adc214a0dc0bc616816632f27ea336fb56dfd")) - val Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) - val Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) - val Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) - val Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) - val Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) + val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) + val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) + val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) + val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) + val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) assert(Seq(payload0, payload1, payload2, payload3, payload4) == payloads) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) @@ -88,15 +90,15 @@ class SphinxSpec extends FunSuite { // each node parses and forwards the packet // node #0 - val ParsedPacket(payload0, packet1, sharedSecret0) = parsePacket(privKeys(0), associatedData, packet.serialize) + val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) // node #1 - val ParsedPacket(payload1, packet2, sharedSecret1) = parsePacket(privKeys(1), associatedData, packet1.serialize) + val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) // node #2 - val ParsedPacket(payload2, packet3, sharedSecret2) = parsePacket(privKeys(2), associatedData, packet2.serialize) + val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) // node #3 - val ParsedPacket(payload3, packet4, sharedSecret3) = parsePacket(privKeys(3), associatedData, packet3.serialize) + val Success(ParsedPacket(payload3, packet4, sharedSecret3)) = parsePacket(privKeys(3), associatedData, packet3.serialize) // node #4 - val ParsedPacket(payload4, packet5, sharedSecret4) = parsePacket(privKeys(4), associatedData, packet4.serialize) + val Success(ParsedPacket(payload4, packet5, sharedSecret4)) = parsePacket(privKeys(4), associatedData, packet4.serialize) assert(packet5.isLastPacket) // node #4 want to reply with an error message @@ -122,7 +124,7 @@ class SphinxSpec extends FunSuite { // origin parses error packet and can see that it comes from node #4 - val Some(ErrorPacket(pubkey, failure)) = parseErrorPacket(error4, sharedSecrets) + val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error4, sharedSecrets) assert(pubkey == publicKeys(4)) assert(failure == TemporaryNodeFailure) } @@ -135,11 +137,11 @@ class SphinxSpec extends FunSuite { // each node parses and forwards the packet // node #0 - val ParsedPacket(payload0, packet1, sharedSecret0) = parsePacket(privKeys(0), associatedData, packet.serialize) + val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) // node #1 - val ParsedPacket(payload1, packet2, sharedSecret1) = parsePacket(privKeys(1), associatedData, packet1.serialize) + val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) // node #2 - val ParsedPacket(payload2, packet3, sharedSecret2) = parsePacket(privKeys(2), associatedData, packet2.serialize) + val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) // node #2 want to reply with an error message val error = createErrorPacket(sharedSecret2, InvalidRealm) @@ -149,7 +151,7 @@ class SphinxSpec extends FunSuite { val error2 = forwardErrorPacket(error1, sharedSecret0) // origin parses error packet and can see that it comes from node #2 - val Some(ErrorPacket(pubkey, failure)) = parseErrorPacket(error2, sharedSecrets) + val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error2, sharedSecrets) assert(pubkey == publicKeys(2)) assert(failure == InvalidRealm) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala index bb3d0835c..7460da928 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala @@ -12,6 +12,8 @@ import org.scalatest.FunSuite import org.scalatest.junit.JUnitRunner import scodec.bits.BitVector +import scala.util.Success + /** * Created by PM on 31/05/2016. */ @@ -51,25 +53,25 @@ class HtlcGenerationSpec extends FunSuite { assert(packet_b.serialize.size === Sphinx.PacketLength) // let's peel the onion - val ParsedPacket(bin_b, packet_c, _) = Sphinx.parsePacket(priv_b, paymentHash, packet_b.serialize) + val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b, paymentHash, packet_b.serialize) val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value assert(packet_c.serialize.size === Sphinx.PacketLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) - val ParsedPacket(bin_c, packet_d, _) = Sphinx.parsePacket(priv_c, paymentHash, packet_c.serialize) + val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c, paymentHash, packet_c.serialize) val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).require.value assert(packet_d.serialize.size === Sphinx.PacketLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) - val ParsedPacket(bin_d, packet_e, _) = Sphinx.parsePacket(priv_d, paymentHash, packet_d.serialize) + val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d, paymentHash, packet_d.serialize) val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).require.value assert(packet_e.serialize.size === Sphinx.PacketLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) - val ParsedPacket(bin_e, packet_random, _) = Sphinx.parsePacket(priv_e, paymentHash, packet_e.serialize) + val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e, paymentHash, packet_e.serialize) val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_e.data)).require.value assert(packet_random.serialize.size === Sphinx.PacketLength) assert(payload_e.amtToForward === finalAmountMsat) @@ -86,25 +88,25 @@ class HtlcGenerationSpec extends FunSuite { assert(add.onion.length === Sphinx.PacketLength) // let's peel the onion - val ParsedPacket(bin_b, packet_c, _) = Sphinx.parsePacket(priv_b, paymentHash, add.onion) + val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b, paymentHash, add.onion) val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value assert(packet_c.serialize.size === Sphinx.PacketLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) - val ParsedPacket(bin_c, packet_d, _) = Sphinx.parsePacket(priv_c, paymentHash, packet_c.serialize) + val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c, paymentHash, packet_c.serialize) val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_c.data)).require.value assert(packet_d.serialize.size === Sphinx.PacketLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) - val ParsedPacket(bin_d, packet_e, _) = Sphinx.parsePacket(priv_d, paymentHash, packet_d.serialize) + val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d, paymentHash, packet_d.serialize) val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_d.data)).require.value assert(packet_e.serialize.size === Sphinx.PacketLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) - val ParsedPacket(bin_e, packet_random, _) = Sphinx.parsePacket(priv_e, paymentHash, packet_e.serialize) + val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e, paymentHash, packet_e.serialize) val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_e.data)).require.value assert(packet_random.serialize.size === Sphinx.PacketLength) assert(payload_e.amtToForward === finalAmountMsat) @@ -120,7 +122,7 @@ class HtlcGenerationSpec extends FunSuite { assert(add.onion.size === Sphinx.PacketLength) // let's peel the onion - val ParsedPacket(bin_b, packet_random, _) = Sphinx.parsePacket(priv_b, paymentHash, add.onion) + val Success(ParsedPacket(bin_b, packet_random, _)) = Sphinx.parsePacket(priv_b, paymentHash, add.onion) val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(bin_b.data)).require.value assert(packet_random.serialize.size === Sphinx.PacketLength) assert(payload_b.amtToForward === finalAmountMsat) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 859fc8125..097cdd16d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -36,6 +36,33 @@ class PaymentLifecycleSpec extends BaseRouterSpec { sender.expectMsg(PaymentFailed(request.paymentHash, LocalFailure(RouteNotFound) :: Nil)) } + test("payment failed (unparseable failure)") { case (router, _) => + val relayer = TestProbe() + val routerForwarder = TestProbe() + val paymentFSM = TestFSMRef(new PaymentLifecycle(a, routerForwarder.ref, relayer.ref)) + val monitor = TestProbe() + val sender = TestProbe() + + paymentFSM ! SubscribeTransitionCallBack(monitor.ref) + val CurrentState(_, WAITING_FOR_REQUEST) = monitor.expectMsgClass(classOf[CurrentState[_]]) + + val request = SendPayment(142000L, "42" * 32, d, maxAttempts = 2) + sender.send(paymentFSM, request) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + val WaitingForRoute(_, _, Nil) = paymentFSM.stateData + routerForwarder.expectMsg(RouteRequest(a, d, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) + routerForwarder.forward(router) + awaitCond(paymentFSM.stateName == WAITING_FOR_PAYMENT_COMPLETE) + val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData + + relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) + sender.send(paymentFSM, UpdateFailHtlc("00" * 32, 0, "42" * 32)) + + // then the payment lifecycle will ask for a new route excluding all intermediate nodes + routerForwarder.expectMsg(RouteRequest(a, d, ignoreNodes = Set(c), ignoreChannels = Set.empty)) + awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) + } + test("payment failed (first hop returns an UpdateFailMalformedHtlc)") { case (router, _) => val relayer = TestProbe() val routerForwarder = TestProbe()