1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-20 10:39:19 +01:00

Put Sphinx.parse* function in Try blocks (#283)

Also handle as best as possible the case where we receive an unparseable
failure.

This fixes #280.
This commit is contained in:
Pierre-Marie Padiou 2017-12-15 19:27:56 +01:00 committed by GitHub
parent 28037ea8fe
commit 84ee39d769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 104 additions and 59 deletions

View File

@ -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)
}

View File

@ -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")

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)

View File

@ -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()