diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index f63859ff35..a4ed875223 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -54,6 +54,7 @@ eclair { option_anchor_outputs = disabled option_anchors_zero_fee_htlc_tx = disabled option_shutdown_anysegwit = optional + option_onion_messages = disabled trampoline_payment = disabled keysend = disabled } @@ -333,6 +334,10 @@ eclair { "mempool.space" ] } + + onion-messages { + rate-limit-per-second-per-peer = 10 + } } akka { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 5dba8a4a7e..4c392d5642 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -203,6 +203,11 @@ object Features { val mandatory = 26 } + case object OnionMessages extends Feature { + val rfcName = "option_onion_messages" + val mandatory = 38 + } + // TODO: @t-bast: update feature bits once spec-ed (currently reserved here: https://github.com/lightningnetwork/lightning-rfc/issues/605) // We're not advertising these bits yet in our announcements, clients have to assume support. // This is why we haven't added them yet to `areSupported`. @@ -231,6 +236,7 @@ object Features { AnchorOutputs, AnchorOutputsZeroFeeHtlcTx, ShutdownAnySegwit, + OnionMessages, KeySend ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 37fff15cc1..79d295a7e0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -94,7 +94,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, maxPaymentAttempts: Int, enableTrampolinePayment: Boolean, balanceCheckInterval: FiniteDuration, - blockchainWatchdogSources: Seq[String]) { + blockchainWatchdogSources: Seq[String], + onionMessageRateLimitPerSecond: Double) { val privateKey: Crypto.PrivateKey = nodeKeyManager.nodeKey.privateKey val nodeId: PublicKey = nodeKeyManager.nodeId @@ -457,7 +458,8 @@ object NodeParams extends Logging { maxPaymentAttempts = config.getInt("max-payment-attempts"), enableTrampolinePayment = config.getBoolean("trampoline-payments-enable"), balanceCheckInterval = FiniteDuration(config.getDuration("balance-check-interval").getSeconds, TimeUnit.SECONDS), - blockchainWatchdogSources = config.getStringList("blockchain-watchdog.sources").asScala.toSeq + blockchainWatchdogSources = config.getStringList("blockchain-watchdog.sources").asScala.toSeq, + onionMessageRateLimitPerSecond = config.getDouble("onion-messages.rate-limit-per-second-per-peer") ) } } 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 12bd21ce7f..aad4b95b66 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 @@ -24,6 +24,7 @@ import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.Attempt import scodec.bits.ByteVector +import scodec.codecs.provide import scala.annotation.tailrec import scala.util.{Failure, Success, Try} @@ -217,7 +218,7 @@ object Sphinx extends Logging { * @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet. * @return the next packet. */ - def wrap(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Either[ByteVector, protocol.OnionRoutingPacket], onionPayloadFiller: ByteVector = ByteVector.empty): protocol.OnionRoutingPacket = { + def wrap(payload: ByteVector, associatedData: ByteVector, ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Either[ByteVector, protocol.OnionRoutingPacket], onionPayloadFiller: ByteVector = ByteVector.empty): protocol.OnionRoutingPacket = { require(payload.length <= PayloadLength - MacLength, s"packet payload cannot exceed ${PayloadLength - MacLength} bytes") val (currentMac, currentPayload): (ByteVector32, ByteVector) = packet match { @@ -248,7 +249,7 @@ object Sphinx extends Logging { * @return An onion packet with all shared secrets. The onion packet can be sent to the first node in the list, and * the shared secrets (one per node) can be used to parse returned failure messages if needed. */ - def create(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { + def create(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector): PacketAndSecrets = { val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) @@ -272,7 +273,7 @@ object Sphinx extends Logging { * When an invalid onion is received, its hash should be included in the failure message. */ def hash(onion: protocol.OnionRoutingPacket): ByteVector32 = - Crypto.sha256(OnionCodecs.onionRoutingPacketCodec(onion.payload.length.toInt).encode(onion).require.toByteVector) + Crypto.sha256(OnionCodecs.onionRoutingPacketCodec(provide(onion.payload.length.toInt)).encode(onion).require.toByteVector) } @@ -291,6 +292,12 @@ object Sphinx extends Logging { override val PayloadLength = 400 } + /** + * A message onion packet is used when requesting/sending an invoice from/to a remote node when using offers (BOLT12). + * @param PayloadLength SHOULD be 1300 or 32768 + */ + case class MessagePacket(PayloadLength: Int) extends OnionRoutingPacket[Onion.MessagePacket] + /** * A properly decrypted failure from a node in the route. * @@ -396,12 +403,19 @@ object Sphinx extends Logging { case class BlindedNode(blindedPublicKey: PublicKey, encryptedPayload: ByteVector) /** - * @param introductionNode the first node should not be blinded, otherwise the sender cannot locate it. - * @param blindedNodes blinded nodes (not including the introduction node). + * @param introductionNodeId the first node, not be blinded so that the sender can locate it. + * @param blindingKey blinding tweak that can be used by the introduction node to derive the private key that + * matches the blinded public key. + * @param blindedNodes blinded nodes (including the introduction node). */ - case class BlindedRoute(introductionNode: IntroductionNode, blindedNodes: Seq[BlindedNode]) { - val nodeIds: Seq[PublicKey] = introductionNode.publicKey +: blindedNodes.map(_.blindedPublicKey) - val encryptedPayloads: Seq[ByteVector] = introductionNode.encryptedPayload +: blindedNodes.map(_.encryptedPayload) + case class BlindedRoute(introductionNodeId: PublicKey, blindingKey: PublicKey, blindedNodes: Seq[BlindedNode]) { + val introductionNode: IntroductionNode = IntroductionNode(introductionNodeId, blindedNodes.head.blindedPublicKey, blindingKey, blindedNodes.head.encryptedPayload) + val subsequentNodes: Seq[BlindedNode] = blindedNodes.tail + + val blindedNodeIds: Seq[PublicKey] = blindedNodes.map(_.blindedPublicKey) + val encryptedPayloads: Seq[ByteVector] = blindedNodes.map(_.encryptedPayload) + + val nodeIds: Seq[PublicKey] = introductionNodeId +: blindedNodeIds.tail } /** @@ -424,8 +438,7 @@ object Sphinx extends Logging { e = e.multiply(PrivateKey(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes))) (BlindedNode(blindedPublicKey, encryptedPayload ++ mac), blindingKey) }.unzip - val introductionNode = IntroductionNode(publicKeys.head, blindedHops.head.blindedPublicKey, blindingKeys.head, blindedHops.head.encryptedPayload) - BlindedRoute(introductionNode, blindedHops.tail) + BlindedRoute(publicKeys.head, blindingKeys.head, blindedHops) } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 65d9e98e35..f3f1c22e6e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -21,6 +21,7 @@ import akka.event.Logging.MDC import akka.event.{BusLogging, DiagnosticLoggingAdapter} import akka.util.Timeout import com.google.common.net.HostAndPort +import com.google.common.util.concurrent.RateLimiter import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.Features.Wumbo @@ -35,7 +36,7 @@ import fr.acinq.eclair.io.Monitoring.Metrics import fr.acinq.eclair.io.PeerConnection.KillReason import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.wire.protocol -import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, RoutingMessage, UnknownMessage, Warning} +import fr.acinq.eclair.wire.protocol.{Error, HasChannelId, HasTemporaryChannelId, LightningMessage, NodeAddress, OnionMessage, OnionMessages, RoutingMessage, UnknownMessage, Warning} import scodec.bits.ByteVector import java.net.InetSocketAddress @@ -55,6 +56,8 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA import Peer._ + private val messageRelayRateLimiter = RateLimiter.create(nodeParams.onionMessageRateLimitPerSecond) + startWith(INSTANTIATING, Nothing) when(INSTANTIATING) { @@ -243,6 +246,20 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: OnChainA d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_DISCONNECTED) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) gotoConnected(connectionReady, d.channels) + case Event(msg: OnionMessage, d: ConnectedData) => + if (nodeParams.features.hasFeature(Features.OnionMessages) && messageRelayRateLimiter.tryAcquire()) { + OnionMessages.process(nodeParams.privateKey, msg) match { + case OnionMessages.DropMessage(_) => () // We ignore bad messages + case OnionMessages.RelayMessage(nextNodeId, dataToRelay) => context.parent ! Peer.SendOnionMessage(nextNodeId, dataToRelay) + case OnionMessages.ReceiveMessage(_, _) => () // We only relay messages + } + } + stay() + + case Event(Peer.SendOnionMessage(toNodeId, msg), d: ConnectedData) if toNodeId == remoteNodeId => + d.peerConnection ! msg + stay() + case Event(unknownMsg: UnknownMessage, d: ConnectedData) if nodeParams.pluginMessageTags.contains(unknownMsg.tag) => context.system.eventStream.publish(UnknownMessageReceived(self, remoteNodeId, unknownMsg, d.connectionInfo)) stay() @@ -427,6 +444,8 @@ object Peer { def apply(uri: NodeURI): Connect = new Connect(uri.nodeId, Some(uri.address)) } + case class SendOnionMessage(nodeId: PublicKey, message: OnionMessage) extends PossiblyHarmful + case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, channelType_opt: Option[SupportedChannelType], fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) extends PossiblyHarmful { require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index d1f13b7f25..e06b537aed 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -71,6 +71,10 @@ class Switchboard(nodeParams: NodeParams, peerFactory: Switchboard.PeerFactory) case None => sender() ! Status.Failure(new RuntimeException("peer not found")) } + case s@Peer.SendOnionMessage(nodeId, _) => + val peer = createOrGetPeer(nodeId, offlineChannels = Set.empty) + peer forward s + case o: Peer.OpenChannel => getPeer(o.remoteNodeId) match { case Some(peer) => peer forward o diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index 893e35ce77..332fd6f579 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -85,8 +85,8 @@ object IncomingPacket { case Left(failure) => Left(failure) // NB: we don't validate the ChannelRelayPacket here because its fees and cltv depend on what channel we'll choose to use. case Right(DecodedOnionPacket(payload: Onion.ChannelRelayPayload, next)) => Right(ChannelRelayPacket(add, payload, next)) - case Right(DecodedOnionPacket(payload: Onion.FinalTlvPayload, _)) => payload.records.get[OnionTlv.TrampolineOnion] match { - case Some(OnionTlv.TrampolineOnion(trampolinePacket)) => decryptOnion(add.paymentHash, privateKey)(trampolinePacket, Sphinx.TrampolinePacket) match { + case Right(DecodedOnionPacket(payload: Onion.FinalTlvPayload, _)) => payload.records.get[OnionPaymentPayloadTlv.TrampolineOnion] match { + case Some(OnionPaymentPayloadTlv.TrampolineOnion(trampolinePacket)) => decryptOnion(add.paymentHash, privateKey)(trampolinePacket, Sphinx.TrampolinePacket) match { case Left(failure) => Left(failure) case Right(DecodedOnionPacket(innerPayload: Onion.NodeRelayPayload, next)) => validateNodeRelay(add, payload, innerPayload, next) case Right(DecodedOnionPacket(innerPayload: Onion.FinalPayload, _)) => validateFinal(add, payload, innerPayload) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index 9b0df2f1a3..776d5b0419 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -287,7 +287,7 @@ class NodeRelay private(nodeParams: NodeParams, context.log.debug("sending the payment to the next trampoline node") val payFSM = outgoingPaymentFactory.spawnOutgoingPayFSM(context, paymentCfg, multiPart = true) val paymentSecret = randomBytes32() // we generate a new secret to protect against probing attacks - val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routeParams = routeParams, additionalTlvs = Seq(OnionTlv.TrampolineOnion(packetOut))) + val payment = SendMultiPartPayment(payFsmAdapters, paymentSecret, payloadOut.outgoingNodeId, payloadOut.amountToForward, payloadOut.outgoingCltv, nodeParams.maxPaymentAttempts, routeParams = routeParams, additionalTlvs = Seq(OnionPaymentPayloadTlv.TrampolineOnion(packetOut))) payFSM ! payment payFSM } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index e8015f2442..52da97cab2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -320,7 +320,7 @@ object MultiPartPaymentLifecycle { maxAttempts: Int, assistedRoutes: Seq[Seq[ExtraHop]] = Nil, routeParams: RouteParams, - additionalTlvs: Seq[OnionTlv] = Nil, + additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil) { require(totalAmount > 0.msat, s"total amount must be > 0") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala index d0a8cceab9..6e95ec5a62 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala @@ -71,7 +71,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn sender() ! paymentId val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, Nil) val finalExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1) - val finalPayload = Onion.FinalTlvPayload(TlvStream(Seq(OnionTlv.AmountToForward(r.recipientAmount), OnionTlv.OutgoingCltv(finalExpiry), OnionTlv.PaymentData(randomBytes32(), r.recipientAmount), OnionTlv.KeySend(r.paymentPreimage)), r.userCustomTlvs)) + val finalPayload = Onion.FinalTlvPayload(TlvStream(Seq(OnionPaymentPayloadTlv.AmountToForward(r.recipientAmount), OnionPaymentPayloadTlv.OutgoingCltv(finalExpiry), OnionPaymentPayloadTlv.PaymentData(randomBytes32(), r.recipientAmount), OnionPaymentPayloadTlv.KeySend(r.paymentPreimage)), r.userCustomTlvs)) val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) fsm ! PaymentLifecycle.SendPaymentToNode(sender(), r.recipientNodeId, finalPayload, r.maxAttempts, routeParams = r.routeParams) @@ -140,7 +140,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, Some(trampolineSecret)) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, trampoline, r.trampolineFees, r.trampolineExpiryDelta) - payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo) + payFsm ! PaymentLifecycle.SendPaymentToRoute(sender(), Left(r.route), Onion.createMultiPartPayload(r.amount, trampolineAmount, trampolineExpiry, trampolineSecret, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))), r.paymentRequest.routingInfo) case Nil => sender() ! SendPaymentToRouteResponse(paymentId, parentPaymentId, None) val payFsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg) @@ -175,7 +175,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn val trampolineSecret = randomBytes32() val (trampolineAmount, trampolineExpiry, trampolineOnion) = buildTrampolinePayment(r, r.trampolineNodeId, trampolineFees, trampolineExpiryDelta) val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg) - fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionTlv.TrampolineOnion(trampolineOnion))) + fsm ! SendMultiPartPayment(self, trampolineSecret, r.trampolineNodeId, trampolineAmount, trampolineExpiry, nodeParams.maxPaymentAttempts, r.paymentRequest.routingInfo, r.routeParams, Seq(OnionPaymentPayloadTlv.TrampolineOnion(trampolineOnion))) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala index 36b9287b99..a4a36eb161 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecs.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.eclair.wire.Monitoring.{Metrics, Tags} import fr.acinq.eclair.wire.protocol.CommonCodecs._ +import fr.acinq.eclair.wire.protocol.OnionCodecs.onionRoutingPacketCodec import fr.acinq.eclair.{Features, KamonExt} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ @@ -308,6 +309,11 @@ object LightningMessageCodecs { ("timestampRange" | uint32) :: ("tlvStream" | GossipTimestampFilterTlv.gossipTimestampFilterTlvCodec)).as[GossipTimestampFilter] + val onionMessageCodec: Codec[OnionMessage] = ( + ("blindingKey" | publicKey) :: + ("onionPacket" | OnionCodecs.messageOnionPacketCodec) :: + ("tlvStream" | OnionMessageTlv.onionMessageTlvCodec)).as[OnionMessage] + // NB: blank lines to minimize merge conflicts // @@ -361,6 +367,7 @@ object LightningMessageCodecs { .typecase(263, queryChannelRangeCodec) .typecase(264, replyChannelRangeCodec) .typecase(265, gossipTimestampFilterCodec) + .typecase(513, onionMessageCodec) // NB: blank lines to minimize merge conflicts // diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index c7773fc716..3974851076 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -320,6 +320,8 @@ object ReplyChannelRange { case class GossipTimestampFilter(chainHash: ByteVector32, firstTimestamp: TimestampSecond, timestampRange: Long, tlvStream: TlvStream[GossipTimestampFilterTlv] = TlvStream.empty) extends RoutingMessage with HasChainHash +case class OnionMessage(blindingKey: PublicKey, onionRoutingPacket: OnionRoutingPacket, tlvStream: TlvStream[OnionMessageTlv] = TlvStream.empty) extends LightningMessage + // NB: blank lines to minimize merge conflicts // diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Onion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Onion.scala index 7adf76a155..f82338464d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Onion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/Onion.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.wire.protocol import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.{BlindedNode, BlindedRoute} import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.wire.protocol.CommonCodecs._ import fr.acinq.eclair.wire.protocol.TlvCodecs._ @@ -123,18 +124,22 @@ Notes: case class OnionRoutingPacket(version: Int, publicKey: ByteVector, payload: ByteVector, hmac: ByteVector32) /** Tlv types used inside onion messages. */ -sealed trait OnionTlv extends Tlv +sealed trait OnionPaymentPayloadTlv extends Tlv -object OnionTlv { +sealed trait OnionMessagePayloadTlv extends Tlv + +sealed trait BlindedTlv extends Tlv + +object OnionPaymentPayloadTlv { /** Amount to forward to the next node. */ - case class AmountToForward(amount: MilliSatoshi) extends OnionTlv + case class AmountToForward(amount: MilliSatoshi) extends OnionPaymentPayloadTlv /** CLTV value to use for the HTLC offered to the next node. */ - case class OutgoingCltv(cltv: CltvExpiry) extends OnionTlv + case class OutgoingCltv(cltv: CltvExpiry) extends OnionPaymentPayloadTlv /** Id of the channel to use to forward a payment to the next node. */ - case class OutgoingChannelId(shortChannelId: ShortChannelId) extends OnionTlv + case class OutgoingChannelId(shortChannelId: ShortChannelId) extends OnionPaymentPayloadTlv /** * Bolt 11 payment details (only included for the last node). @@ -142,44 +147,53 @@ object OnionTlv { * @param secret payment secret specified in the Bolt 11 invoice. * @param totalAmount total amount in multi-part payments. When missing, assumed to be equal to AmountToForward. */ - case class PaymentData(secret: ByteVector32, totalAmount: MilliSatoshi) extends OnionTlv + case class PaymentData(secret: ByteVector32, totalAmount: MilliSatoshi) extends OnionPaymentPayloadTlv /** * Route blinding lets the recipient provide some encrypted data for each intermediate node in the blinded part of the * route. This data cannot be decrypted or modified by the sender and usually contains information to locate the next * node without revealing it to the sender. */ - case class EncryptedRecipientData(data: ByteVector) extends OnionTlv + case class EncryptedRecipientData(data: ByteVector) extends OnionPaymentPayloadTlv /** Blinding ephemeral public key that should be used to derive shared secrets when using route blinding. */ - case class BlindingPoint(publicKey: PublicKey) extends OnionTlv + case class BlindingPoint(publicKey: PublicKey) extends OnionPaymentPayloadTlv with BlindedTlv /** Id of the next node. */ - case class OutgoingNodeId(nodeId: PublicKey) extends OnionTlv + case class OutgoingNodeId(nodeId: PublicKey) extends OnionPaymentPayloadTlv /** * Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment * because the final recipient doesn't support trampoline. */ - case class InvoiceFeatures(features: ByteVector) extends OnionTlv + case class InvoiceFeatures(features: ByteVector) extends OnionPaymentPayloadTlv /** * Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment * because the final recipient doesn't support trampoline. */ - case class InvoiceRoutingInfo(extraHops: List[List[PaymentRequest.ExtraHop]]) extends OnionTlv + case class InvoiceRoutingInfo(extraHops: List[List[PaymentRequest.ExtraHop]]) extends OnionPaymentPayloadTlv /** An encrypted trampoline onion packet. */ - case class TrampolineOnion(packet: OnionRoutingPacket) extends OnionTlv + case class TrampolineOnion(packet: OnionRoutingPacket) extends OnionPaymentPayloadTlv /** Pre-image included by the sender of a payment in case of a donation */ - case class KeySend(paymentPreimage: ByteVector32) extends OnionTlv + case class KeySend(paymentPreimage: ByteVector32) extends OnionPaymentPayloadTlv + + case class ReplyPath(blindedRoute: BlindedRoute) extends OnionMessagePayloadTlv + + case class EncTlv(bytes: ByteVector) extends OnionMessagePayloadTlv + + case class NextNodeId(nodeId: PublicKey) extends BlindedTlv + + case class Padding(bytes: ByteVector) extends BlindedTlv + case class PathId(bytes: ByteVector) extends BlindedTlv } object Onion { - import OnionTlv._ + import OnionPaymentPayloadTlv._ /* * We use the following architecture for onion payloads: @@ -206,7 +220,11 @@ object Onion { /** Variable-length onion payload with optional additional tlv records. */ sealed trait TlvFormat extends PerHopPayloadFormat { - def records: TlvStream[OnionTlv] + def records: TlvStream[OnionPaymentPayloadTlv] + } + + sealed trait MessageTlvFormat extends PerHopPayloadFormat { + def records: TlvStream[OnionMessagePayloadTlv] } /** Onion packet type (see [[fr.acinq.eclair.crypto.Sphinx.OnionRoutingPacket]]). */ @@ -218,6 +236,9 @@ object Onion { /** See [[fr.acinq.eclair.crypto.Sphinx.TrampolinePacket]]. */ sealed trait TrampolinePacket extends PacketType + /** See [[fr.acinq.eclair.crypto.Sphinx.MessagePacket]]. */ + sealed trait MessagePacket extends PacketType + /** Per-hop payload from an HTLC's payment onion (after decryption and decoding). */ sealed trait PerHopPayload @@ -245,7 +266,7 @@ object Onion { case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with LegacyFormat - case class ChannelRelayTlvPayload(records: TlvStream[OnionTlv]) extends ChannelRelayPayload with TlvFormat { + case class ChannelRelayTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends ChannelRelayPayload with TlvFormat { override val amountToForward = records.get[AmountToForward].get.amount override val outgoingCltv = records.get[OutgoingCltv].get.cltv override val outgoingChannelId = records.get[OutgoingChannelId].get.shortChannelId @@ -253,10 +274,10 @@ object Onion { object ChannelRelayTlvPayload { def apply(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry): ChannelRelayTlvPayload = - ChannelRelayTlvPayload(TlvStream(OnionTlv.AmountToForward(amountToForward), OnionTlv.OutgoingCltv(outgoingCltv), OnionTlv.OutgoingChannelId(outgoingChannelId))) + ChannelRelayTlvPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amountToForward), OnionPaymentPayloadTlv.OutgoingCltv(outgoingCltv), OnionPaymentPayloadTlv.OutgoingChannelId(outgoingChannelId))) } - case class NodeRelayPayload(records: TlvStream[OnionTlv]) extends RelayPayload with TlvFormat with TrampolinePacket { + case class NodeRelayPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends RelayPayload with TlvFormat with TrampolinePacket { val amountToForward = records.get[AmountToForward].get.amount val outgoingCltv = records.get[OutgoingCltv].get.cltv val outgoingNodeId = records.get[OutgoingNodeId].get.nodeId @@ -270,7 +291,7 @@ object Onion { val invoiceRoutingInfo = records.get[InvoiceRoutingInfo].map(_.extraHops) } - case class FinalTlvPayload(records: TlvStream[OnionTlv]) extends FinalPayload with TlvFormat { + case class FinalTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends FinalPayload with TlvFormat { override val amount = records.get[AmountToForward].get.amount override val expiry = records.get[OutgoingCltv].get.cltv override val paymentSecret = records.get[PaymentData].get.secret @@ -281,12 +302,30 @@ object Onion { override val paymentPreimage = records.get[KeySend].map(_.paymentPreimage) } + case class MessageRelayPayload(records: TlvStream[OnionMessagePayloadTlv]) extends MessagePacket with MessageTlvFormat { + val blindedTlv: ByteVector = records.get[EncTlv].get.bytes + } + + case class MessageFinalPayload(records: TlvStream[OnionMessagePayloadTlv]) extends MessagePacket with MessageTlvFormat { + val blindedTlv: ByteVector = records.get[EncTlv].get.bytes + val replyPath: Option[ReplyPath] = records.get[ReplyPath] + } + + case class RelayBlindedTlv(records: TlvStream[BlindedTlv]) { + val nextNodeId: PublicKey = records.get[NextNodeId].get.nodeId + val nextBlinding: Option[PublicKey] = records.get[BlindingPoint].map(_.publicKey) + } + + case class FinalBlindedTlv(records: TlvStream[BlindedTlv]) { + val pathId: Option[ByteVector] = records.get[PathId].map(_.bytes) + } + def createNodeRelayPayload(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey): NodeRelayPayload = NodeRelayPayload(TlvStream(AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(nextNodeId))) /** Create a trampoline inner payload instructing the trampoline node to relay via a non-trampoline payment. */ def createNodeRelayToNonTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: PaymentRequest): NodeRelayPayload = { - val tlvs = Seq[OnionTlv](AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(targetNodeId), InvoiceFeatures(invoice.features.toByteVector), InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList))) + val tlvs = Seq[OnionPaymentPayloadTlv](AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(targetNodeId), InvoiceFeatures(invoice.features.toByteVector), InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList))) val tlvs2 = invoice.paymentSecret.map(s => tlvs :+ PaymentData(s, totalAmount)).getOrElse(tlvs) NodeRelayPayload(TlvStream(tlvs2)) } @@ -294,7 +333,7 @@ object Onion { def createSinglePartPayload(amount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = FinalTlvPayload(TlvStream(Seq(AmountToForward(amount), OutgoingCltv(expiry), PaymentData(paymentSecret, amount)), userCustomTlvs)) - def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, additionalTlvs: Seq[OnionTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = + def createMultiPartPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, additionalTlvs: Seq[OnionPaymentPayloadTlv] = Nil, userCustomTlvs: Seq[GenericTlv] = Nil): FinalPayload = FinalTlvPayload(TlvStream(AmountToForward(amount) +: OutgoingCltv(expiry) +: PaymentData(paymentSecret, totalAmount) +: additionalTlvs, userCustomTlvs)) /** Create a trampoline outer payload. */ @@ -306,19 +345,22 @@ object Onion { object OnionCodecs { import Onion._ - import OnionTlv._ + import OnionPaymentPayloadTlv._ import scodec.codecs._ import scodec.{Attempt, Codec, DecodeResult, Decoder, Err} - def onionRoutingPacketCodec(payloadLength: Int): Codec[OnionRoutingPacket] = ( - ("version" | uint8) :: - ("publicKey" | bytes(33)) :: - ("onionPayload" | bytes(payloadLength)) :: - ("hmac" | bytes32)).as[OnionRoutingPacket] + def onionRoutingPacketCodec(payloadLength: Codec[Int]): Codec[OnionRoutingPacket] = ( + variableSizePrefixedBytes(payloadLength, + ("version" | uint8) ~ + ("publicKey" | bytes(33)), + ("onionPayload" | bytes)) ~ + ("hmac" | bytes32) flattenLeftPairs).as[OnionRoutingPacket] + + val paymentOnionPacketCodec: Codec[OnionRoutingPacket] = onionRoutingPacketCodec(provide(Sphinx.PaymentPacket.PayloadLength)) - val paymentOnionPacketCodec: Codec[OnionRoutingPacket] = onionRoutingPacketCodec(Sphinx.PaymentPacket.PayloadLength) + val trampolineOnionPacketCodec: Codec[OnionRoutingPacket] = onionRoutingPacketCodec(provide(Sphinx.TrampolinePacket.PayloadLength)) - val trampolineOnionPacketCodec: Codec[OnionRoutingPacket] = onionRoutingPacketCodec(Sphinx.TrampolinePacket.PayloadLength) + val messageOnionPacketCodec: Codec[OnionRoutingPacket] = onionRoutingPacketCodec(uint16.xmap(_ - 66, _ + 66)) /** * The 1.1 BOLT spec changed the onion frame format to use variable-length per-hop payloads. @@ -351,7 +393,7 @@ object OnionCodecs { private val keySend: Codec[KeySend] = variableSizeBytesLong(varintoverflow, bytes32).as[KeySend] - private val onionTlvCodec = discriminated[OnionTlv].by(varint) + private val onionPaymentTlvCodec = discriminated[OnionPaymentPayloadTlv].by(varint) .typecase(UInt64(2), amountToForward) .typecase(UInt64(4), outgoingCltv) .typecase(UInt64(6), outgoingChannelId) @@ -365,7 +407,7 @@ object OnionCodecs { .typecase(UInt64(66100), trampolineOnion) .typecase(UInt64(5482373484L), keySend) - val tlvPerHopPayloadCodec: Codec[TlvStream[OnionTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionTlv](onionTlvCodec).complete + val tlvPerHopPayloadCodec: Codec[TlvStream[OnionPaymentPayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionPaymentPayloadTlv](onionPaymentTlvCodec).complete private val legacyRelayPerHopPayloadCodec: Codec[RelayLegacyPayload] = ( ("realm" | constant(ByteVector.fromByte(0))) :: @@ -383,6 +425,15 @@ object OnionCodecs { // @formatter:on } + case class ForbiddenTlv(tag: UInt64) extends Err { + // @formatter:off + val failureMessage: FailureMessage = InvalidOnionPayload(tag, 0) + override def message = failureMessage.message + override def context: List[String] = Nil + override def pushContext(ctx: String): Err = this + // @formatter:on + } + val channelRelayPerHopPayloadCodec: Codec[ChannelRelayPayload] = fallback(tlvPerHopPayloadCodec, legacyRelayPerHopPayloadCodec).narrow({ case Left(tlvs) if tlvs.get[AmountToForward].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(2))) case Left(tlvs) if tlvs.get[OutgoingCltv].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) @@ -412,9 +463,65 @@ object OnionCodecs { case FinalTlvPayload(tlvs) => tlvs }) + private val replyHopCodec: Codec[BlindedNode] = (("nodeId" | publicKey) :: ("encTlv" | variableSizeBytes(uint16, bytes))).as[BlindedNode] + + private val replyPathCodec: Codec[ReplyPath] = (("firstNodeId" | publicKey) :: ("blinding" | publicKey) :: ("path" | list(replyHopCodec).xmap[Seq[BlindedNode]](_.toSeq, _.toList))).as[BlindedRoute].as[ReplyPath] + + private val encTlvCodec: Codec[EncTlv] = bytes.as[EncTlv] + + private val messageTlvCodec = discriminated[OnionMessagePayloadTlv].by(varint) + .typecase(UInt64(2), replyPathCodec) + .typecase(UInt64(10), encTlvCodec) + + val messagePerHopPayloadCodec: Codec[TlvStream[OnionMessagePayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionMessagePayloadTlv](messageTlvCodec).complete + + val messageRelayPayloadCodec: Codec[MessageRelayPayload] = messagePerHopPayloadCodec.narrow({ + case tlvs if tlvs.get[EncTlv].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(10))) + case tlvs if tlvs.get[ReplyPath].nonEmpty => Attempt.failure(ForbiddenTlv(UInt64(2))) + case tlvs => Attempt.successful(MessageRelayPayload(tlvs)) + }, { + case MessageRelayPayload(tlvs) => tlvs + }) + + val messageFinalPayloadCodec: Codec[MessageFinalPayload] = messagePerHopPayloadCodec.narrow({ + case tlvs if tlvs.get[EncTlv].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(10))) + case tlvs => Attempt.successful(MessageFinalPayload(tlvs)) + }, { + case MessageFinalPayload(tlvs) => tlvs + }) + def perHopPayloadCodecByPacketType[T <: PacketType](packetType: Sphinx.OnionRoutingPacket[T], isLastPacket: Boolean): Codec[PacketType] = packetType match { case Sphinx.PaymentPacket => if (isLastPacket) finalPerHopPayloadCodec.upcast[PacketType] else channelRelayPerHopPayloadCodec.upcast[PacketType] case Sphinx.TrampolinePacket => if (isLastPacket) finalPerHopPayloadCodec.upcast[PacketType] else nodeRelayPerHopPayloadCodec.upcast[PacketType] + case Sphinx.MessagePacket(payloadLength) => if (isLastPacket) messageFinalPayloadCodec.upcast[PacketType] else messageRelayPayloadCodec.upcast[PacketType] } -} \ No newline at end of file + private val padding: Codec[Padding] = variableSizeBytesLong(varintoverflow, "padding" | bytes).as[Padding] + + private val nextNodeId: Codec[NextNodeId] = variableSizeBytesLong(varintoverflow, "node_id" | publicKey).as[NextNodeId] + + private val blindingKey: Codec[BlindingPoint] = variableSizeBytesLong(varintoverflow, "blinding" | publicKey).as[BlindingPoint] + + private val pathId: Codec[PathId] = variableSizeBytesLong(varintoverflow, "path_id" | bytes).as[PathId] + + private val blindedTlvCodec: Codec[TlvStream[BlindedTlv]] = TlvCodecs.tlvStream[BlindedTlv]( + discriminated[BlindedTlv].by(varint) + .typecase(UInt64(1), padding) + .typecase(UInt64(4), nextNodeId) + .typecase(UInt64(12), blindingKey) + .typecase(UInt64(14), pathId)).complete + + val relayBlindedTlvCodec: Codec[RelayBlindedTlv] = blindedTlvCodec.narrow({ + case tlvs if tlvs.get[NextNodeId].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4))) + case tlvs if tlvs.get[PathId].nonEmpty => Attempt.failure(ForbiddenTlv(UInt64(14))) + case tlvs => Attempt.successful(RelayBlindedTlv(tlvs)) + }, { + case RelayBlindedTlv(tlvs) => tlvs + }) + + val finalBlindedTlvCodec: Codec[FinalBlindedTlv] = blindedTlvCodec.narrow( + tlvs => Attempt.successful(FinalBlindedTlv(tlvs)) + , { + case FinalBlindedTlv(tlvs) => tlvs + }) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessageTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessageTlv.scala new file mode 100644 index 0000000000..4e8ab13185 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessageTlv.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2021 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.eclair.wire.protocol.CommonCodecs.varint +import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream +import scodec.Codec +import scodec.codecs.discriminated + +/** + * Created by thomash on 10/09/2021. + */ + +sealed trait OnionMessageTlv extends Tlv + +object OnionMessageTlv { + val onionMessageTlvCodec: Codec[TlvStream[OnionMessageTlv]] = tlvStream(discriminated[OnionMessageTlv].by(varint)) +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessages.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessages.scala new file mode 100644 index 0000000000..1bbd368c4c --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessages.scala @@ -0,0 +1,115 @@ +/* + * Copyright 2021 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.wire.protocol.Onion.{MessageFinalPayload, MessageRelayPayload} +import fr.acinq.eclair.wire.protocol.OnionCodecs.messageRelayPayloadCodec +import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.EncTlv +import scodec.bits.ByteVector +import scodec.{Attempt, DecodeResult} + +import scala.util.{Failure, Success} + +object OnionMessages { + + case class IntermediateNode(nodeId: PublicKey, padding: Option[ByteVector] = None) + + case class Recipient(nodeId: PublicKey, pathId: Option[ByteVector], padding: Option[ByteVector] = None) + + def buildRoute(blindingSecret: PrivateKey, intermediateNodes: Seq[IntermediateNode], destination: Either[Recipient, Sphinx.RouteBlinding.BlindedRoute]): Sphinx.RouteBlinding.BlindedRoute = { + val last = destination match { + case Left(Recipient(nodeId, _, _)) => OnionPaymentPayloadTlv.NextNodeId(nodeId) :: Nil + case Right(Sphinx.RouteBlinding.BlindedRoute(nodeId, blindingKey, _)) => OnionPaymentPayloadTlv.NextNodeId(nodeId) :: OnionPaymentPayloadTlv.BlindingPoint(blindingKey) :: Nil + } + val relayInstructions = intermediateNodes.tail.map(node => OnionPaymentPayloadTlv.NextNodeId(node.nodeId) :: Nil) :+ last + val intermediatePayloads = relayInstructions + .zip(intermediateNodes).map { case (tlvs, hop) => hop.padding.map(OnionPaymentPayloadTlv.Padding(_) :: Nil).getOrElse(Nil) ++ tlvs } + .map(tlvs => Onion.RelayBlindedTlv(TlvStream(tlvs))) + .map(OnionCodecs.relayBlindedTlvCodec.encode(_).require.bytes) + destination match { + case Left(Recipient(nodeId, pathId, padding)) => + val tlvs = padding.map(OnionPaymentPayloadTlv.Padding(_) :: Nil).getOrElse(Nil) ++ pathId.map(OnionPaymentPayloadTlv.PathId(_) :: Nil).getOrElse(Nil) + val lastPayload = OnionCodecs.finalBlindedTlvCodec.encode(Onion.FinalBlindedTlv(TlvStream(tlvs))).require.bytes + Sphinx.RouteBlinding.create(blindingSecret, intermediateNodes.map(_.nodeId) :+ nodeId, intermediatePayloads :+ lastPayload) + case Right(route) => + val Sphinx.RouteBlinding.BlindedRoute(introductionNodeId, blindingKey, blindedNodes) = Sphinx.RouteBlinding.create(blindingSecret, intermediateNodes.map(_.nodeId), intermediatePayloads) + Sphinx.RouteBlinding.BlindedRoute(introductionNodeId, blindingKey, blindedNodes ++ route.blindedNodes) + } + } + + def buildMessage(sessionKey: PrivateKey, blindingSecret: PrivateKey, intermediateNodes: Seq[IntermediateNode], destination: Either[Recipient, Sphinx.RouteBlinding.BlindedRoute], content: List[OnionMessagePayloadTlv]): OnionMessage = { + val route = buildRoute(blindingSecret, intermediateNodes, destination) + val lastPayload = messageRelayPayloadCodec.encode(MessageRelayPayload(TlvStream(EncTlv(route.encryptedPayloads.last) :: content))).require.bytes + val payloads = route.encryptedPayloads.dropRight(1).map(encTlv => messageRelayPayloadCodec.encode(MessageRelayPayload(TlvStream(EncTlv(encTlv)))).require.bytes) :+ lastPayload + val payloadSize = payloads.map(_.length + Sphinx.MacLength).sum + val packetSize = if (payloadSize <= 1300) { + 1300 + } else if (payloadSize <= 32768) { + 32768 + } else { + payloadSize.toInt + } + val Sphinx.PacketAndSecrets(packet, _) = Sphinx.MessagePacket(packetSize).create(sessionKey, route.blindedNodes.map(_.blindedPublicKey), payloads, ByteVector.empty) + OnionMessage(blindingSecret.publicKey, packet) + } + + sealed trait Action + + case class DropMessage(reason: String) extends Action + + case class RelayMessage(nextNodeId: PublicKey, dataToRelay: OnionMessage) extends Action + + case class ReceiveMessage(finalPayload: MessageFinalPayload, pathId: Option[ByteVector]) extends Action + + def process(privateKey: PrivateKey, msg: OnionMessage): Action = { + if (msg.onionRoutingPacket.payload.length > 32768) { + DropMessage("Message too large") + } else { + val packetType = Sphinx.MessagePacket(msg.onionRoutingPacket.payload.length.toInt) + val blindedPrivateKey = Sphinx.RouteBlinding.derivePrivateKey(privateKey, msg.blindingKey) + packetType.peel(blindedPrivateKey, ByteVector.empty, msg.onionRoutingPacket) match { + case Left(_: BadOnion) => DropMessage("Can't decrypt onion") + case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => (OnionCodecs.perHopPayloadCodecByPacketType(packetType, p.isLastPacket).decode(payload.bits): @unchecked) match { + case Attempt.Successful(DecodeResult(relayPayload: MessageRelayPayload, _)) => + Sphinx.RouteBlinding.decryptPayload(privateKey, msg.blindingKey, relayPayload.blindedTlv) match { + case Success((decrypted, nextBlindingKey)) => + OnionCodecs.relayBlindedTlvCodec.decode(decrypted.bits) match { + case Attempt.Successful(DecodeResult(relayNext, _)) => + val toRelay = OnionMessage(relayNext.nextBlinding.getOrElse(nextBlindingKey), nextPacket) + RelayMessage(relayNext.nextNodeId, toRelay) + case Attempt.Failure(_) => DropMessage("Can't decode blinded TLV") + } + case Failure(_) => DropMessage("Can't decrypt blinded TLV") + } + case Attempt.Successful(DecodeResult(finalPayload: MessageFinalPayload, _)) => + Sphinx.RouteBlinding.decryptPayload(privateKey, msg.blindingKey, finalPayload.blindedTlv) match { + case Success((decrypted, _)) => + OnionCodecs.finalBlindedTlvCodec.decode(decrypted.bits) match { + case Attempt.Successful(DecodeResult(messageToSelf, _)) => + ReceiveMessage(finalPayload, messageToSelf.pathId) + case Attempt.Failure(_) => DropMessage("Can't decode blinded TLV") + } + case Failure(_) => DropMessage("Can't decrypt blinded TLV") + } + case Attempt.Failure(_) => DropMessage("Can't decode onion") + } + } + } + } +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index daf8ab41db..9b0fb22fa9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -32,6 +32,7 @@ import org.scalatest.Tag import scodec.bits.ByteVector import java.util.UUID +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong import scala.concurrent.duration._ @@ -197,7 +198,8 @@ object TestConstants { enableTrampolinePayment = true, instanceId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), balanceCheckInterval = 1 hour, - blockchainWatchdogSources = blockchainWatchdogSources + blockchainWatchdogSources = blockchainWatchdogSources, + onionMessageRateLimitPerSecond = 10 ) def channelParams: LocalParams = Peer.makeChannelParams( @@ -323,7 +325,8 @@ object TestConstants { enableTrampolinePayment = true, instanceId = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), balanceCheckInterval = 1 hour, - blockchainWatchdogSources = blockchainWatchdogSources + blockchainWatchdogSources = blockchainWatchdogSources, + onionMessageRateLimitPerSecond = 10 ) def channelParams: LocalParams = Peer.makeChannelParams( 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 01776389b1..25da7531d0 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 @@ -378,14 +378,14 @@ class SphinxSpec extends AnyFunSuite { PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), )) - assert(blindedRoute.blindedNodes.map(_.blindedPublicKey) === Seq( + assert(blindedRoute.subsequentNodes.map(_.blindedPublicKey) === Seq( PublicKey(hex"022b09d77fb3374ee3ed9d2153e15e9962944ad1690327cbb0a9acb7d90f168763"), PublicKey(hex"03d9f889364dc5a173460a2a6cc565b4ca78931792115dd6ef82c0e18ced837372"), PublicKey(hex"03bfddd2253b42fe12edd37f9071a3883830ed61a4bc347eeac63421629cf032b5"), PublicKey(hex"03a8588bc4a0a2f0d2fb8d5c0f8d062fb4d78bfba24a85d0ddeb4fd35dd3b34110"), )) - assert(blindedRoute.encryptedPayloads === blindedRoute.introductionNode.encryptedPayload +: blindedRoute.blindedNodes.map(_.encryptedPayload)) - assert(blindedRoute.blindedNodes.map(_.encryptedPayload) === Seq( + assert(blindedRoute.encryptedPayloads === blindedRoute.introductionNode.encryptedPayload +: blindedRoute.subsequentNodes.map(_.encryptedPayload)) + assert(blindedRoute.subsequentNodes.map(_.encryptedPayload) === Seq( hex"146c9694ead7de2a54fc43e8bb927bfc377dda7ed5a2e36b327b739e368aa602e43e07e14b3d7ed493e7ea6245924d9a03d22f0fca56babd7da19f49b7", hex"8ad7d5d448f15208417a1840f82274101b3c254c24b1b49fd676fd0c4293c9aa66ed51da52579e934a869f016f213044d1b13b63bf586e9c9832106b59", hex"52a45a884542d180e76fe84fc13e71a01f65d943ff89aed29b94644a91b037b9143cfda8f1ff25ba61c37108a5ae57d9ddc5ab688ee8b2f9f6bd94522c", @@ -448,13 +448,13 @@ class SphinxSpec extends AnyFunSuite { )) val payloads = Seq( // The sender sends normal onion payloads to the first two hops. - TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(500)), OnionTlv.OutgoingCltv(CltvExpiry(1000)), OnionTlv.OutgoingChannelId(ShortChannelId(10))), - TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(450)), OnionTlv.OutgoingCltv(CltvExpiry(900)), OnionTlv.OutgoingChannelId(ShortChannelId(15))), + TlvStream[OnionPaymentPayloadTlv](OnionPaymentPayloadTlv.AmountToForward(MilliSatoshi(500)), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(1000)), OnionPaymentPayloadTlv.OutgoingChannelId(ShortChannelId(10))), + TlvStream[OnionPaymentPayloadTlv](OnionPaymentPayloadTlv.AmountToForward(MilliSatoshi(450)), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(900)), OnionPaymentPayloadTlv.OutgoingChannelId(ShortChannelId(15))), // The sender includes the blinding key and the first encrypted recipient data in the introduction node's payload. - TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(400)), OnionTlv.OutgoingCltv(CltvExpiry(860)), OnionTlv.BlindingPoint(blindingEphemeralKey0), OnionTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(0))), + TlvStream[OnionPaymentPayloadTlv](OnionPaymentPayloadTlv.AmountToForward(MilliSatoshi(400)), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(860)), OnionPaymentPayloadTlv.BlindingPoint(blindingEphemeralKey0), OnionPaymentPayloadTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(0))), // The sender includes the correct encrypted recipient data in each blinded node's payload. - TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(250)), OnionTlv.OutgoingCltv(CltvExpiry(750)), OnionTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(1))), - TlvStream[OnionTlv](OnionTlv.AmountToForward(MilliSatoshi(250)), OnionTlv.OutgoingCltv(CltvExpiry(750)), OnionTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(2))), + TlvStream[OnionPaymentPayloadTlv](OnionPaymentPayloadTlv.AmountToForward(MilliSatoshi(250)), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(750)), OnionPaymentPayloadTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(1))), + TlvStream[OnionPaymentPayloadTlv](OnionPaymentPayloadTlv.AmountToForward(MilliSatoshi(250)), OnionPaymentPayloadTlv.OutgoingCltv(CltvExpiry(750)), OnionPaymentPayloadTlv.EncryptedRecipientData(blindedRoute.encryptedPayloads(2))), ).map(tlvs => OnionCodecs.tlvPerHopPayloadCodec.encode(tlvs).require.bytes) val senderSessionKey = PrivateKey(hex"0202020202020202020202020202020202020202020202020202020202020202") @@ -470,9 +470,9 @@ class SphinxSpec extends AnyFunSuite { // However it contains a blinding point and encrypted data, which it can decrypt to discover the next node. val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, nextPacket1) val tlvs2 = OnionCodecs.tlvPerHopPayloadCodec.decode(payload2.bits).require.value - assert(tlvs2.get[OnionTlv.BlindingPoint].map(_.publicKey) === Some(blindingEphemeralKey0)) - assert(tlvs2.get[OnionTlv.EncryptedRecipientData].nonEmpty) - val Success((recipientTlvs2, blindingEphemeralKey1)) = EncryptedRecipientDataCodecs.decode(privKeys(2), blindingEphemeralKey0, tlvs2.get[OnionTlv.EncryptedRecipientData].get.data) + assert(tlvs2.get[OnionPaymentPayloadTlv.BlindingPoint].map(_.publicKey) === Some(blindingEphemeralKey0)) + assert(tlvs2.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) + val Success((recipientTlvs2, blindingEphemeralKey1)) = EncryptedRecipientDataCodecs.decode(privKeys(2), blindingEphemeralKey0, tlvs2.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) assert(recipientTlvs2.get[EncryptedRecipientDataTlv.OutgoingChannelId].map(_.shortChannelId) === Some(ShortChannelId(1105))) assert(recipientTlvs2.get[EncryptedRecipientDataTlv.OutgoingNodeId].map(_.nodeId) === Some(publicKeys(3))) @@ -483,8 +483,8 @@ class SphinxSpec extends AnyFunSuite { val blindedPrivKey3 = RouteBlinding.derivePrivateKey(privKeys(3), blindingEphemeralKey1) val Right(DecryptedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.peel(blindedPrivKey3, associatedData, nextPacket2) val tlvs3 = OnionCodecs.tlvPerHopPayloadCodec.decode(payload3.bits).require.value - assert(tlvs3.get[OnionTlv.EncryptedRecipientData].nonEmpty) - val Success((recipientTlvs3, blindingEphemeralKey2)) = EncryptedRecipientDataCodecs.decode(privKeys(3), blindingEphemeralKey1, tlvs3.get[OnionTlv.EncryptedRecipientData].get.data) + assert(tlvs3.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) + val Success((recipientTlvs3, blindingEphemeralKey2)) = EncryptedRecipientDataCodecs.decode(privKeys(3), blindingEphemeralKey1, tlvs3.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) assert(recipientTlvs3.get[EncryptedRecipientDataTlv.OutgoingNodeId].map(_.nodeId) === Some(publicKeys(4))) // The fifth hop is the blinded recipient. @@ -493,8 +493,8 @@ class SphinxSpec extends AnyFunSuite { val blindedPrivKey4 = RouteBlinding.derivePrivateKey(privKeys(4), blindingEphemeralKey2) val Right(DecryptedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.peel(blindedPrivKey4, associatedData, nextPacket3) val tlvs4 = OnionCodecs.tlvPerHopPayloadCodec.decode(payload4.bits).require.value - assert(tlvs4.get[OnionTlv.EncryptedRecipientData].nonEmpty) - val Success((recipientTlvs4, _)) = EncryptedRecipientDataCodecs.decode(privKeys(4), blindingEphemeralKey2, tlvs4.get[OnionTlv.EncryptedRecipientData].get.data) + assert(tlvs4.get[OnionPaymentPayloadTlv.EncryptedRecipientData].nonEmpty) + val Success((recipientTlvs4, _)) = EncryptedRecipientDataCodecs.decode(privKeys(4), blindingEphemeralKey2, tlvs4.get[OnionPaymentPayloadTlv.EncryptedRecipientData].get.data) assert(recipientTlvs4.get[EncryptedRecipientDataTlv.RecipientSecret].map(_.data) === Some(associatedData.bytes)) assert(Seq(payload0, payload1, payload2, payload3, payload4) == payloads) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala index 9c0e6b083d..3bb83ed15a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartHandlerSpec.scala @@ -483,7 +483,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentPreimage = randomBytes32() val paymentHash = Crypto.sha256(paymentPreimage) val paymentSecret = randomBytes32() - val payload = FinalTlvPayload(TlvStream(Seq(OnionTlv.AmountToForward(amountMsat), OnionTlv.OutgoingCltv(defaultExpiry), OnionTlv.PaymentData(paymentSecret, 0 msat), OnionTlv.KeySend(paymentPreimage)))) + val payload = FinalTlvPayload(TlvStream(Seq(OnionPaymentPayloadTlv.AmountToForward(amountMsat), OnionPaymentPayloadTlv.OutgoingCltv(defaultExpiry), OnionPaymentPayloadTlv.PaymentData(paymentSecret, 0 msat), OnionPaymentPayloadTlv.KeySend(paymentPreimage)))) assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) @@ -505,7 +505,7 @@ class MultiPartHandlerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike val paymentPreimage = randomBytes32() val paymentHash = Crypto.sha256(paymentPreimage) val paymentSecret = randomBytes32() - val payload = FinalTlvPayload(TlvStream(Seq(OnionTlv.AmountToForward(amountMsat), OnionTlv.OutgoingCltv(defaultExpiry), OnionTlv.PaymentData(paymentSecret, 0 msat), OnionTlv.KeySend(paymentPreimage)))) + val payload = FinalTlvPayload(TlvStream(Seq(OnionPaymentPayloadTlv.AmountToForward(amountMsat), OnionPaymentPayloadTlv.OutgoingCltv(defaultExpiry), OnionPaymentPayloadTlv.PaymentData(paymentSecret, 0 msat), OnionPaymentPayloadTlv.KeySend(paymentPreimage)))) assert(nodeParams.db.payments.getIncomingPayment(paymentHash) === None) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index e2b12fc7c4..724e45a115 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -147,7 +147,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS import f._ // We include a bunch of additional tlv records. - val trampolineTlv = OnionTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32())) + val trampolineTlv = OnionPaymentPayloadTlv.TrampolineOnion(OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(400)(0), randomBytes32())) val userCustomTlv = GenericTlv(UInt64(561), hex"deadbeef") val payment = SendMultiPartPayment(sender.ref, randomBytes32(), e, finalAmount + 1000.msat, expiry, 1, routeParams = routeParams, additionalTlvs = Seq(trampolineTlv), userCustomTlvs = Seq(userCustomTlv)) sender.send(payFsm, payment) @@ -155,7 +155,7 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS router.send(payFsm, RouteResponse(Seq(Route(500000 msat, hop_ab_1 :: hop_be :: Nil), Route(501000 msat, hop_ac_1 :: hop_ce :: Nil)))) val childPayments = childPayFsm.expectMsgType[SendPaymentToRoute] :: childPayFsm.expectMsgType[SendPaymentToRoute] :: Nil childPayments.map(_.finalPayload.asInstanceOf[Onion.FinalTlvPayload]).foreach(p => { - assert(p.records.get[OnionTlv.TrampolineOnion] === Some(trampolineTlv)) + assert(p.records.get[OnionPaymentPayloadTlv.TrampolineOnion] === Some(trampolineTlv)) assert(p.records.unknown.toSeq === Seq(userCustomTlv)) }) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala index a43e5b34e6..278e09c427 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala @@ -33,7 +33,7 @@ import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator, PaymentLife import fr.acinq.eclair.router.RouteNotFound import fr.acinq.eclair.router.Router._ import fr.acinq.eclair.wire.protocol.Onion.FinalTlvPayload -import fr.acinq.eclair.wire.protocol.OnionTlv.{AmountToForward, KeySend, OutgoingCltv} +import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, KeySend, OutgoingCltv} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiryDelta, Features, MilliSatoshiLong, NodeParams, TestConstants, TestKitBaseClass, randomBytes32, randomKey} import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -146,7 +146,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike sender.send(initiator, req) val id = sender.expectMsgType[UUID] payFsm.expectMsg(SendPaymentConfig(id, id, None, paymentHash, finalAmount, c, Upstream.Local(id), Some(pr), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)) - payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(sender.ref, c, FinalTlvPayload(TlvStream(OnionTlv.AmountToForward(finalAmount), OnionTlv.OutgoingCltv(req.finalExpiry(nodeParams.currentBlockHeight)), OnionTlv.PaymentData(pr.paymentSecret.get, finalAmount))), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) + payFsm.expectMsg(PaymentLifecycle.SendPaymentToNode(sender.ref, c, FinalTlvPayload(TlvStream(OnionPaymentPayloadTlv.AmountToForward(finalAmount), OnionPaymentPayloadTlv.OutgoingCltv(req.finalExpiry(nodeParams.currentBlockHeight)), OnionPaymentPayloadTlv.PaymentData(pr.paymentSecret.get, finalAmount))), 1, routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams)) } test("forward multi-part payment") { f => @@ -191,11 +191,11 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(msg.targetNodeId === b) assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1) assert(msg.totalAmount === finalAmount + trampolineFees) - assert(msg.additionalTlvs.head.isInstanceOf[OnionTlv.TrampolineOnion]) + assert(msg.additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) assert(msg.maxAttempts === nodeParams.maxPaymentAttempts) // Verify that the trampoline node can correctly peel the trampoline onion. - val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionTlv.TrampolineOnion].packet + val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet val Right(decrypted) = Sphinx.TrampolinePacket.peel(priv_b.privateKey, pr.paymentHash, trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = OnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value @@ -231,10 +231,10 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(msg.targetNodeId === b) assert(msg.targetExpiry.toLong === currentBlockCount + 9 + 12 + 1) assert(msg.totalAmount === finalAmount + trampolineFees) - assert(msg.additionalTlvs.head.isInstanceOf[OnionTlv.TrampolineOnion]) + assert(msg.additionalTlvs.head.isInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion]) // Verify that the trampoline node can correctly peel the trampoline onion. - val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionTlv.TrampolineOnion].packet + val trampolineOnion = msg.additionalTlvs.head.asInstanceOf[OnionPaymentPayloadTlv.TrampolineOnion].packet val Right(decrypted) = Sphinx.TrampolinePacket.peel(priv_b.privateKey, pr.paymentHash, trampolineOnion) assert(!decrypted.isLastPacket) val trampolinePayload = OnionCodecs.nodeRelayPerHopPayloadCodec.decode(decrypted.payload.bits).require.value @@ -368,7 +368,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike assert(msg.finalPayload.paymentSecret === payment.trampolineSecret.get) assert(msg.finalPayload.totalAmount === finalAmount + trampolineFees) assert(msg.finalPayload.isInstanceOf[Onion.FinalTlvPayload]) - val trampolineOnion = msg.finalPayload.asInstanceOf[Onion.FinalTlvPayload].records.get[OnionTlv.TrampolineOnion] + val trampolineOnion = msg.finalPayload.asInstanceOf[Onion.FinalTlvPayload].records.get[OnionPaymentPayloadTlv.TrampolineOnion] assert(trampolineOnion.nonEmpty) // Verify that the trampoline node can correctly peel the trampoline onion. diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index e9148931d4..17c7878128 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -29,7 +29,7 @@ import fr.acinq.eclair.payment.PaymentRequest.PaymentRequestFeatures import fr.acinq.eclair.router.Router.{ChannelHop, NodeHop} import fr.acinq.eclair.transactions.Transactions.InputInfo import fr.acinq.eclair.wire.protocol.Onion.{ChannelRelayTlvPayload, FinalTlvPayload} -import fr.acinq.eclair.wire.protocol.OnionTlv.{AmountToForward, OutgoingCltv, PaymentData} +import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.{AmountToForward, OutgoingCltv, PaymentData} import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TestConstants, TimestampSecond, TimestampSecondLong, nodeFee, randomBytes32, randomKey} import org.scalatest.BeforeAndAfterAll @@ -352,7 +352,7 @@ class PaymentPacketSpec extends AnyFunSuite with BeforeAndAfterAll { object PaymentPacketSpec { /** Build onion from arbitrary tlv stream (potentially invalid). */ - def buildTlvOnion[T <: Onion.PacketType](packetType: Sphinx.OnionRoutingPacket[T])(nodes: Seq[PublicKey], payloads: Seq[TlvStream[OnionTlv]], associatedData: ByteVector32): OnionRoutingPacket = { + def buildTlvOnion[T <: Onion.PacketType](packetType: Sphinx.OnionRoutingPacket[T])(nodes: Seq[PublicKey], payloads: Seq[TlvStream[OnionPaymentPayloadTlv]], associatedData: ByteVector32): OnionRoutingPacket = { require(nodes.size == payloads.size) val sessionKey = randomKey() val payloadsBin: Seq[ByteVector] = payloads.map(OnionCodecs.tlvPerHopPayloadCodec.encode) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala index 25ea87e986..40235d4ca0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/ChannelRelayerSpec.scala @@ -88,9 +88,9 @@ class ChannelRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("a test("relay an htlc-add with onion tlv payload") { f => import f._ - import fr.acinq.eclair.wire.protocol.OnionTlv._ + import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ - val payload = ChannelRelayTlvPayload(TlvStream[OnionTlv](AmountToForward(outgoingAmount), OutgoingCltv(outgoingExpiry), OutgoingChannelId(shortId1))) + val payload = ChannelRelayTlvPayload(TlvStream[OnionPaymentPayloadTlv](AmountToForward(outgoingAmount), OutgoingCltv(outgoingExpiry), OutgoingChannelId(shortId1))) val r = createValidIncomingPacket(1100000 msat, CltvExpiry(400100), payload) val u = createLocalUpdate(shortId1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala index da5dea57e9..71a0a46d03 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/NodeRelayerSpec.scala @@ -648,7 +648,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(outgoingAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18)) val incomingPayments = incomingMultiPart.map(incoming => { val innerPayload = Onion.createNodeRelayToNonTrampolinePayload(incoming.innerPayload.amountToForward, incoming.innerPayload.amountToForward, outgoingExpiry, outgoingNodeId, pr) - val invalidPayload = innerPayload.copy(records = TlvStream(innerPayload.records.records.collect { case r if !r.isInstanceOf[OnionTlv.PaymentData] => r })) // we remove the payment secret + val invalidPayload = innerPayload.copy(records = TlvStream(innerPayload.records.records.collect { case r if !r.isInstanceOf[OnionPaymentPayloadTlv.PaymentData] => r })) // we remove the payment secret incoming.copy(innerPayload = invalidPayload) }) val (nodeRelayer, _) = f.createNodeRelay(incomingPayments.head) @@ -676,7 +676,7 @@ class NodeRelayerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("appl assert(outgoingPayment.totalAmount === outgoingAmount) assert(outgoingPayment.targetExpiry === outgoingExpiry) assert(outgoingPayment.targetNodeId === outgoingNodeId) - assert(outgoingPayment.additionalTlvs === Seq(OnionTlv.TrampolineOnion(nextTrampolinePacket))) + assert(outgoingPayment.additionalTlvs === Seq(OnionPaymentPayloadTlv.TrampolineOnion(nextTrampolinePacket))) assert(outgoingPayment.assistedRoutes === Nil) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala index c9f9498659..80d14390d5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/EncryptedRecipientDataSpec.scala @@ -23,7 +23,7 @@ class EncryptedRecipientDataSpec extends AnyFunSuiteLike { val blindedRoute = RouteBlinding.create(sessionKey, nodePrivKeys.map(_.publicKey), payloads.map(_._2)) val blinding0 = sessionKey.publicKey - val Success((decryptedPayload0, blinding1)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys.head, blinding0, blindedRoute.encryptedPayloads(0)) + val Success((decryptedPayload0, blinding1)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(0), blinding0, blindedRoute.encryptedPayloads(0)) val Success((decryptedPayload1, blinding2)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(1), blinding1, blindedRoute.encryptedPayloads(1)) val Success((decryptedPayload2, blinding3)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(2), blinding2, blindedRoute.encryptedPayloads(2)) val Success((decryptedPayload3, _)) = EncryptedRecipientDataCodecs.decode(nodePrivKeys(3), blinding3, blindedRoute.encryptedPayloads(3)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionCodecsSpec.scala index 7674321587..2d45e9cce0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionCodecsSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.wire.protocol.Onion._ import fr.acinq.eclair.wire.protocol.OnionCodecs._ -import fr.acinq.eclair.wire.protocol.OnionTlv._ +import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv._ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, ShortChannelId, UInt64} import org.scalatest.funsuite.AnyFunSuite import scodec.Attempt @@ -80,9 +80,9 @@ class OnionCodecsSpec extends AnyFunSuite { test("encode/decode variable-length (tlv) relay per-hop payload") { val testCases = Map( - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))) -> hex"11 02020231 04012a 06080000000000000451", - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105)), EncryptedRecipientData(hex"0123456789abcdef"), BlindingPoint(PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2"))) -> hex"3e 02020231 04012a 06080000000000000451 0a080123456789abcdef 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2", - TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))), Seq(GenericTlv(65535, hex"06c1"))) -> hex"17 02020231 04012a 06080000000000000451 fdffff0206c1", + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))) -> hex"11 02020231 04012a 06080000000000000451", + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105)), EncryptedRecipientData(hex"0123456789abcdef"), BlindingPoint(PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2"))) -> hex"3e 02020231 04012a 06080000000000000451 0a080123456789abcdef 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2", + TlvStream[OnionPaymentPayloadTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105))), Seq(GenericTlv(65535, hex"06c1"))) -> hex"17 02020231 04012a 06080000000000000451 fdffff0206c1", ) for ((expected, bin) <- testCases) { @@ -99,7 +99,7 @@ class OnionCodecsSpec extends AnyFunSuite { test("encode/decode variable-length (tlv) node relay per-hop payload") { val nodeId = PublicKey(hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619") - val expected = TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingNodeId(nodeId)) + val expected = TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingNodeId(nodeId)) val bin = hex"2e 02020231 04012a fe000102322102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" val decoded = nodeRelayPerHopPayloadCodec.decode(bin.bits).require.value @@ -124,7 +124,7 @@ class OnionCodecsSpec extends AnyFunSuite { List(ExtraHop(node1, ShortChannelId(1), 10 msat, 100, CltvExpiryDelta(144))), List(ExtraHop(node2, ShortChannelId(2), 20 msat, 150, CltvExpiryDelta(12)), ExtraHop(node3, ShortChannelId(3), 30 msat, 200, CltvExpiryDelta(24))) ) - val expected = TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105 msat), InvoiceFeatures(features), OutgoingNodeId(nodeId), InvoiceRoutingInfo(routingHints)) + val expected = TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105 msat), InvoiceFeatures(features), OutgoingNodeId(nodeId), InvoiceRoutingInfo(routingHints)) val bin = hex"fa 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451 fe00010231010a fe000102322102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fe000102339b01036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e200000000000000010000000a00000064009002025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce148600000000000000020000001400000096000c02a051267759c3a149e3e72372f4e0c4054ba597ebfd0eda78a2273023667205ee00000000000000030000001e000000c80018" val decoded = nodeRelayPerHopPayloadCodec.decode(bin.bits).require.value @@ -143,15 +143,15 @@ class OnionCodecsSpec extends AnyFunSuite { test("encode/decode variable-length (tlv) final per-hop payload") { val testCases = Map( - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)) -> hex"29 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105 msat)) -> hex"2b 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451", - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105 msat), EncryptedRecipientData(hex"00aa11"), BlindingPoint(PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2"))) -> hex"53 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451 0a0300aa11 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2", - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 4294967295L msat)) -> hex"2d 02020231 04012a 0824eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619ffffffff", - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 4294967296L msat)) -> hex"2e 02020231 04012a 0825eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190100000000", - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1099511627775L msat)) -> hex"2e 02020231 04012a 0825eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619ffffffffff", - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)) -> hex"33 02020231 04012a 06080000000000000451 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", - TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)), Seq(GenericTlv(65535, hex"06c1"))) -> hex"2f 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fdffff0206c1", - TlvStream[OnionTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat), TrampolineOnion(OnionRoutingPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"cff34152f3a36e52ca94e74927203a560392b9cc7ce3c45809c6be52166c24a595716880f95f178bf5b30ca63141f74db6e92795c6130877cfdac3d4bd3087ee73c65d627ddd709112a848cc99e303f3706509aa43ba7c8a88cba175fccf9a8f5016ef06d3b935dbb15196d7ce16dc1a7157845566901d7b2197e52cab4ce487014b14816e5805f9fcacb4f8f88b8ff176f1b94f6ce6b00bc43221130c17d20ef629db7c5f7eafaa166578c720619561dd14b3277db557ec7dcdb793771aef0f2f667cfdbeae3ac8d331c5994779dffb31e5fc0dbdedc0c592ca6d21c18e47fe3528d6975c19517d7e2ea8c5391cf17d0fe30c80913ed887234ccb48808f7ef9425bcd815c3b586210979e3bb286ef2851bf9ce04e28c40a203df98fd648d2f1936fd2f1def0e77eecb277229b4b682322371c0a1dbfcd723a991993df8cc1f2696b84b055b40a1792a29f710295a18fbd351b0f3ff34cd13941131b8278ba79303c89117120eea691738a9954908195143b039dbeed98f26a92585f3d15cf742c953799d3272e0545e9b744be9d3b4c", ByteVector32(hex"bb079bfc4b35190eee9f59a1d7b41ba2f773179f322dafb4b1af900c289ebd6c")))) -> hex"fd0203 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fe00010234fd01d20002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619cff34152f3a36e52ca94e74927203a560392b9cc7ce3c45809c6be52166c24a595716880f95f178bf5b30ca63141f74db6e92795c6130877cfdac3d4bd3087ee73c65d627ddd709112a848cc99e303f3706509aa43ba7c8a88cba175fccf9a8f5016ef06d3b935dbb15196d7ce16dc1a7157845566901d7b2197e52cab4ce487014b14816e5805f9fcacb4f8f88b8ff176f1b94f6ce6b00bc43221130c17d20ef629db7c5f7eafaa166578c720619561dd14b3277db557ec7dcdb793771aef0f2f667cfdbeae3ac8d331c5994779dffb31e5fc0dbdedc0c592ca6d21c18e47fe3528d6975c19517d7e2ea8c5391cf17d0fe30c80913ed887234ccb48808f7ef9425bcd815c3b586210979e3bb286ef2851bf9ce04e28c40a203df98fd648d2f1936fd2f1def0e77eecb277229b4b682322371c0a1dbfcd723a991993df8cc1f2696b84b055b40a1792a29f710295a18fbd351b0f3ff34cd13941131b8278ba79303c89117120eea691738a9954908195143b039dbeed98f26a92585f3d15cf742c953799d3272e0545e9b744be9d3b4cbb079bfc4b35190eee9f59a1d7b41ba2f773179f322dafb4b1af900c289ebd6c" + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)) -> hex"29 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105 msat)) -> hex"2b 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451", + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1105 msat), EncryptedRecipientData(hex"00aa11"), BlindingPoint(PublicKey(hex"036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2"))) -> hex"53 02020231 04012a 0822eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190451 0a0300aa11 0c21036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2", + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 4294967295L msat)) -> hex"2d 02020231 04012a 0824eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619ffffffff", + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 4294967296L msat)) -> hex"2e 02020231 04012a 0825eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866190100000000", + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 1099511627775L msat)) -> hex"2e 02020231 04012a 0825eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619ffffffffff", + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), OutgoingChannelId(ShortChannelId(1105)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)) -> hex"33 02020231 04012a 06080000000000000451 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + TlvStream[OnionPaymentPayloadTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)), Seq(GenericTlv(65535, hex"06c1"))) -> hex"2f 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fdffff0206c1", + TlvStream[OnionPaymentPayloadTlv](AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat), TrampolineOnion(OnionRoutingPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"cff34152f3a36e52ca94e74927203a560392b9cc7ce3c45809c6be52166c24a595716880f95f178bf5b30ca63141f74db6e92795c6130877cfdac3d4bd3087ee73c65d627ddd709112a848cc99e303f3706509aa43ba7c8a88cba175fccf9a8f5016ef06d3b935dbb15196d7ce16dc1a7157845566901d7b2197e52cab4ce487014b14816e5805f9fcacb4f8f88b8ff176f1b94f6ce6b00bc43221130c17d20ef629db7c5f7eafaa166578c720619561dd14b3277db557ec7dcdb793771aef0f2f667cfdbeae3ac8d331c5994779dffb31e5fc0dbdedc0c592ca6d21c18e47fe3528d6975c19517d7e2ea8c5391cf17d0fe30c80913ed887234ccb48808f7ef9425bcd815c3b586210979e3bb286ef2851bf9ce04e28c40a203df98fd648d2f1936fd2f1def0e77eecb277229b4b682322371c0a1dbfcd723a991993df8cc1f2696b84b055b40a1792a29f710295a18fbd351b0f3ff34cd13941131b8278ba79303c89117120eea691738a9954908195143b039dbeed98f26a92585f3d15cf742c953799d3272e0545e9b744be9d3b4c", ByteVector32(hex"bb079bfc4b35190eee9f59a1d7b41ba2f773179f322dafb4b1af900c289ebd6c")))) -> hex"fd0203 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 fe00010234fd01d20002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619cff34152f3a36e52ca94e74927203a560392b9cc7ce3c45809c6be52166c24a595716880f95f178bf5b30ca63141f74db6e92795c6130877cfdac3d4bd3087ee73c65d627ddd709112a848cc99e303f3706509aa43ba7c8a88cba175fccf9a8f5016ef06d3b935dbb15196d7ce16dc1a7157845566901d7b2197e52cab4ce487014b14816e5805f9fcacb4f8f88b8ff176f1b94f6ce6b00bc43221130c17d20ef629db7c5f7eafaa166578c720619561dd14b3277db557ec7dcdb793771aef0f2f667cfdbeae3ac8d331c5994779dffb31e5fc0dbdedc0c592ca6d21c18e47fe3528d6975c19517d7e2ea8c5391cf17d0fe30c80913ed887234ccb48808f7ef9425bcd815c3b586210979e3bb286ef2851bf9ce04e28c40a203df98fd648d2f1936fd2f1def0e77eecb277229b4b682322371c0a1dbfcd723a991993df8cc1f2696b84b055b40a1792a29f710295a18fbd351b0f3ff34cd13941131b8278ba79303c89117120eea691738a9954908195143b039dbeed98f26a92585f3d15cf742c953799d3272e0545e9b744be9d3b4cbb079bfc4b35190eee9f59a1d7b41ba2f773179f322dafb4b1af900c289ebd6c" ) for ((expected, bin) <- testCases) { @@ -166,7 +166,7 @@ class OnionCodecsSpec extends AnyFunSuite { } test("encode/decode variable-length (tlv) final per-hop payload with custom user records") { - val tlvs = TlvStream[OnionTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)), Seq(GenericTlv(5432123456L, hex"16c7ec71663784ff100b6eface1e60a97b92ea9d18b8ece5e558586bc7453828"))) + val tlvs = TlvStream[OnionPaymentPayloadTlv](Seq(AmountToForward(561 msat), OutgoingCltv(CltvExpiry(42)), PaymentData(ByteVector32(hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), 0 msat)), Seq(GenericTlv(5432123456L, hex"16c7ec71663784ff100b6eface1e60a97b92ea9d18b8ece5e558586bc7453828"))) val bin = hex"53 02020231 04012a 0820eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619 ff0000000143c7a0402016c7ec71663784ff100b6eface1e60a97b92ea9d18b8ece5e558586bc7453828" val encoded = finalPerHopPayloadCodec.encode(FinalTlvPayload(tlvs)).require.bytes diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionMessagesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionMessagesSpec.scala new file mode 100644 index 0000000000..287c4952ac --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/OnionMessagesSpec.scala @@ -0,0 +1,210 @@ +/* + * Copyright 2021 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.wire.protocol + +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.bitcoin.{ByteVector32, Crypto} +import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.crypto.Sphinx.{MessagePacket, PacketAndSecrets} +import fr.acinq.eclair.wire.protocol.Onion.MessageRelayPayload +import fr.acinq.eclair.wire.protocol.OnionCodecs.messageRelayPayloadCodec +import fr.acinq.eclair.wire.protocol.OnionMessages.{IntermediateNode, Recipient} +import fr.acinq.eclair.wire.protocol.OnionPaymentPayloadTlv.EncTlv +import org.scalatest.funsuite.AnyFunSuite +import scodec.bits.{ByteVector, HexStringSyntax} + +/** + * Created by thomash on 23/09/2021. + */ + +class OnionMessagesSpec extends AnyFunSuite { + + test("Spec tests") { + val alice = PrivateKey(hex"414141414141414141414141414141414141414141414141414141414141414101") + assert(alice.publicKey == PublicKey(hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) + val bob = PrivateKey(hex"424242424242424242424242424242424242424242424242424242424242424201") + assert(bob.publicKey == PublicKey(hex"0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c")) + val carol = PrivateKey(hex"434343434343434343434343434343434343434343434343434343434343434301") + assert(carol.publicKey == PublicKey(hex"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007")) + val dave = PrivateKey(hex"444444444444444444444444444444444444444444444444444444444444444401") + assert(dave.publicKey == PublicKey(hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991")) + + val blindingSecret = PrivateKey(hex"050505050505050505050505050505050505050505050505050505050505050501") + assert(blindingSecret.publicKey == PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7")) + val blindingOverride = PrivateKey(hex"070707070707070707070707070707070707070707070707070707070707070701") + assert(blindingOverride.publicKey == PublicKey(hex"02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f")) + + /* + * Building the onion manually + */ + val messageForAlice = Onion.RelayBlindedTlv(TlvStream(OnionPaymentPayloadTlv.NextNodeId(bob.publicKey))) + val encodedForAlice = OnionCodecs.relayBlindedTlvCodec.encode(messageForAlice).require.bytes + assert(encodedForAlice == hex"04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c") + val messageForBob = Onion.RelayBlindedTlv(TlvStream(OnionPaymentPayloadTlv.NextNodeId(carol.publicKey), OnionPaymentPayloadTlv.BlindingPoint(blindingOverride.publicKey))) + val encodedForBob = OnionCodecs.relayBlindedTlvCodec.encode(messageForBob).require.bytes + assert(encodedForBob == hex"0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa20070c2102989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f") + val messageForCarol = Onion.RelayBlindedTlv(TlvStream(OnionPaymentPayloadTlv.Padding(hex"0000000000000000000000000000000000000000000000000000000000000000000000"), OnionPaymentPayloadTlv.NextNodeId(dave.publicKey))) + val encodedForCarol = OnionCodecs.relayBlindedTlvCodec.encode(messageForCarol).require.bytes + assert(encodedForCarol == hex"012300000000000000000000000000000000000000000000000000000000000000000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991") + val messageForDave = Onion.FinalBlindedTlv(TlvStream(OnionPaymentPayloadTlv.PathId(hex"01234567"))) + val encodedForDave = OnionCodecs.finalBlindedTlvCodec.encode(messageForDave).require.bytes + assert(encodedForDave == hex"0e0401234567") + + // Building blinded path Carol -> Dave + val routeFromCarol = Sphinx.RouteBlinding.create(blindingOverride, carol.publicKey :: dave.publicKey :: Nil, encodedForCarol :: encodedForDave :: Nil) + + // Building blinded path Alice -> Bob + val routeToCarol = Sphinx.RouteBlinding.create(blindingSecret, alice.publicKey :: bob.publicKey :: Nil, encodedForAlice :: encodedForBob :: Nil) + + val publicKeys = routeToCarol.blindedNodes.map(_.blindedPublicKey) concat routeFromCarol.blindedNodes.map(_.blindedPublicKey) + val encryptedPayloads = routeToCarol.encryptedPayloads concat routeFromCarol.encryptedPayloads + val payloads = encryptedPayloads.map(encTlv => messageRelayPayloadCodec.encode(MessageRelayPayload(TlvStream(EncTlv(encTlv)))).require.bytes) + + val sessionKey = PrivateKey(hex"090909090909090909090909090909090909090909090909090909090909090901") + + val PacketAndSecrets(packet, _) = MessagePacket(1300).create(sessionKey, publicKeys, payloads, ByteVector.empty) + assert(packet.hmac == ByteVector32(hex"ea4569794d5cc2f08213b96bcec8969982ba538e41f04c7ce4afca4aca5a202d")) + assert(packet.publicKey == PublicKey(hex"0256b328b30c8bf5839e24058747879408bdb36241dc9c2e7c619faa12b2920967").value) + assert(packet.payload == + hex"36df3dc57743a3408baa1bb039a03c41ddfa1c187f0db0759135f884a2be7581f2e800d0fb1219a57552c707b1d3236c314f544ddac62f68df9faad250068c568854c078839ccb49a176e98730d6206add36dd8de8bcd2dcfee4ec43370a585c741e629f" ++ + hex"6f49c22d8e5c3527a559e174eb82dc51b4b384d8597a7d2f6a7e86ac3f0c49d018fb4a43ed8fa6f054801430f4abc79b6ff4420fa2cde40855ba1fe7058b9d82e13acb215f857107989d407ff388c4925fa6acc21fc16af689fecc90ddf3978167980409" ++ + hex"13b5853c4749335052fc2ea891b61ff9b30e0a1577bfecd3dc82198fdadcebab4ecdf69f616cce916106ad3138190bfd1499a9266a18fc42e859377a52840f102fc462b3c931b84e7ffd008d27d899be4b74e8ce9e0f8933815074e94973054665575061" ++ + hex"fc2623aa38021d0759f7d1f9f150abd72cc82bac988dcb324e6c7a0c2a22975bd5dd462ae52ef8f37b40d7fc3f994bd42c4d277163bb39696711daad871a363bade53e3acf7d40c8a000e1e83acaa9ca7f664c1e7d9ba360f243ba4eaa5d81febc99984e" ++ + hex"b77f4b9f8faba4ebfd225d4cc1f4d13982bb9d4812935e13ef0d08b2ccf93d42d15b4ee91cb11ff1158e04d8ab10c53585ccf812d58f289c2610fd8aaf5e40c4fd96d3fc623cae2f95bf597fafc94d978c9392760a6f9f2e32a228b31118863c7ca8ca21" ++ + hex"4dad17410d09448d727f4f42f7dcf5bc4209fb911a18123090caf087971501eb4262c20184e8ad6243b2c92f2a2f3f1bdfbf1b9108484b86504fa4447402dbff07bbf493ffa3b0b2a0df722c5778cef4657eb071caf3748b8fedf61f379b9b1470cdf9ef" ++ + hex"262eaf97ba358c548029532576ca98d8b89e57825b60d1ec12c97c3a3f1928ee3d87da2dde7b6bb352b91270a8cf04069dea88c3155947f027e3234d4db2bf13de362818100505788ecfcff1862fe22eef0aa2d12686d3ec3d9d5d81c9a04afb31733f91" ++ + hex"3fb5ac71e6dc9e13470f898e417a737ca18a65b9b478a27ef50885384f3e547b5fade2ff5e17296c69c1d6792e321e7cfe1e0f9decd020437ab76f22b908a0f2155a0be1b348427c1ebfbff5e23b7213d2d5625f616d1567c8b4c4401b6423cad5b98513" ++ + hex"985f7ac915f063318537202573ed5fcd3ba4a9acb44f33d3fc167d9ba395849b99bf0158416eeb7f133e6802958ad3cae11e097832fa44461869cf0fab5e8f0957f3fb72b598207ed761efc534851aa33393c79df5cd9ffd7f43898e9c20d4f8b5f39023" ++ + hex"e1bffafec41d834e6c4bfdc453a8f67155511e044de8026c8259bfcd470ba2c85ef96865339eae3b3df728a571ea5a7c14c52fadf9783caad9141aec8c797a791aa78a09f6a2d791fcf110a7dd8a810026188f9280bb651efc81d9a357c21c27cb6631e9" ++ + hex"7cd55d097686d072ff300cc404ef4eab24b5d967c4addbb77c46db252310231bd1c9b2fe13d55f49af3b59be61aeb97b2bf5021914eea254e51b92eb2456ec8a868fd14708588cea7f74be16a632f1a39b49391df6d1a40a644fd09d36758022af76b9d9" ++ + hex"bce57692376f6cd612144977ba6a987e1a15e585c7b280efe95d117eda3d4c2a8284812a2abbb70a3059bf205c4404aed9b4c1793ea0ad222ca162def569ba5168ec020c8cd92f226585fe4cc82c55b993d411c631b06c9d570806d750f9eef59d33265c" ++ + hex"a2a8085cf7f92eb60c34a32aceb4f9e51d7492a5778914e6866974bcbe149789ad7cac704d85d6a7c242804758b8361588c6a45fe2f55bcfe72348d27ac548d661a75c279bd094158b9b7b06d31df202050dc79be27f3b8619c4f917b17dc60b2e75ea30") + val onionForAlice = OnionMessage(blindingSecret.publicKey, packet) + + /* + * Building the onion with functions from `OnionMessages` + */ + val replyPath = OnionMessages.buildRoute(blindingOverride, IntermediateNode(carol.publicKey, padding = Some(hex"0000000000000000000000000000000000000000000000000000000000000000000000")) :: Nil, Left(Recipient(dave.publicKey, pathId = Some(hex"01234567")))) + assert(replyPath == routeFromCarol) + val message = OnionMessages.buildMessage(sessionKey, blindingSecret, IntermediateNode(alice.publicKey) :: IntermediateNode(bob.publicKey) :: Nil, Right(replyPath), Nil) + assert(message == onionForAlice) + + /* + * Checking that the onion is relayed properly + */ + OnionMessages.process(alice, onionForAlice) match { + case OnionMessages.RelayMessage(nextNodeId, onionForBob) => + assert(nextNodeId == bob.publicKey) + OnionMessages.process(bob, onionForBob) match { + case OnionMessages.RelayMessage(nextNodeId, onionForCarol) => + assert(nextNodeId == carol.publicKey) + OnionMessages.process(carol, onionForCarol) match { + case OnionMessages.RelayMessage(nextNodeId, onionForDave) => + assert(nextNodeId == dave.publicKey) + OnionMessages.process(dave, onionForDave) match { + case OnionMessages.ReceiveMessage(_, _) => () + case x => fail(x.toString) + } + case x => fail(x.toString) + } + case x => fail(x.toString) + } + case x => fail(x.toString) + } + } + + test("Simple enctlv for Alice, next is Bob") { + val nodePrivateKey = PrivateKey(hex"414141414141414141414141414141414141414141414141414141414141414101") + val nodeId = PublicKey(hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619") + assert(nodePrivateKey.publicKey == nodeId) + val blindingSecret = PrivateKey(hex"050505050505050505050505050505050505050505050505050505050505050501") + val blindingKey = PublicKey(hex"0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7") + assert(blindingSecret.publicKey == blindingKey) + val sharedSecret = ByteVector32(hex"2e83e9bc7821d3f6cec7301fa8493aee407557624fb5745bede9084852430e3f") + assert(Sphinx.computeSharedSecret(nodeId, blindingSecret) == sharedSecret) + assert(Sphinx.computeSharedSecret(blindingKey, nodePrivateKey) == sharedSecret) + assert(Sphinx.mac(ByteVector("blinded_node_id".getBytes), sharedSecret) == ByteVector32(hex"7d846b3445621d49a665e5698c52141e9dda8fa2fe0c3da7e0f9008ccc588a38")) + val blindedNodeId = PublicKey(hex"02004b5662061e9db495a6ad112b6c4eba228a079e8e304d9df50d61043acbc014") + val nextNodeId = PublicKey(hex"0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c") + val encmsg = hex"04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c" + val Sphinx.RouteBlinding.BlindedRoute(_, _, blindedNodes) = Sphinx.RouteBlinding.create(blindingSecret, nodeId :: Nil, encmsg :: Nil) + assert(blindedNodes.head.blindedPublicKey == blindedNodeId) + assert(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes) == ByteVector32(hex"bae3d9ea2b06efd1b7b9b49b6cdcaad0e789474a6939ffa54ff5ec9224d5b76c")) + val enctlv = hex"6970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9" + assert(blindedNodes.head.encryptedPayload == enctlv) + val message = Onion.RelayBlindedTlv(TlvStream(OnionPaymentPayloadTlv.NextNodeId(nextNodeId))) + assert(OnionCodecs.relayBlindedTlvCodec.encode(message).require.bytes == encmsg) + val relayNext = OnionCodecs.relayBlindedTlvCodec.decode(encmsg.bits).require.value + assert(relayNext.nextNodeId == nextNodeId) + assert(relayNext.nextBlinding.isEmpty) + assert(Sphinx.RouteBlinding.decryptPayload(nodePrivateKey, blindingKey, enctlv).get._1 == encmsg) + } + + test("Blinding-key-override enctlv for Bob, next is Carol") { + val nodePrivateKey = PrivateKey(hex"424242424242424242424242424242424242424242424242424242424242424201") + val nodeId = PublicKey(hex"0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c") + assert(nodePrivateKey.publicKey == nodeId) + val blindingSecret = PrivateKey(hex"76d4de6c329c79623842dcf8f8eaee90c9742df1b5231f5350df4a231d16ebcf01") + val blindingKey = PublicKey(hex"03fc5e56da97b462744c9a6b0ba9d5b3ffbfb1a08367af9cc6ea5ae03c79a78eec") + assert(blindingSecret.publicKey == blindingKey) + val sharedSecret = ByteVector32(hex"f18a1ddb1cb27d8fc4faf2cf317e87524fcc6b7f053496d95bf6e6809d09851e") + assert(Sphinx.computeSharedSecret(nodeId, blindingSecret) == sharedSecret) + assert(Sphinx.computeSharedSecret(blindingKey, nodePrivateKey) == sharedSecret) + assert(Sphinx.mac(ByteVector("blinded_node_id".getBytes), sharedSecret) == ByteVector32(hex"8074773a3745818b0d97dd875023486cc35e7afd95f5e9ec1363f517979e8373")) + val blindedNodeId = PublicKey(hex"026ea8e36f78e038c659beba9229699796127471d9c7a24a0308533371fd63ad48") + val nextNodeId = PublicKey(hex"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007") + val encmsg = hex"0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa20070c2102989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f" + val Sphinx.RouteBlinding.BlindedRoute(_, _, blindedHops) = Sphinx.RouteBlinding.create(blindingSecret, nodeId :: Nil, encmsg :: Nil) + assert(blindedHops.head.blindedPublicKey == blindedNodeId) + assert(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes) == ByteVector32(hex"9afb8b2ebc174dcf9e270be24771da7796542398d29d4ff6a4e7b6b4b9205cfe")) + val enctlv = hex"1630da85e8759b8f3b94d74a539c6f0d870a87cf03d4986175865a2985553c997b560c36613bd9184c1a6d41a37027aabdab5433009d8409a1b638eb90373778a05716af2c215b3d31db7b2c2659716e663ba3d9c909" + assert(blindedHops.head.encryptedPayload == enctlv) + val message = Onion.RelayBlindedTlv(TlvStream(OnionPaymentPayloadTlv.NextNodeId(nextNodeId), OnionPaymentPayloadTlv.BlindingPoint(PrivateKey(hex"070707070707070707070707070707070707070707070707070707070707070701").publicKey))) + assert(OnionCodecs.relayBlindedTlvCodec.encode(message).require.bytes == encmsg) + val relayNext = OnionCodecs.relayBlindedTlvCodec.decode(encmsg.bits).require.value + assert(relayNext.nextNodeId == nextNodeId) + assert(relayNext.nextBlinding contains PrivateKey(hex"070707070707070707070707070707070707070707070707070707070707070701").publicKey) + assert(Sphinx.RouteBlinding.decryptPayload(nodePrivateKey, blindingKey, enctlv).get._1 == encmsg) + } + + test("Padded enctlv for Carol, next is Dave") { + val nodePrivateKey = PrivateKey(hex"434343434343434343434343434343434343434343434343434343434343434301") + val nodeId = PublicKey(hex"027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007") + assert(nodePrivateKey.publicKey == nodeId) + val blindingSecret = PrivateKey(hex"070707070707070707070707070707070707070707070707070707070707070701") + val blindingKey = PublicKey(hex"02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f") + assert(blindingSecret.publicKey == blindingKey) + val sharedSecret = ByteVector32(hex"8c0f7716da996c4913d720dbf691b559a4945bf70cdd18e0b61e3e42635efc9c") + assert(Sphinx.computeSharedSecret(nodeId, blindingSecret) == sharedSecret) + assert(Sphinx.computeSharedSecret(blindingKey, nodePrivateKey) == sharedSecret) + assert(Sphinx.mac(ByteVector("blinded_node_id".getBytes), sharedSecret) == ByteVector32(hex"02afb2187075c8af51488242194b44c02624785ccd6fd43b5796c68f3025bf88")) + val blindedNodeId = PublicKey(hex"02f4f524562868a09d5f54fb956ade3fa51ef071d64d923e395cc6db5e290ec67b") + val nextNodeId = PublicKey(hex"032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991") + val encmsg = hex"012300000000000000000000000000000000000000000000000000000000000000000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991" + val Sphinx.RouteBlinding.BlindedRoute(_, _, blindedHops) = Sphinx.RouteBlinding.create(blindingSecret, nodeId :: Nil, encmsg :: Nil) + assert(blindedHops.head.blindedPublicKey == blindedNodeId) + assert(Crypto.sha256(blindingKey.value ++ sharedSecret.bytes) == ByteVector32(hex"cc3b918cda6b1b049bdbe469c4dd952935e7c1518dd9c7ed0cd2cd5bc2742b82")) + val enctlv = hex"8285acbceb37dfb38b877a888900539be656233cd74a55c55344fb068f9d8da365340d21db96fb41b76123207daeafdfb1f571e3fea07a22e10da35f03109a0380b3c69fcbed9c698086671809658761cf65ecbc3c07a2e5" + assert(blindedHops.head.encryptedPayload == enctlv) + val message = Onion.RelayBlindedTlv(TlvStream(OnionPaymentPayloadTlv.Padding(hex"0000000000000000000000000000000000000000000000000000000000000000000000"), OnionPaymentPayloadTlv.NextNodeId(nextNodeId))) + assert(OnionCodecs.relayBlindedTlvCodec.encode(message).require.bytes == encmsg) + val relayNext = OnionCodecs.relayBlindedTlvCodec.decode(encmsg.bits).require.value + assert(relayNext.nextNodeId == nextNodeId) + assert(relayNext.nextBlinding.isEmpty) + assert(Sphinx.RouteBlinding.decryptPayload(nodePrivateKey, blindingKey, enctlv).get._1 == encmsg) + } +}