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 dd31156e72..fb9c0fea56 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 @@ -119,11 +119,15 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR log.info(s"forwarding htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} to shortChannelId=$selectedShortChannelId") register ! Register.ForwardShortId(selectedShortChannelId, cmdAdd) } - case Left(badOnion) => + case Left(badOnion: BadOnion) => log.warning(s"couldn't parse onion: reason=${badOnion.message}") val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, FailureMessageCodecs.failureCode(badOnion), commit = true) log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} reason=malformed onionHash=${cmdFail.onionHash} failureCode=${cmdFail.failureCode}") commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) + case Left(failure) => + log.warning(s"couldn't process onion: reason=${failure.message}") + val cmdFail = CMD_FAIL_HTLC(add.id, Right(failure), commit = true) + commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) } case Status.Failure(Register.ForwardShortIdFailure(Register.ForwardShortId(shortChannelId, CMD_ADD_HTLC(_, _, _, _, Right(add), _, _)))) => @@ -229,12 +233,12 @@ object Relayer extends Logging { * @param privateKey this node's private key * @return the payload for the next hop or an error. */ - def decryptPacket(add: UpdateAddHtlc, privateKey: PrivateKey, features: ByteVector): Either[BadOnion, NextPayload] = + def decryptPacket(add: UpdateAddHtlc, privateKey: PrivateKey, features: ByteVector): Either[FailureMessage, NextPayload] = Sphinx.PaymentPacket.peel(privateKey, add.paymentHash, add.onionRoutingPacket) match { case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => OnionCodecs.perHopPayloadCodec.decode(payload.bits) match { case Attempt.Successful(DecodeResult(OnionPerHopPayload(Left(_)), _)) if !Features.hasVariableLengthOnion(features) => - Left(InvalidOnionPayload(Sphinx.PaymentPacket.hash(add.onionRoutingPacket))) + Left(InvalidRealm) case Attempt.Successful(DecodeResult(perHopPayload, remainder)) => if (remainder.nonEmpty) { logger.warn(s"${remainder.length} bits remaining after per-hop payload decoding: there might be an issue with the onion codec") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index 63a9dd6113..9b342d3582 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -43,7 +43,6 @@ case object RequiredNodeFeatureMissing extends Perm with Node { def message = "p case class InvalidOnionVersion(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion version was not understood by the processing node" } case class InvalidOnionHmac(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion HMAC was incorrect when it reached the processing node" } case class InvalidOnionKey(onionHash: ByteVector32) extends BadOnion with Perm { def message = "ephemeral key was unparsable by the processing node" } -case class InvalidOnionPayload(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion per-hop payload could not be parsed" } case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def message = s"channel ${update.shortChannelId} is currently unavailable" } case object PermanentChannelFailure extends Perm { def message = "channel is permanently unavailable" } case object RequiredChannelFeatureMissing extends Perm { def message = "channel requires features not present in the onion" } @@ -59,6 +58,7 @@ case object FinalExpiryTooSoon extends FailureMessage { def message = "payment e case class FinalIncorrectCltvExpiry(expiry: CltvExpiry) extends FailureMessage { def message = "payment expiry doesn't match the value in the onion" } case class FinalIncorrectHtlcAmount(amount: MilliSatoshi) extends FailureMessage { def message = "payment amount is incorrect in the final htlc" } case object ExpiryTooFar extends FailureMessage { def message = "payment expiry is too far in the future" } +case class InvalidOnionPayload(onionHash: ByteVector32) extends Perm { def message = "onion per-hop payload is invalid" } // @formatter:on object FailureMessageCodecs { @@ -78,7 +78,6 @@ object FailureMessageCodecs { .typecase(NODE | 2, provide(TemporaryNodeFailure)) .typecase(PERM | 2, provide(PermanentNodeFailure)) .typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing)) - .typecase(BADONION | PERM, sha256.as[InvalidOnionPayload]) .typecase(BADONION | PERM | 4, sha256.as[InvalidOnionVersion]) .typecase(BADONION | PERM | 5, sha256.as[InvalidOnionHmac]) .typecase(BADONION | PERM | 6, sha256.as[InvalidOnionKey]) @@ -97,6 +96,7 @@ object FailureMessageCodecs { .typecase(18, ("expiry" | cltvExpiry).as[FinalIncorrectCltvExpiry]) .typecase(19, ("amountMsat" | millisatoshi).as[FinalIncorrectHtlcAmount]) .typecase(21, provide(ExpiryTooFar)) + .typecase(PERM, sha256.as[InvalidOnionPayload]) /** * Return the failure code for a given failure message. This method actually encodes the failure message, which is a diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index 8f7db6047d..dd04104112 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -308,11 +308,7 @@ class RelayerSpec extends TestkitBaseClass { val add_ab = UpdateAddHtlc(channelId_ab, 123456, amount_ab, paymentHash, expiry_ab, onion) sender.send(relayer, ForwardAdd(add_ab)) - val fail = register.expectMsgType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]].message - assert(fail.id === add_ab.id) - assert(fail.onionHash == Sphinx.PaymentPacket.hash(add_ab.onionRoutingPacket)) - assert(fail.failureCode === (FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM)) - + register.expectMsg(Register.Forward(channelId_ab, CMD_FAIL_HTLC(add_ab.id, Right(InvalidOnionPayload(Sphinx.PaymentPacket.hash(add_ab.onionRoutingPacket))), commit = true))) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) } @@ -333,11 +329,7 @@ class RelayerSpec extends TestkitBaseClass { val add_ab = UpdateAddHtlc(channelId_ab, 123456, amount_ab, paymentHash, expiry_ab, onion) sender.send(relayer, ForwardAdd(add_ab)) - val fail = register.expectMsgType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]].message - assert(fail.id === add_ab.id) - assert(fail.onionHash == Sphinx.PaymentPacket.hash(add_ab.onionRoutingPacket)) - assert(fail.failureCode === (FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM)) - + register.expectMsg(Register.Forward(channelId_ab, CMD_FAIL_HTLC(add_ab.id, Right(InvalidRealm), commit = true))) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) } @@ -460,11 +452,7 @@ class RelayerSpec extends TestkitBaseClass { val add_ab = UpdateAddHtlc(channelId_ab, 123456, amount_ab, paymentHash, expiry_ab, onion) sender.send(relayer, ForwardAdd(add_ab)) - val fail = register.expectMsgType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]].message - assert(fail.id === add_ab.id) - assert(fail.onionHash == Sphinx.PaymentPacket.hash(add_ab.onionRoutingPacket)) - assert(fail.failureCode === (FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM)) - + register.expectMsg(Register.Forward(channelId_ab, CMD_FAIL_HTLC(add_ab.id, Right(InvalidOnionPayload(Sphinx.PaymentPacket.hash(add_ab.onionRoutingPacket))), commit = true))) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index 615a7f1adf..445e55ec32 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -44,10 +44,10 @@ class FailureMessageCodecsSpec extends FunSuite { test("encode/decode all channel messages") { val msgs: List[FailureMessage] = InvalidRealm :: TemporaryNodeFailure :: PermanentNodeFailure :: RequiredNodeFeatureMissing :: - InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: InvalidOnionPayload(randomBytes32) :: + InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: TemporaryChannelFailure(channelUpdate) :: PermanentChannelFailure :: RequiredChannelFeatureMissing :: UnknownNextPeer :: AmountBelowMinimum(123456 msat, channelUpdate) :: FeeInsufficient(546463 msat, channelUpdate) :: IncorrectCltvExpiry(CltvExpiry(1211), channelUpdate) :: ExpiryTooSoon(channelUpdate) :: - IncorrectOrUnknownPaymentDetails(123456 msat) :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(CltvExpiry(1234)) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: Nil + IncorrectOrUnknownPaymentDetails(123456 msat) :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(CltvExpiry(1234)) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: InvalidOnionPayload(randomBytes32) :: Nil msgs.foreach { msg => { @@ -62,8 +62,7 @@ class FailureMessageCodecsSpec extends FunSuite { val msgs = Map( (BADONION | PERM | 4) -> InvalidOnionVersion(randomBytes32), (BADONION | PERM | 5) -> InvalidOnionHmac(randomBytes32), - (BADONION | PERM | 6) -> InvalidOnionKey(randomBytes32), - (BADONION | PERM) -> InvalidOnionPayload(randomBytes32) + (BADONION | PERM | 6) -> InvalidOnionKey(randomBytes32) ) for ((code, message) <- msgs) {