From 2cffdd72ebe2ef0741238bc56966b8f5b4c98ef0 Mon Sep 17 00:00:00 2001 From: Thomas HUET <81159533+thomash-acinq@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:30:13 +0200 Subject: [PATCH] Build onion messages (#623) Build onion messages and routes for onion messages. Also make blinded paths mandatory when building offers as we are a wallet hidden behind a trampoline node. Co-authored-by: t-bast --- .../acinq/lightning/crypto/RouteBlinding.kt | 29 +- .../acinq/lightning/message/OnionMessages.kt | 181 +++++++++ .../fr/acinq/lightning/wire/MessageOnion.kt | 3 + .../fr/acinq/lightning/wire/OfferTypes.kt | 90 +++-- .../fr/acinq/lightning/wire/RouteBlinding.kt | 9 +- .../message/OnionMessagesTestsCommon.kt | 286 ++++++++++++++ .../payment/Bolt12InvoiceTestsCommon.kt | 26 +- .../payment/PaymentPacketTestsCommon.kt | 4 +- .../lightning/wire/OfferTypesTestsCommon.kt | 359 ++++++++++++++---- 9 files changed, 844 insertions(+), 143 deletions(-) create mode 100644 src/commonMain/kotlin/fr/acinq/lightning/message/OnionMessages.kt create mode 100644 src/commonTest/kotlin/fr/acinq/lightning/message/OnionMessagesTestsCommon.kt diff --git a/src/commonMain/kotlin/fr/acinq/lightning/crypto/RouteBlinding.kt b/src/commonMain/kotlin/fr/acinq/lightning/crypto/RouteBlinding.kt index 3e223e210..2451ea225 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/crypto/RouteBlinding.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/crypto/RouteBlinding.kt @@ -10,12 +10,10 @@ import fr.acinq.lightning.crypto.sphinx.Sphinx object RouteBlinding { /** - * @param nodeId introduction node's id (which cannot be blinded since the sender need to find a route to it). - * @param blindedPublicKey blinded public key, which hides the real public key. - * @param blindingEphemeralKey blinding tweak that can be used by the receiving node to derive the private key that - * matches the blinded public key. - * @param encryptedPayload encrypted payload that can be decrypted with the introduction node's private key and the - * blinding ephemeral key. + * @param nodeId introduction node's id (which cannot be blinded since the sender need to find a route to it). + * @param blindedPublicKey blinded public key, which hides the real public key. + * @param blindingEphemeralKey blinding tweak that can be used by the receiving node to derive the private key that matches the blinded public key. + * @param encryptedPayload encrypted payload that can be decrypted with the introduction node's private key and the blinding ephemeral key. */ data class IntroductionNode( val nodeId: EncodedNodeId, @@ -26,16 +24,14 @@ object RouteBlinding { /** * @param blindedPublicKey blinded public key, which hides the real public key. - * @param encryptedPayload encrypted payload that can be decrypted with the receiving node's private key and the - * blinding ephemeral key. + * @param encryptedPayload encrypted payload that can be decrypted with the receiving node's private key and the blinding ephemeral key. */ data class BlindedNode(val blindedPublicKey: PublicKey, val encryptedPayload: ByteVector) /** * @param introductionNodeId the first node, not 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). + * @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). */ data class BlindedRoute( val introductionNodeId: EncodedNodeId, @@ -58,7 +54,7 @@ object RouteBlinding { * * @param sessionKey this node's session key. * @param publicKeys public keys of each node on the route, starting from the introduction point. - * @param payloads payloads that should be encrypted for each node on the route. + * @param payloads payloads that should be encrypted for each node on the route. * @return a blinded route. */ fun create(sessionKey: PrivateKey, publicKeys: List, payloads: List): BlindedRoute { @@ -85,7 +81,7 @@ object RouteBlinding { /** * Compute the blinded private key that must be used to decrypt an incoming blinded onion. * - * @param privateKey this node's private key. + * @param privateKey this node's private key. * @param blindingEphemeralKey unblinding ephemeral key. * @return this node's blinded private key. */ @@ -97,9 +93,9 @@ object RouteBlinding { /** * Decrypt the encrypted payload (usually found in the onion) that contains instructions to locate the next node. * - * @param privateKey this node's private key. + * @param privateKey this node's private key. * @param blindingEphemeralKey unblinding ephemeral key. - * @param encryptedPayload encrypted payload for this node. + * @param encryptedPayload encrypted payload for this node. * @return a tuple (decrypted payload, unblinding ephemeral key for the next node) */ fun decryptPayload( @@ -116,8 +112,7 @@ object RouteBlinding { byteArrayOf(), encryptedPayload.takeRight(16).toByteArray() ) - val nextBlindingEphemeralKey = - Sphinx.blind(blindingEphemeralKey, Sphinx.computeBlindingFactor(blindingEphemeralKey, sharedSecret)) + val nextBlindingEphemeralKey = Sphinx.blind(blindingEphemeralKey, Sphinx.computeBlindingFactor(blindingEphemeralKey, sharedSecret)) return Pair(ByteVector(decrypted), nextBlindingEphemeralKey) } } \ No newline at end of file diff --git a/src/commonMain/kotlin/fr/acinq/lightning/message/OnionMessages.kt b/src/commonMain/kotlin/fr/acinq/lightning/message/OnionMessages.kt new file mode 100644 index 000000000..6891f5833 --- /dev/null +++ b/src/commonMain/kotlin/fr/acinq/lightning/message/OnionMessages.kt @@ -0,0 +1,181 @@ +package fr.acinq.lightning.message + +import fr.acinq.bitcoin.ByteVector +import fr.acinq.bitcoin.PrivateKey +import fr.acinq.bitcoin.PublicKey +import fr.acinq.bitcoin.byteVector +import fr.acinq.bitcoin.utils.Either +import fr.acinq.lightning.EncodedNodeId +import fr.acinq.lightning.ShortChannelId +import fr.acinq.lightning.crypto.RouteBlinding +import fr.acinq.lightning.crypto.sphinx.Sphinx +import fr.acinq.lightning.logging.MDCLogger +import fr.acinq.lightning.utils.toByteVector +import fr.acinq.lightning.wire.* + +object OnionMessages { + data class IntermediateNode(val nodeId: PublicKey, val outgoingChannelId: ShortChannelId? = null, val padding: ByteVector? = null, val customTlvs: Set = setOf()) { + fun toTlvStream(nextNodeId: EncodedNodeId, nextBlinding: PublicKey? = null): TlvStream { + val tlvs = setOfNotNull( + outgoingChannelId?.let { RouteBlindingEncryptedDataTlv.OutgoingChannelId(it) } ?: RouteBlindingEncryptedDataTlv.OutgoingNodeId(nextNodeId), + nextBlinding?.let { RouteBlindingEncryptedDataTlv.NextBlinding(it) }, + padding?.let { RouteBlindingEncryptedDataTlv.Padding(it) }, + ) + return TlvStream(tlvs, customTlvs) + } + } + + sealed class Destination { + data class BlindedPath(val route: RouteBlinding.BlindedRoute) : Destination() + data class Recipient(val nodeId: PublicKey, val pathId: ByteVector?, val padding: ByteVector? = null, val customTlvs: Set = setOf()) : Destination() + + companion object { + operator fun invoke(contactInfo: OfferTypes.ContactInfo): Destination = + when (contactInfo) { + is OfferTypes.ContactInfo.BlindedPath -> BlindedPath(contactInfo.route) + is OfferTypes.ContactInfo.RecipientNodeId -> Recipient(contactInfo.nodeId, null) + } + } + } + + private fun buildIntermediatePayloads( + intermediateNodes: List, + lastNodeId: EncodedNodeId, + lastBlinding: PublicKey? = null + ): List { + return if (intermediateNodes.isEmpty()) { + listOf() + } else { + val intermediatePayloads = intermediateNodes.dropLast(1).zip(intermediateNodes.drop(1)).map { (current, next) -> current.toTlvStream(EncodedNodeId(next.nodeId)) } + // The last intermediate node may contain a blinding override when the recipient is hidden behind a blinded path. + val lastPayload = intermediateNodes.last().toTlvStream(lastNodeId, lastBlinding) + (intermediatePayloads + lastPayload).map { RouteBlindingEncryptedData(it).write().byteVector() } + } + } + + fun buildRoute( + blindingSecret: PrivateKey, + intermediateNodes: List, + destination: Destination + ): RouteBlinding.BlindedRoute { + return when (destination) { + is Destination.Recipient -> { + val intermediatePayloads = buildIntermediatePayloads(intermediateNodes, EncodedNodeId(destination.nodeId)) + val tlvs = setOfNotNull( + destination.padding?.let { RouteBlindingEncryptedDataTlv.Padding(it) }, + destination.pathId?.let { RouteBlindingEncryptedDataTlv.PathId(it) } + ) + val lastPayload = RouteBlindingEncryptedData(TlvStream(tlvs, destination.customTlvs)).write().toByteVector() + RouteBlinding.create( + blindingSecret, + intermediateNodes.map { it.nodeId } + destination.nodeId, + intermediatePayloads + lastPayload + ) + } + is Destination.BlindedPath -> when { + intermediateNodes.isEmpty() -> destination.route + else -> { + // We concatenate our blinded path with the destination's blinded path. + val intermediatePayloads = buildIntermediatePayloads( + intermediateNodes, + destination.route.introductionNodeId, + destination.route.blindingKey + ) + val routePrefix = RouteBlinding.create( + blindingSecret, + intermediateNodes.map { it.nodeId }, + intermediatePayloads + ) + RouteBlinding.BlindedRoute( + routePrefix.introductionNodeId, + routePrefix.blindingKey, + routePrefix.blindedNodes + destination.route.blindedNodes + ) + } + } + } + } + + sealed class BuildMessageError + data class MessageTooLarge(val payloadSize: Int) : BuildMessageError() + + /** + * Builds an encrypted onion containing a message that should be relayed to the destination. + * + * @param sessionKey a random key to encrypt the onion. + * @param blindingSecret a random key to create the blinded path. + * @param intermediateNodes list of intermediate nodes between us and the destination (can be empty if we want to contact the destination directly). + * @param destination the destination of this message, can be a node id or a blinded route. + * @param content list of TLVs to send to the recipient of the message. + */ + fun buildMessage( + sessionKey: PrivateKey, + blindingSecret: PrivateKey, + intermediateNodes: List, + destination: Destination, + content: TlvStream + ): Either { + val route = buildRoute(blindingSecret, intermediateNodes, destination) + val payloads = buildList { + // Intermediate nodes only receive blinded path relay information. + addAll(route.encryptedPayloads.dropLast(1).map { MessageOnion(TlvStream(OnionMessagePayloadTlv.EncryptedData(it))).write() }) + // The destination receives the message contents and the blinded path information. + add(MessageOnion(content.copy(records = content.records + OnionMessagePayloadTlv.EncryptedData(route.encryptedPayloads.last()))).write()) + } + val payloadSize = payloads.sumOf { it.size + Sphinx.MacLength } + val packetSize = when { + payloadSize <= 1300 -> 1300 + payloadSize <= 32768 -> 32768 + payloadSize <= 65432 -> 65432 // this corresponds to a total lightning message size of 65535 + else -> return Either.Left(MessageTooLarge(payloadSize)) + } + // Since we are setting the packet size based on the payload, the onion creation should never fail. + val packet = Sphinx.create( + sessionKey, + route.blindedNodes.map { it.blindedPublicKey }, + payloads, + associatedData = null, + packetSize + ).packet + return Either.Right(OnionMessage(route.blindingKey, packet)) + } + + /** + * @param content message received. + * @param blindedPrivateKey private key of the blinded node id used in our blinded path. + * @param pathId path_id that we included in our blinded path for ourselves. + */ + data class DecryptedMessage(val content: MessageOnion, val blindedPrivateKey: PrivateKey, val pathId: ByteVector) + + fun decryptMessage(privateKey: PrivateKey, msg: OnionMessage, logger: MDCLogger): DecryptedMessage? { + val blindedPrivateKey = RouteBlinding.derivePrivateKey(privateKey, msg.blindingKey) + return when (val decrypted = Sphinx.peel(blindedPrivateKey, associatedData = ByteVector.empty, msg.onionRoutingPacket)) { + is Either.Right -> try { + val message = MessageOnion.read(decrypted.value.payload.toByteArray()) + val (decryptedPayload, nextBlinding) = RouteBlinding.decryptPayload( + privateKey, + msg.blindingKey, + message.encryptedData + ) + val relayInfo = RouteBlindingEncryptedData.read(decryptedPayload.toByteArray()) + if (!decrypted.value.isLastPacket && relayInfo.nextNodeId == EncodedNodeId(privateKey.publicKey())) { + // We may add ourselves to the route several times at the end to hide the real length of the route. + val nextMessage = OnionMessage(relayInfo.nextBlindingOverride ?: nextBlinding, decrypted.value.nextPacket) + decryptMessage(privateKey, nextMessage, logger) + } else if (decrypted.value.isLastPacket && relayInfo.pathId != null) { + DecryptedMessage(message, blindedPrivateKey, relayInfo.pathId) + } else { + logger.warning { "ignoring onion message for which we're not the destination (next_node_id=${relayInfo.nextNodeId}, path_id=${relayInfo.pathId?.toHex()})" } + null + } + } catch (e: Throwable) { + logger.warning { "ignoring onion message that couldn't be decoded: ${e.message}" } + null + } + is Either.Left -> { + logger.warning { "ignoring onion message that couldn't be decrypted: ${decrypted.value.message}" } + null + } + } + } +} diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt index 0e9516995..465b467e8 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/MessageOnion.kt @@ -132,6 +132,9 @@ data class MessageOnion(val records: TlvStream) { true, @Suppress("UNCHECKED_CAST") mapOf( OnionMessagePayloadTlv.ReplyPath.tag to OnionMessagePayloadTlv.ReplyPath.Companion as TlvValueReader, OnionMessagePayloadTlv.EncryptedData.tag to OnionMessagePayloadTlv.EncryptedData.Companion as TlvValueReader, + OnionMessagePayloadTlv.InvoiceRequest.tag to OnionMessagePayloadTlv.InvoiceRequest.Companion as TlvValueReader, + OnionMessagePayloadTlv.Invoice.tag to OnionMessagePayloadTlv.Invoice.Companion as TlvValueReader, + OnionMessagePayloadTlv.InvoiceError.tag to OnionMessagePayloadTlv.InvoiceError.Companion as TlvValueReader ) ) diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt index 4fa4e4468..6aba80649 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/OfferTypes.kt @@ -9,11 +9,10 @@ import fr.acinq.bitcoin.utils.Either.Left import fr.acinq.bitcoin.utils.Either.Right import fr.acinq.bitcoin.utils.Try import fr.acinq.bitcoin.utils.runTrying -import fr.acinq.lightning.CltvExpiryDelta -import fr.acinq.lightning.Features +import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 -import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.crypto.RouteBlinding +import fr.acinq.lightning.message.OnionMessages /** * Lightning Bolt 12 offers @@ -45,7 +44,7 @@ object OfferTypes { val blindingKey = PublicKey(LightningCodecs.bytes(input, 33)) val blindedNodes = ArrayList() val numBlindedNodes = LightningCodecs.byte(input) - for (i in 1 .. numBlindedNodes) { + for (i in 1..numBlindedNodes) { val blindedKey = PublicKey(LightningCodecs.bytes(input, 33)) val payload = ByteVector(LightningCodecs.bytes(input, LightningCodecs.u16(input))) blindedNodes.add(RouteBlinding.BlindedNode(blindedKey, payload)) @@ -202,7 +201,7 @@ object OfferTypes { override val tag: Long get() = OfferPaths.tag override fun write(out: Output) { - for(path in paths){ + for (path in paths) { writePath(path, out) } } @@ -412,7 +411,7 @@ object OfferTypes { override val tag: Long get() = InvoicePaths.tag override fun write(out: Output) { - for(path in paths){ + for (path in paths) { writePath(path, out) } } @@ -430,12 +429,14 @@ object OfferTypes { } } - data class PaymentInfo(val feeBase: MilliSatoshi, - val feeProportionalMillionths: Int, - val cltvExpiryDelta: CltvExpiryDelta, - val minHtlc: MilliSatoshi, - val maxHtlc: MilliSatoshi, - val allowedFeatures: Features) { + data class PaymentInfo( + val feeBase: MilliSatoshi, + val feeProportionalMillionths: Long, + val cltvExpiryDelta: CltvExpiryDelta, + val minHtlc: MilliSatoshi, + val maxHtlc: MilliSatoshi, + val allowedFeatures: Features + ) { fun fee(amount: MilliSatoshi): MilliSatoshi { return feeBase + amount * feeProportionalMillionths / 1_000_000L } @@ -443,7 +444,7 @@ object OfferTypes { fun writePaymentInfo(paymentInfo: PaymentInfo, out: Output) { LightningCodecs.writeU32(paymentInfo.feeBase.msat.toInt(), out) - LightningCodecs.writeU32(paymentInfo.feeProportionalMillionths, out) + LightningCodecs.writeU32(paymentInfo.feeProportionalMillionths.toInt(), out) LightningCodecs.writeU16(paymentInfo.cltvExpiryDelta.toInt(), out) LightningCodecs.writeU64(paymentInfo.minHtlc.msat, out) LightningCodecs.writeU64(paymentInfo.maxHtlc.msat, out) @@ -459,7 +460,7 @@ object OfferTypes { val minHtlc = MilliSatoshi(LightningCodecs.u64(input)) val maxHtlc = MilliSatoshi(LightningCodecs.u64(input)) val allowedFeatures = Features(LightningCodecs.bytes(input, LightningCodecs.u16(input))) - return PaymentInfo(feeBase, feeProportionalMillionths, cltvExpiryDelta, minHtlc, maxHtlc, allowedFeatures) + return PaymentInfo(feeBase, feeProportionalMillionths.toLong(), cltvExpiryDelta, minHtlc, maxHtlc, allowedFeatures) } /** @@ -647,7 +648,7 @@ object OfferTypes { // Offer TLVs are in the range (0, 80). return TlvStream( tlvs.records.filterIsInstance().toSet(), - tlvs.unknown.filter{it.tag < 80}.toSet() + tlvs.unknown.filter { it.tag < 80 }.toSet() ) } @@ -655,7 +656,7 @@ object OfferTypes { // Invoice request TLVs are in the range [0, 160): invoice request metadata (tag 0), offer TLVs, and additional invoice request TLVs in the range [80, 160). return TlvStream( tlvs.records.filterIsInstance().toSet(), - tlvs.unknown.filter{it.tag < 160}.toSet() + tlvs.unknown.filter { it.tag < 160 }.toSet() ) } @@ -704,11 +705,11 @@ object OfferTypes { } } - sealed class InvalidTlvPayload { - abstract val tag: Long - } + // @formatter:off + sealed class InvalidTlvPayload { abstract val tag: Long } data class MissingRequiredTlv(override val tag: Long) : InvalidTlvPayload() data class ForbiddenTlv(override val tag: Long) : InvalidTlvPayload() + // @formatter:on data class Offer(val records: TlvStream) { val chains: List = records.get()?.chains ?: listOf(Block.LivenetGenesisBlock.hash) @@ -742,14 +743,14 @@ object OfferTypes { val hrp = "lno" /** - * @param amount_opt amount if it can be determined at offer creation time. + * @param amount amount if it can be determined at offer creation time. * @param description description of the offer. * @param nodeId the nodeId to use for this offer, which should be different from our public nodeId if we're hiding behind a blinded route. * @param features invoice features. * @param chain chain on which the offer is valid. */ - operator fun invoke( - amount_opt: MilliSatoshi?, + internal fun createInternal( + amount: MilliSatoshi?, description: String, nodeId: PublicKey, features: Features, @@ -759,19 +760,49 @@ object OfferTypes { ): Offer { val tlvs: Set = setOfNotNull( if (chain != Block.LivenetGenesisBlock.hash) OfferChains(listOf(chain)) else null, - amount_opt?.let { OfferAmount(it) }, + amount?.let { OfferAmount(it) }, OfferDescription(description), if (features != Features.empty) OfferFeatures(features) else null, - OfferNodeId(nodeId), + OfferNodeId(nodeId) // TODO: If the spec allows it, removes `OfferNodeId` when we already set `OfferPaths`. ) + additionalTlvs return Offer(TlvStream(tlvs, customTlvs)) } + /** + * Create an offer using a single-hop blinded path going through our trampoline node. + * + * @param amount amount if it can be determined at offer creation time. + * @param description description of the offer. + * @param nodeParams our node parameters. + * @param trampolineNode our trampoline node. + * @param pathId pathId on which we will listen for invoice requests. + */ + fun createBlindedOffer( + amount: MilliSatoshi?, + description: String, + nodeParams: NodeParams, + trampolineNode: NodeUri, + pathId: ByteVector32, + additionalTlvs: Set = setOf(), + customTlvs: Set = setOf() + ): Offer { + val path = OnionMessages.buildRoute(PrivateKey(pathId), listOf(OnionMessages.IntermediateNode(trampolineNode.id, ShortChannelId.peerId(nodeParams.nodeId))), OnionMessages.Destination.Recipient(nodeParams.nodeId, pathId)) + val offerNodeId = path.blindedNodeIds.last() + return createInternal( + amount, + description, + offerNodeId, + nodeParams.features.bolt12Features(), + nodeParams.chainHash, + additionalTlvs + OfferPaths(listOf(ContactInfo.BlindedPath(path))), + customTlvs + ) + } + fun validate(records: TlvStream): Either { if (records.get() == null) return Left(MissingRequiredTlv(10L)) if (records.get() == null) return Left(MissingRequiredTlv(22L)) - if (records.unknown.any { it.tag >= 80 }) - return Left(ForbiddenTlv(records.unknown.find{it.tag >= 80}!!.tag)) + if (records.unknown.any { it.tag >= 80 }) return Left(ForbiddenTlv(records.unknown.find { it.tag >= 80 }!!.tag)) return Right(Offer(records)) } @@ -840,7 +871,7 @@ object OfferTypes { override fun toString(): String = encode() - fun unsigned(): TlvStream = removeSignature(records) + fun unsigned(): TlvStream = removeSignature(records) companion object { val hrp = "lnr" @@ -893,8 +924,7 @@ object OfferTypes { if (records.get() == null) return Left(MissingRequiredTlv(0L)) if (records.get() == null) return Left(MissingRequiredTlv(88)) if (records.get() == null) return Left(MissingRequiredTlv(240)) - if (records.unknown.any { it.tag >= 160 }) - return Left(ForbiddenTlv(records.unknown.find{ it.tag >= 160 }!!.tag)) + if (records.unknown.any { it.tag >= 160 }) return Left(ForbiddenTlv(records.unknown.find { it.tag >= 160 }!!.tag)) return Right(InvoiceRequest(records)) } @@ -936,7 +966,7 @@ object OfferTypes { } } } - + object Invoice { val tlvSerializer = TlvStreamSerializer( false, @Suppress("UNCHECKED_CAST") mapOf( diff --git a/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt b/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt index 263800ceb..f66e64003 100644 --- a/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt +++ b/src/commonMain/kotlin/fr/acinq/lightning/wire/RouteBlinding.kt @@ -75,11 +75,11 @@ sealed class RouteBlindingEncryptedDataTlv : Tlv { } /** Information for the relaying node to build the next HTLC. */ - data class PaymentRelay(val cltvExpiryDelta: CltvExpiryDelta, val feeProportionalMillionths: Int, val feeBase: MilliSatoshi) : RouteBlindingEncryptedDataTlv() { + data class PaymentRelay(val cltvExpiryDelta: CltvExpiryDelta, val feeProportionalMillionths: Long, val feeBase: MilliSatoshi) : RouteBlindingEncryptedDataTlv() { override val tag: Long get() = PaymentRelay.tag override fun write(out: Output) { LightningCodecs.writeU16(cltvExpiryDelta.toInt(), out) - LightningCodecs.writeU32(feeProportionalMillionths, out) + LightningCodecs.writeU32(feeProportionalMillionths.toInt(), out) LightningCodecs.writeTU32(feeBase.msat.toInt(), out) } @@ -89,7 +89,7 @@ sealed class RouteBlindingEncryptedDataTlv : Tlv { val cltvExpiryDelta = CltvExpiryDelta(LightningCodecs.u16(input)) val feeProportionalMillionths = LightningCodecs.u32(input) val feeBase = MilliSatoshi(LightningCodecs.tu32(input).toLong()) - return PaymentRelay(cltvExpiryDelta, feeProportionalMillionths, feeBase) + return PaymentRelay(cltvExpiryDelta, feeProportionalMillionths.toLong(), feeBase) } } } @@ -130,6 +130,7 @@ sealed class RouteBlindingEncryptedDataTlv : Tlv { data class RouteBlindingEncryptedData(val records: TlvStream) { val nextNodeId = records.get()?.nodeId + val outgoingChannelId = records.get()?.shortChannelId val pathId = records.get()?.data val nextBlindingOverride = records.get()?.blinding @@ -142,7 +143,7 @@ data class RouteBlindingEncryptedData(val records: TlvStream, RouteBlindingEncryptedDataTlv.OutgoingChannelId.tag to RouteBlindingEncryptedDataTlv.OutgoingChannelId as TlvValueReader, diff --git a/src/commonTest/kotlin/fr/acinq/lightning/message/OnionMessagesTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/message/OnionMessagesTestsCommon.kt new file mode 100644 index 000000000..3b927eaf6 --- /dev/null +++ b/src/commonTest/kotlin/fr/acinq/lightning/message/OnionMessagesTestsCommon.kt @@ -0,0 +1,286 @@ +package fr.acinq.lightning.message + +import fr.acinq.bitcoin.* +import fr.acinq.bitcoin.utils.Either +import fr.acinq.lightning.EncodedNodeId +import fr.acinq.lightning.Lightning.randomBytes +import fr.acinq.lightning.Lightning.randomBytes32 +import fr.acinq.lightning.Lightning.randomKey +import fr.acinq.lightning.ShortChannelId +import fr.acinq.lightning.crypto.RouteBlinding +import fr.acinq.lightning.crypto.sphinx.Sphinx +import fr.acinq.lightning.logging.MDCLogger +import fr.acinq.lightning.message.OnionMessages.Destination.BlindedPath +import fr.acinq.lightning.message.OnionMessages.Destination.Recipient +import fr.acinq.lightning.message.OnionMessages.IntermediateNode +import fr.acinq.lightning.message.OnionMessages.buildMessage +import fr.acinq.lightning.message.OnionMessages.buildRoute +import fr.acinq.lightning.message.OnionMessages.decryptMessage +import fr.acinq.lightning.tests.utils.testLoggerFactory +import fr.acinq.lightning.utils.toByteVector +import fr.acinq.lightning.wire.* +import org.kodein.memory.text.toHex +import kotlin.test.* + +class OnionMessagesTestsCommon { + + val logger: MDCLogger = MDCLogger(testLoggerFactory.newLogger(this::class)) + + private fun relayMessage(privateKey: PrivateKey, msg: OnionMessage): Pair, OnionMessage> { + val blindedPrivateKey = RouteBlinding.derivePrivateKey(privateKey, msg.blindingKey) + val decrypted = Sphinx.peel( + blindedPrivateKey, + ByteVector.empty, + msg.onionRoutingPacket + ).right!! + val message = MessageOnion.read(decrypted.payload.toByteArray()) + val (decryptedPayload, nextBlinding) = RouteBlinding.decryptPayload( + privateKey, + msg.blindingKey, + message.encryptedData + ) + val relayInfo = RouteBlindingEncryptedData.read(decryptedPayload.toByteArray()) + assertFalse(decrypted.isLastPacket) + return Pair( + relayInfo.nextNodeId?.let { Either.Right(it) } ?: Either.Left(relayInfo.outgoingChannelId!!), + OnionMessage(relayInfo.nextBlindingOverride ?: nextBlinding, decrypted.nextPacket) + ) + } + + @Test + fun `single-hop onion message`() { + val sessionKey = randomKey() + val blindingSecret = randomKey() + val destination = randomKey() + val pathId = randomBytes32() + val message = buildMessage(sessionKey, blindingSecret, listOf(), Recipient(destination.publicKey(), pathId), TlvStream.empty()) + assertIs>(message) + + val decrypted = decryptMessage(destination, message.value, logger) + assertNotNull(decrypted) + assertEquals(pathId, decrypted.pathId) + } + + @Test + fun `multi-hop onion message`() { + val alice = PrivateKey.fromHex("414141414141414141414141414141414141414141414141414141414141414101") + assertEquals(PublicKey.fromHex("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619"), alice.publicKey()) + val bob = PrivateKey.fromHex("424242424242424242424242424242424242424242424242424242424242424201") + assertEquals(PublicKey.fromHex("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"), bob.publicKey()) + val carol = PrivateKey.fromHex("434343434343434343434343434343434343434343434343434343434343434301") + assertEquals(PublicKey.fromHex("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007"), carol.publicKey()) + val dave = PrivateKey.fromHex("444444444444444444444444444444444444444444444444444444444444444401") + assertEquals(PublicKey.fromHex("032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991"), dave.publicKey()) + + val blindingSecret = PrivateKey.fromHex("050505050505050505050505050505050505050505050505050505050505050501") + assertEquals(PublicKey.fromHex("0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7"), blindingSecret.publicKey()) + val blindingOverride = PrivateKey.fromHex("070707070707070707070707070707070707070707070707070707070707070701") + assertEquals(PublicKey.fromHex("02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f"), blindingOverride.publicKey()) + + // Building the onion manually + val messageForAlice = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId(bob.publicKey())))) + val encodedForAlice = messageForAlice.write().toByteVector() + assertEquals("04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", encodedForAlice.toHex()) + val messageForBob = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId(carol.publicKey())), RouteBlindingEncryptedDataTlv.NextBlinding(blindingOverride.publicKey()))) + val encodedForBob = messageForBob.write().toByteVector() + assertEquals("0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007082102989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f", encodedForBob.toHex()) + val messageForCarol = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.Padding(ByteVector.fromHex("0000000000000000000000000000000000000000000000000000000000000000000000")), RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId(dave.publicKey())))) + val encodedForCarol = messageForCarol.write().toByteVector() + assertEquals("012300000000000000000000000000000000000000000000000000000000000000000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", encodedForCarol.toHex()) + val messageForDave = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.PathId(ByteVector.fromHex("01234567")))) + val encodedForDave = messageForDave.write().toByteVector() + assertEquals("060401234567", encodedForDave.toHex()) + + // Building blinded path Carol -> Dave + val routeFromCarol = RouteBlinding.create(blindingOverride, listOf(carol.publicKey(), dave.publicKey()), listOf(encodedForCarol, encodedForDave)) + + // Building blinded path Alice -> Bob + val routeToCarol = RouteBlinding.create(blindingSecret, listOf(alice.publicKey(), bob.publicKey()), listOf(encodedForAlice, encodedForBob)) + + val publicKeys = routeToCarol.blindedNodes.map { it.blindedPublicKey } + routeFromCarol.blindedNodes.map { it.blindedPublicKey } + val encryptedPayloads = routeToCarol.encryptedPayloads + routeFromCarol.encryptedPayloads + val payloads = encryptedPayloads.map { MessageOnion.tlvSerializer.write(TlvStream(OnionMessagePayloadTlv.EncryptedData(it))) } + val expectedPayloads = listOf( + "3504336970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9", + "5804561630da85e8759b8f3b94d74a539c6f0d870a87cf03d4986175865a2985553c997b560c32613bd9184c1a6d41a37027aabdab5433009d8409a1b638eb90373778a05716af2c2140b3196dca23997cdad4cfa7a7adc8d4", + "5a04588285acbceb37dfb38b877a888900539be656233cd74a55c55344fb068f9d8da365340d21db96fb41b76123207daeafdfb1f571e3fea07a22e10da35f03109a0380b3c69fcbed9c698086671809658761cf65ecbc3c07a2e5", + "180416a20771fd5ff63f8ee26fac46c9de93cf6bd5916a928c" + ) + assertEquals(expectedPayloads, payloads.map { it.toHex() }) + + val sessionKey = PrivateKey.fromHex("090909090909090909090909090909090909090909090909090909090909090901") + + val (packet, _) = Sphinx.create(sessionKey, publicKeys, payloads, null, 1300) + assertEquals("d84e7135092450c8cc98bb969aa6d9127dd07da53a3c46b2e9339d111f5f301d", packet.hmac.toHex()) + assertEquals("0256b328b30c8bf5839e24058747879408bdb36241dc9c2e7c619faa12b2920967", packet.publicKey.toHex()) + assertEquals("37d167dcefdb678725cb8074d3224dfe235ba3f22f71ac8a2c9d1398b1175295b1dd3f14c02d698021e8a8856637306c6f195e01494e6d75bfc0812f3f6d74e4dce347ffc1c8e01595fa595f68f3e7358aad4bf2d9412e9f307a25b6d5e4045174551b1c867264d3905e4f05b2e5bcfed7e7276660bf7e956bce5afa3d5e7e4c15883b856bc93dd9d6a968838ef51314d38dd41e5ab84b8846dca3c61d87e54c0ecf116b3cd5b3f1fcfbba3067cc329437cb301749447ad106f43955a643b52c66d465fc7abd2add1ab398aa63c890ae3dc564395bb7a4bbe28325ccdb07503285dacf90b5e09f4e455fb42459741f9d497000298b99f1e70adc28f59a1be85a96952f27b6a6c5d6a08822b4f5cae05daa6c2ce2f8ca5fdd4e8f0df46b94791b3159fe8eace11bcf8d58b532967a024f7e7e85929456a1332d9139ce7de92b9a5985acab8cd7630c9a0580bfd74b28e7ce5bd25e63e7ae369795dfe74c21e24b8bbf02d1f4eb8fbd86920f41d573488abe059166aabbc3be187c435423ead6a5473994e0246efe76e419893aa2d7566b2645f3496d97585de9c92b8c5a5226398cc459ce84abc02fe2b45b5ecaf21961730d4a34bbe6fdfe720e71e3d81a494c01080d8039360d534c6ee5a3c47a1874e526969add9126b30d9192f85ba45bcfd7029cc7560f0e25e14b5deaa805360c4967705e85325ac055922863470f5397e8404022488caebf9204acd6cb02a11088aebf7e497b4ff1172f0a9c6bf980914cc4eb42fc78b457add549abf1134f84922b217502938b42d10b35079f44c5168d4c3e9fe7ca8094ef72ed73ef84f1d3530b6b3545f9f4f013e7e8cbcf2619f57754a7380ce6a9532ee14c55990faa43df6c09530a314b5f4ce597f5ec9b776e8597ce258ac47dac43bd3ac9e52788ff3a66b7dc07cd1bc3e6d197339d85fa8d3d6c3054dd1a5e416c714b544de6eb55209e40e3cac412a51748370160d2d73b6d97abd62f7bae70df27cd199c511fa693019c5717d471e934906b98cd974fda4dd1cb5e2d721044a0be2bdf24d0971e09f2f39488fe389fc5230699b4df7cec7447e5be4ea49bd7c3fe1a5ec7358510dc1dd9c1a8da68c0863188d80549e49f7c00f57d2009b2427b2aed1569603fc247734039469f9fdf3ddd3a22fa95c5d8066a468327a02b474c9915419af82c8edc67686984767fe7885207c6820f6c2e57cb8fd0bcb9981ebc8065c74e970a5d593c3b73ee25a0877ca096a9f7edfee6d43bd817c7d415fea9abb6f206c61aa36942df9318762a76b9da26d0d41a0ae9eee042a175f82dc134bf6f2d46a218db358d6852940e6e30df4a58ac6cb409e7ce99afe1e3f42768bd617af4d0a235d0ba0dd5075f9cc091784395d30e7e42d4e006db21bea9b45d1f122b75c051e84e2281573ef54ebad053218fff0cc28ea89a06adc218d4134f407654990592e75462f5ee4a463c1e46425222d48761162da8049613cafd7ecc52ff8024e9d58512b958e3a3d12dede84e1441247700bca0f992875349448b430683c756438fd4e91f3d44f3cf624ed21f3c63cf92615ecc201d0cd3159b1b3fccd8f29d2daba9ac5ba87b1dd2f83323a2b2d3176b803ce9c7bdc4bae615925eb22a213df1eeb2f8ff95586536caf042d565984aacf1425a120a5d8d7a9cbb70bf4852e116b89ff5b198d672220af2be4246372e7c3836cf50d732212a3e3346ff92873ace57fa687b2b1aab3e8dc6cb9f93f865d998cff0a1680d9012a9597c90a070e525f66226cc287814f4ac4157b15a0b25aa110946cd69fd404fafd5656669bfd1d9e509eabc004c5a", packet.payload.toHex()) + val onionForAlice = OnionMessage(blindingSecret.publicKey(), packet) + + // Building the onion with functions from `OnionMessages` + val replyPath = buildRoute(blindingOverride, listOf(IntermediateNode(carol.publicKey(), padding = ByteVector.fromHex("0000000000000000000000000000000000000000000000000000000000000000000000"))), Recipient(dave.publicKey(), ByteVector.fromHex("01234567"))) + assertEquals(routeFromCarol, replyPath) + val message = buildMessage(sessionKey, blindingSecret, listOf(IntermediateNode(alice.publicKey()), IntermediateNode(bob.publicKey())), BlindedPath(replyPath), TlvStream.empty()) + assertEquals(Either.Right(onionForAlice), message) + + // Checking that the onion is relayed properly + val (nextNodeId1, onionForBob) = relayMessage(alice, onionForAlice) + assertEquals(Either.Right(EncodedNodeId(bob.publicKey())), nextNodeId1) + val (nextNodeId2, onionForCarol) = relayMessage(bob, onionForBob) + assertEquals(Either.Right(EncodedNodeId(carol.publicKey())), nextNodeId2) + val (nextNodeId3, onionForDave) = relayMessage(carol, onionForCarol) + assertEquals(Either.Right(EncodedNodeId(dave.publicKey())), nextNodeId3) + val decrypted = decryptMessage(dave, onionForDave, logger)!! + assertNotNull(decrypted) + assertEquals("01234567", decrypted.pathId.toHex()) + } + + @Test + fun `relay message from alice to bob`() { + val alice = PrivateKey.fromHex("414141414141414141414141414141414141414141414141414141414141414101") + val bob = PrivateKey.fromHex("424242424242424242424242424242424242424242424242424242424242424201") + val blindingSecret = PrivateKey.fromHex("050505050505050505050505050505050505050505050505050505050505050501") + val blindingKey = PublicKey.fromHex("0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f7") + assertEquals(blindingKey, blindingSecret.publicKey()) + val sharedSecret = ByteVector32.fromValidHex("2e83e9bc7821d3f6cec7301fa8493aee407557624fb5745bede9084852430e3f") + assertEquals(sharedSecret, Sphinx.computeSharedSecret(alice.publicKey(), blindingSecret)) + assertEquals(sharedSecret, Sphinx.computeSharedSecret(blindingKey, alice)) + assertEquals("7d846b3445621d49a665e5698c52141e9dda8fa2fe0c3da7e0f9008ccc588a38", Sphinx.mac("blinded_node_id".encodeToByteArray().toByteVector(), sharedSecret).toHex()) + val blindedAlice = PublicKey.fromHex("02004b5662061e9db495a6ad112b6c4eba228a079e8e304d9df50d61043acbc014") + val blindedPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId(bob.publicKey())))) + val encodedBlindedPayload = blindedPayload.write().toByteVector() + assertEquals("04210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", encodedBlindedPayload.toHex()) + val blindedRoute = RouteBlinding.create(blindingSecret, listOf(alice.publicKey()), listOf(encodedBlindedPayload)) + assertEquals(blindedAlice, blindedRoute.blindedNodes.first().blindedPublicKey) + assertEquals("bae3d9ea2b06efd1b7b9b49b6cdcaad0e789474a6939ffa54ff5ec9224d5b76c", Crypto.sha256(blindingKey.value + sharedSecret).toHex()) + assertEquals("6970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9", blindedRoute.blindedNodes.first().encryptedPayload.toHex()) + val decryptedPayload = RouteBlindingEncryptedData.read(RouteBlinding.decryptPayload(alice, blindingKey, blindedRoute.blindedNodes.first().encryptedPayload).first.toByteArray()) + assertEquals(blindedPayload, decryptedPayload) + } + + @Test + fun `relay message from bob to carol with blinding override`() { + val bob = PrivateKey.fromHex("424242424242424242424242424242424242424242424242424242424242424201") + val carol = PrivateKey.fromHex("434343434343434343434343434343434343434343434343434343434343434301") + val blindingSecret = PrivateKey.fromHex("76d4de6c329c79623842dcf8f8eaee90c9742df1b5231f5350df4a231d16ebcf01") + val blindingKey = PublicKey.fromHex("03fc5e56da97b462744c9a6b0ba9d5b3ffbfb1a08367af9cc6ea5ae03c79a78eec") + assertEquals(blindingKey, blindingSecret.publicKey()) + val sharedSecret = ByteVector32.fromValidHex("f18a1ddb1cb27d8fc4faf2cf317e87524fcc6b7f053496d95bf6e6809d09851e") + assertEquals(sharedSecret, Sphinx.computeSharedSecret(bob.publicKey(), blindingSecret)) + assertEquals(sharedSecret, Sphinx.computeSharedSecret(blindingKey, bob)) + assertEquals("8074773a3745818b0d97dd875023486cc35e7afd95f5e9ec1363f517979e8373", Sphinx.mac("blinded_node_id".encodeToByteArray().toByteVector(), sharedSecret).toHex()) + val blindedBob = PublicKey.fromHex("026ea8e36f78e038c659beba9229699796127471d9c7a24a0308533371fd63ad48") + val blindingOverride = PrivateKey.fromHex("070707070707070707070707070707070707070707070707070707070707070701").publicKey() + val blindedPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId(carol.publicKey())), RouteBlindingEncryptedDataTlv.NextBlinding(blindingOverride))) + val encodedBlindedPayload = blindedPayload.write().toByteVector() + assertEquals("0421027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007082102989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f", encodedBlindedPayload.toHex()) + val blindedRoute = RouteBlinding.create(blindingSecret, listOf(bob.publicKey()), listOf(encodedBlindedPayload)) + assertEquals(blindedBob, blindedRoute.blindedNodes.first().blindedPublicKey) + assertEquals("9afb8b2ebc174dcf9e270be24771da7796542398d29d4ff6a4e7b6b4b9205cfe", Crypto.sha256(blindingKey.value + sharedSecret).toHex()) + assertEquals("1630da85e8759b8f3b94d74a539c6f0d870a87cf03d4986175865a2985553c997b560c32613bd9184c1a6d41a37027aabdab5433009d8409a1b638eb90373778a05716af2c2140b3196dca23997cdad4cfa7a7adc8d4", blindedRoute.blindedNodes.first().encryptedPayload.toHex()) + val decryptedPayload = RouteBlindingEncryptedData.read(RouteBlinding.decryptPayload(bob, blindingKey, blindedRoute.blindedNodes.first().encryptedPayload).first.toByteArray()) + assertEquals(blindedPayload, decryptedPayload) + assertEquals(blindingOverride, decryptedPayload.nextBlindingOverride) + } + + @Test + fun `relay message from carol to dave with padding`() { + val carol = PrivateKey.fromHex("434343434343434343434343434343434343434343434343434343434343434301") + val dave = PrivateKey.fromHex("444444444444444444444444444444444444444444444444444444444444444401") + val blindingSecret = PrivateKey.fromHex("070707070707070707070707070707070707070707070707070707070707070701") + val blindingKey = PublicKey.fromHex("02989c0b76cb563971fdc9bef31ec06c3560f3249d6ee9e5d83c57625596e05f6f") + assertEquals(blindingKey, blindingSecret.publicKey()) + val sharedSecret = ByteVector32.fromValidHex("8c0f7716da996c4913d720dbf691b559a4945bf70cdd18e0b61e3e42635efc9c") + assertEquals(sharedSecret, Sphinx.computeSharedSecret(carol.publicKey(), blindingSecret)) + assertEquals(sharedSecret, Sphinx.computeSharedSecret(blindingKey, carol)) + assertEquals("02afb2187075c8af51488242194b44c02624785ccd6fd43b5796c68f3025bf88", Sphinx.mac("blinded_node_id".encodeToByteArray().toByteVector(), sharedSecret).toHex()) + val blindedCarol = PublicKey.fromHex("02f4f524562868a09d5f54fb956ade3fa51ef071d64d923e395cc6db5e290ec67b") + val blindedPayload = RouteBlindingEncryptedData(TlvStream(RouteBlindingEncryptedDataTlv.Padding(ByteVector.fromHex("0000000000000000000000000000000000000000000000000000000000000000000000")), RouteBlindingEncryptedDataTlv.OutgoingNodeId(EncodedNodeId(dave.publicKey())))) + val encodedBlindedPayload = blindedPayload.write().toByteVector() + assertEquals("012300000000000000000000000000000000000000000000000000000000000000000000000421032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991", encodedBlindedPayload.toHex()) + val blindedRoute = RouteBlinding.create(blindingSecret, listOf(carol.publicKey()), listOf(encodedBlindedPayload)) + assertEquals(blindedCarol, blindedRoute.blindedNodes.first().blindedPublicKey) + assertEquals("cc3b918cda6b1b049bdbe469c4dd952935e7c1518dd9c7ed0cd2cd5bc2742b82", Crypto.sha256(blindingKey.value + sharedSecret).toHex()) + assertEquals("8285acbceb37dfb38b877a888900539be656233cd74a55c55344fb068f9d8da365340d21db96fb41b76123207daeafdfb1f571e3fea07a22e10da35f03109a0380b3c69fcbed9c698086671809658761cf65ecbc3c07a2e5", blindedRoute.blindedNodes.first().encryptedPayload.toHex()) + val decryptedPayload = RouteBlindingEncryptedData.read(RouteBlinding.decryptPayload(carol, blindingKey, blindedRoute.blindedNodes.first().encryptedPayload).first.toByteArray()) + assertEquals(blindedPayload, decryptedPayload) + } + + @Test + fun `build message with existing route`() { + val sessionKey = randomKey() + val blindingSecret = randomKey() + val blindingOverride = randomKey() + val destination = randomKey() + val replyPath = buildRoute(blindingOverride, listOf(IntermediateNode(destination.publicKey())), Recipient(destination.publicKey(), pathId = ByteVector.fromHex("01234567"))) + assertEquals(blindingOverride.publicKey(), replyPath.blindingKey) + assertEquals(EncodedNodeId(destination.publicKey()), replyPath.introductionNodeId) + val message = buildMessage(sessionKey, blindingSecret, listOf(), BlindedPath(replyPath), TlvStream.empty()).right!! + assertEquals(blindingOverride.publicKey(), message.blindingKey) // blindingSecret was not used as the replyPath was used as is + + val (nextNodeId, message2) = relayMessage(destination, message) + assertEquals(Either.Right(EncodedNodeId(destination.publicKey())), nextNodeId) + val decrypted = decryptMessage(destination, message2, logger)!! + assertNotNull(decrypted) + assertEquals("01234567", decrypted.pathId.toHex()) + } + + @Test + fun `very large multi-hop onion message`() { + val alice = randomKey() + val bob = randomKey() + val carol = randomKey() + val sessionKey = randomKey() + val blindingSecret = randomKey() + val pathId = randomBytes(65201).toByteVector() + val messageForAlice = buildMessage(sessionKey, blindingSecret, listOf(IntermediateNode(alice.publicKey()), IntermediateNode(bob.publicKey())), Recipient(carol.publicKey(), pathId), TlvStream.empty()).right!! + // This message should use the maximum size allowed for lightning messages, without overflowing it. + // Note that we leave 2 bytes for the message length, resulting in a total packet of 65535 bytes. + assertEquals(65533, messageForAlice.write().size) + + // The onion is relayed properly: + val (nextNodeId1, onionForBob) = relayMessage(alice, messageForAlice) + assertEquals(Either.Right(EncodedNodeId(bob.publicKey())), nextNodeId1) + val (nextNodeId2, onionForCarol) = relayMessage(bob, onionForBob) + assertEquals(Either.Right(EncodedNodeId(carol.publicKey())), nextNodeId2) + val decrypted = decryptMessage(carol, onionForCarol, logger)!! + assertNotNull(decrypted) + assertEquals(pathId, decrypted.pathId) + } + + @Test + fun `too large multi-hop onion message`() { + val alice = randomKey() + val bob = randomKey() + val carol = randomKey() + val sessionKey = randomKey() + val blindingSecret = randomKey() + val pathId = randomBytes(65202).toByteVector() + assertEquals( + Either.Left(OnionMessages.MessageTooLarge(65433)), + buildMessage(sessionKey, blindingSecret, listOf(IntermediateNode(alice.publicKey()), IntermediateNode(bob.publicKey())), Recipient(carol.publicKey(), pathId), TlvStream.empty()) + ) + } + + @Test + fun `route with channel ids`() { + val alice = randomKey() + val alice2bob = ShortChannelId(1) + val bob = randomKey() + val bob2carol = ShortChannelId(2) + val carol = randomKey() + val sessionKey = randomKey() + val blindingSecret = randomKey() + val pathId = randomBytes(64).toByteVector() + val messageForAlice = buildMessage(sessionKey, blindingSecret, listOf(IntermediateNode(alice.publicKey(), alice2bob), IntermediateNode(bob.publicKey(), bob2carol)), Recipient(carol.publicKey(), pathId), TlvStream.empty()).right!! + + // The onion is relayed properly: + val (outgoingChannelId1, onionForBob) = relayMessage(alice, messageForAlice) + assertEquals(Either.Left(alice2bob), outgoingChannelId1) + val (outgoingChannelId2, onionForCarol) = relayMessage(bob, onionForBob) + assertEquals(Either.Left(bob2carol), outgoingChannelId2) + val decrypted = decryptMessage(carol, onionForCarol, logger)!! + assertNotNull(decrypted) + assertEquals(pathId, decrypted.pathId) + } + +} \ No newline at end of file diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt index 2357d8e35..492464105 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/Bolt12InvoiceTestsCommon.kt @@ -67,13 +67,11 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { sessionKey: PrivateKey = randomKey(), pathId: ByteVector = randomBytes32() ): PaymentBlindedContactInfo { - val selfPayload = RouteBlindingEncryptedData.tlvSerializer.write( - TlvStream( - RouteBlindingEncryptedDataTlv.PathId(pathId), - RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(1234567), 0.msat), - RouteBlindingEncryptedDataTlv.AllowedFeatures(Features.empty) - ) - ).toByteVector() + val selfPayload = RouteBlindingEncryptedData(TlvStream( + RouteBlindingEncryptedDataTlv.PathId(pathId), + RouteBlindingEncryptedDataTlv.PaymentConstraints(CltvExpiry(1234567), 0.msat), + RouteBlindingEncryptedDataTlv.AllowedFeatures(Features.empty) + )).write().toByteVector() return PaymentBlindedContactInfo( ContactInfo.BlindedPath( RouteBlinding.create( @@ -90,7 +88,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { val nodeKey = randomKey() val payerKey = randomKey() val chain = BlockHash(randomBytes32()) - val offer = Offer(10000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) + val offer = Offer.createInternal(10000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) val request = InvoiceRequest(offer, 11000.msat, 1, Features.empty, payerKey, chain) val invoice = Bolt12Invoice( request, @@ -127,7 +125,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { val nodeKey = randomKey() val payerKey = randomKey() val chain = BlockHash(randomBytes32()) - val offer = Offer(10000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) + val offer = Offer.createInternal(10000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) val basicRequest = InvoiceRequest(offer, 11000.msat, 1, Features.empty, payerKey, chain) val requestWithUnknownTlv = basicRequest.copy(records = TlvStream(basicRequest.records.records, setOf(GenericTlv(87, ByteVector.fromHex("0404"))))) val invoice = Bolt12Invoice( @@ -149,7 +147,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { val nodeKey = randomKey() val payerKey = randomKey() val chain = BlockHash(randomBytes32()) - val offer = Offer(10000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) + val offer = Offer.createInternal(10000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) val request = InvoiceRequest(offer, 11000.msat, 1, Features.empty, payerKey, chain) val invoice = Bolt12Invoice( request, @@ -195,7 +193,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { val nodeKey = randomKey() val payerKey = randomKey() val chain = BlockHash(randomBytes32()) - val offer = Offer(15000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) + val offer = Offer.createInternal(15000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) val request = InvoiceRequest(offer, 15000.msat, 1, Features.empty, payerKey, chain) assertTrue(request.quantity_opt == null) // when paying for a single item, the quantity field must not be present val invoice = Bolt12Invoice( @@ -277,7 +275,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { val nodeKey = randomKey() val payerKey = randomKey() val chain = BlockHash(randomBytes32()) - val offer = Offer(5000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) + val offer = Offer.createInternal(5000.msat, "test offer", nodeKey.publicKey(), Features.empty, chain) val request = InvoiceRequest(offer, 5000.msat, 1, Features.empty, payerKey, chain) val invoice = Bolt12Invoice( request, @@ -414,7 +412,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { val payerKey = PrivateKey.fromHex("d817e8896c67d0bcabfdb93da7eb7fc698c829a181f994dd0ad866a8eda745e8") assertEquals(payerKey.publicKey(), PublicKey.fromHex("031ef4439f638914de79220483dda32dfb7a431e799a5ce5a7643fbd70b2118e4e")) val preimage = ByteVector32.fromValidHex("317d1fd8fec5f3ea23044983c2ba2a8043395b2a0790a815c9b12719aa5f1516") - val offer = Offer(null, "minimal tip", nodeKey.publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) + val offer = Offer.createInternal(null, "minimal tip", nodeKey.publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) val encodedOffer = "lno1pg9k66twd9kkzmpqw35hq93pqf8l2vtlq5w87m4vqfnvtn82adk9wadfgratnp2wg7l7ha4u0gzqw" assertEquals(offer.toString(), encodedOffer) assertEquals(Offer.decode(encodedOffer).get(), offer) @@ -452,7 +450,7 @@ class Bolt12InvoiceTestsCommon : LightningTestSuite() { val payerKey = PrivateKey.fromHex("0e00a9ef505292f90a0e8a7aa99d31750e885c42a3ef8866dd2bf97919aa3891") assertEquals(payerKey.publicKey(), PublicKey.fromHex("033e94f2afd568d128f02ece844ad4a0a1ddf2a4e3a08beb2dba11b3f1134b0517")) val preimage = ByteVector32.fromValidHex("09ad5e952ec39d45461ebdeceac206fb45574ae9054b5a454dd02c65f5ba1b7c") - val offer = Offer(456000000.msat, "minimal offer", nodeKey.publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) + val offer = Offer.createInternal(456000000.msat, "minimal offer", nodeKey.publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) val encodedOffer = "lno1pqzpktszqq9q6mtfde5k6ctvyphkven9wgtzzq7y3tyhuz0newawkdds924x6pet2aexssdrf5je2g2het9xpgw275" assertEquals(offer.toString(), encodedOffer) assertEquals(Offer.decode(encodedOffer).get(), offer) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt index 2fd74682d..97d3b64e6 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/payment/PaymentPacketTestsCommon.kt @@ -366,11 +366,11 @@ class PaymentPacketTestsCommon : LightningTestSuite() { // / \ // a -> b -> c d -> e val features = Features(Feature.BasicMultiPartPayment to FeatureSupport.Optional) - val offer = OfferTypes.Offer(finalAmount, "test offer", e, features, Block.LivenetGenesisBlock.hash) + val offer = OfferTypes.Offer.createInternal(finalAmount, "test offer", e, features, Block.LivenetGenesisBlock.hash) val payerKey = randomKey() val request = OfferTypes.InvoiceRequest(offer, finalAmount, 1, features, payerKey, Block.LivenetGenesisBlock.hash) val blindedRoute = RouteBlinding.create(randomKey(), listOf(randomKey().publicKey()), listOf(randomBytes(40).toByteVector())) - val paymentInfo = OfferTypes.PaymentInfo(channelUpdateDE.feeBaseMsat, channelUpdateDE.feeProportionalMillionths.toInt(), channelUpdateDE.cltvExpiryDelta, channelUpdateDE.htlcMinimumMsat, channelUpdateDE.htlcMaximumMsat!!, Features.empty) + val paymentInfo = OfferTypes.PaymentInfo(channelUpdateDE.feeBaseMsat, channelUpdateDE.feeProportionalMillionths, channelUpdateDE.cltvExpiryDelta, channelUpdateDE.htlcMinimumMsat, channelUpdateDE.htlcMaximumMsat!!, Features.empty) val path = Bolt12Invoice.Companion.PaymentBlindedContactInfo(OfferTypes.ContactInfo.BlindedPath(blindedRoute), paymentInfo) val invoice = Bolt12Invoice(request, paymentPreimage, privE, 600, features, listOf(path)) diff --git a/src/commonTest/kotlin/fr/acinq/lightning/wire/OfferTypesTestsCommon.kt b/src/commonTest/kotlin/fr/acinq/lightning/wire/OfferTypesTestsCommon.kt index 386ecdd7a..f268f7b69 100644 --- a/src/commonTest/kotlin/fr/acinq/lightning/wire/OfferTypesTestsCommon.kt +++ b/src/commonTest/kotlin/fr/acinq/lightning/wire/OfferTypesTestsCommon.kt @@ -9,7 +9,10 @@ import fr.acinq.lightning.* import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.crypto.RouteBlinding +import fr.acinq.lightning.logging.MDCLogger +import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite +import fr.acinq.lightning.tests.utils.testLoggerFactory import fr.acinq.lightning.utils.msat import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.OfferTypes.ContactInfo.BlindedPath @@ -36,13 +39,14 @@ import fr.acinq.lightning.wire.OfferTypes.writePath import kotlin.test.* class OfferTypesTestsCommon : LightningTestSuite() { - val nodeKey = PrivateKey.fromHex("85d08273493e489b9330c85a3e54123874c8cd67c1bf531f4b926c9c555f8e1d") + private val nodeKey = PrivateKey.fromHex("85d08273493e489b9330c85a3e54123874c8cd67c1bf531f4b926c9c555f8e1d") val nodeId = nodeKey.publicKey() + val logger: MDCLogger = MDCLogger(testLoggerFactory.newLogger(this::class)) @Test fun `invoice request is signed`() { val sellerKey = randomKey() - val offer = Offer(100_000.msat, "test offer", sellerKey.publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) + val offer = Offer.createInternal(100_000.msat, "test offer", sellerKey.publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) val payerKey = randomKey() val request = InvoiceRequest(offer, 100_000.msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) assertTrue(request.checkSignature()) @@ -52,7 +56,8 @@ class OfferTypesTestsCommon : LightningTestSuite() { fun `minimal offer`() { val tlvs = setOf( OfferDescription("basic offer"), - OfferNodeId(nodeId)) + OfferNodeId(nodeId) + ) val offer = Offer(TlvStream(tlvs)) val encoded = "lno1pg9kyctnd93jqmmxvejhy93pqvxl9c6mjgkeaxa6a0vtxqteql688v0ywa8qqwx4j05cyskn8ncrj" assertEquals(offer, Offer.decode(encoded).get()) @@ -61,7 +66,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { assertEquals(nodeId, offer.nodeId) // Removing any TLV from the minimal offer makes it invalid. for (tlv in tlvs) { - val incomplete = TlvStream(tlvs.filterNot{it == tlv}.toSet()) + val incomplete = TlvStream(tlvs.filterNot { it == tlv }.toSet()) assertTrue(Offer.validate(incomplete).isLeft) val incompleteEncoded = Bech32.encodeBytes(Offer.hrp, Offer.tlvSerializer.write(incomplete), Bech32.Encoding.Beck32WithoutChecksum) assertTrue(Offer.decode(incompleteEncoded).isFailure) @@ -70,23 +75,26 @@ class OfferTypesTestsCommon : LightningTestSuite() { @Test fun `offer with amount and quantity`() { - val offer = Offer(TlvStream( - OfferChains(listOf(Block.TestnetGenesisBlock.hash)), - OfferAmount(50.msat), - OfferDescription("offer with quantity"), - OfferIssuer("alice@bigshop.com"), - OfferQuantityMax(0), - OfferNodeId(nodeId))) + val offer = Offer( + TlvStream( + OfferChains(listOf(Block.TestnetGenesisBlock.hash)), + OfferAmount(50.msat), + OfferDescription("offer with quantity"), + OfferIssuer("alice@bigshop.com"), + OfferQuantityMax(0), + OfferNodeId(nodeId) + ) + ) val encoded = "lno1qgsyxjtl6luzd9t3pr62xr7eemp6awnejusgf6gw45q75vcfqqqqqqqgqyeq5ym0venx2u3qwa5hg6pqw96kzmn5d968jys3v9kxjcm9gp3xjemndphhqtnrdak3gqqkyypsmuhrtwfzm85mht4a3vcp0yrlgua3u3m5uqpc6kf7nqjz6v70qwg" assertEquals(offer, Offer.decode(encoded).get()) assertEquals(50.msat, offer.amount) assertEquals("offer with quantity", offer.description) - assertEquals( nodeId, offer.nodeId) + assertEquals(nodeId, offer.nodeId) assertEquals("alice@bigshop.com", offer.issuer) assertEquals(Long.MAX_VALUE, offer.quantityMax) } - fun signInvoiceRequest(request: InvoiceRequest, key: PrivateKey): InvoiceRequest { + private fun signInvoiceRequest(request: InvoiceRequest, key: PrivateKey): InvoiceRequest { val tlvs = removeSignature(request.records) val signature = signSchnorr(InvoiceRequest.signatureTag, rootHash(tlvs), key) val signedRequest = InvoiceRequest(tlvs.copy(records = tlvs.records + Signature(signature))) @@ -96,15 +104,25 @@ class OfferTypesTestsCommon : LightningTestSuite() { @Test fun `check that invoice request matches offer`() { - val offer = Offer(2500.msat, "basic offer", randomKey().publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) + val offer = Offer.createInternal(2500.msat, "basic offer", randomKey().publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) val payerKey = randomKey() val request = InvoiceRequest(offer, 2500.msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) assertTrue(request.isValid()) assertEquals(offer, request.offer) - val biggerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { when(it) { is InvoiceRequestAmount -> InvoiceRequestAmount(3000.msat) else -> it }}.toSet())), payerKey) + val biggerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { + when (it) { + is InvoiceRequestAmount -> InvoiceRequestAmount(3000.msat) + else -> it + } + }.toSet())), payerKey) assertTrue(biggerAmount.isValid()) assertEquals(offer, biggerAmount.offer) - val lowerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { when(it) { is InvoiceRequestAmount -> InvoiceRequestAmount(2000.msat) else -> it }}.toSet())), payerKey) + val lowerAmount = signInvoiceRequest(request.copy(records = TlvStream(request.records.records.map { + when (it) { + is InvoiceRequestAmount -> InvoiceRequestAmount(2000.msat) + else -> it + } + }.toSet())), payerKey) assertFalse(lowerAmount.isValid()) val withQuantity = signInvoiceRequest(request.copy(records = TlvStream(request.records.records + InvoiceRequestQuantity(1))), payerKey) assertFalse(withQuantity.isValid()) @@ -112,7 +130,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { @Test fun `check that invoice request matches offer - with features`() { - val offer = Offer(2500.msat, "offer with features", randomKey().publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) + val offer = Offer.createInternal(2500.msat, "offer with features", randomKey().publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) val payerKey = randomKey() val request = InvoiceRequest(offer, 2500.msat, 1, Features(Feature.BasicMultiPartPayment to FeatureSupport.Optional), payerKey, Block.LivenetGenesisBlock.hash) assertTrue(request.isValid()) @@ -127,7 +145,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { @Test fun `check that invoice request matches offer - without amount`() { - val offer = Offer(null, "offer without amount", randomKey().publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) + val offer = Offer.createInternal(null, "offer without amount", randomKey().publicKey(), Features.empty, Block.LivenetGenesisBlock.hash) val payerKey = randomKey() val request = InvoiceRequest(offer, 500.msat, 1, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) assertTrue(request.isValid()) @@ -170,18 +188,25 @@ class OfferTypesTestsCommon : LightningTestSuite() { assertEquals(offer, request2.offer) val noChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.filterNot { it is InvoiceRequestChain }.toSet())), payerKey) assertFalse(noChain.isValid()) - val otherChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.map { when(it){ is InvoiceRequestChain -> InvoiceRequestChain(Block.LivenetGenesisBlock.hash) else -> it }}.toSet())), payerKey) + val otherChain = signInvoiceRequest(request1.copy(records = TlvStream(request1.records.records.map { + when (it) { + is InvoiceRequestChain -> InvoiceRequestChain(Block.LivenetGenesisBlock.hash) + else -> it + } + }.toSet())), payerKey) assertFalse(otherChain.isValid()) } @Test fun `check that invoice request matches offer - multiple items`() { - val offer = Offer(TlvStream( - OfferAmount(500.msat), - OfferDescription("offer for multiple items"), - OfferNodeId(randomKey().publicKey()), - OfferQuantityMax(10), - )) + val offer = Offer( + TlvStream( + OfferAmount(500.msat), + OfferDescription("offer for multiple items"), + OfferNodeId(randomKey().publicKey()), + OfferQuantityMax(10), + ) + ) val payerKey = randomKey() val request = InvoiceRequest(offer, 1600.msat, 3, Features.empty, payerKey, Block.LivenetGenesisBlock.hash) assertNotNull(request.records.get()) @@ -214,7 +239,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { assertEquals(payerKey.publicKey(), invoiceRequest.payerId) // Removing any TLV from the minimal invoice request makes it invalid. for (tlv in tlvs) { - val incomplete = TlvStream(tlvs.filterNot{it == tlv}.toSet()) + val incomplete = TlvStream(tlvs.filterNot { it == tlv }.toSet()) assertTrue(InvoiceRequest.validate(incomplete).isLeft) val incompleteEncoded = Bech32.encodeBytes(InvoiceRequest.hrp, InvoiceRequest.tlvSerializer.write(incomplete), Bech32.Encoding.Beck32WithoutChecksum) assertTrue(InvoiceRequest.decode(incompleteEncoded).isFailure) @@ -227,7 +252,7 @@ class OfferTypesTestsCommon : LightningTestSuite() { data class GenericTlv(val data: ByteVector, override val tag: Long) : Tlv { override fun write(out: Output) { - LightningCodecs.writeBytes(data, out) + LightningCodecs.writeBytes(data, out) } } @@ -238,51 +263,194 @@ class OfferTypesTestsCommon : LightningTestSuite() { } val genericTlvSerializer = TlvStreamSerializer( - false, (0..1000).map { i -> i.toLong() to GenericTlvReader(i.toLong()) }.toMap() + false, (0..1000).associate { i -> i.toLong() to GenericTlvReader(i.toLong()) } ) val testCases = listOf( // Official test vectors. - TestCase("010203e8", 1, ByteVector32.fromValidHex("b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93")), - TestCase("010203e8 02080000010000020003", 2, ByteVector32.fromValidHex("c3774abbf4815aa54ccaa026bff6581f01f3be5fe814c620a252534f434bc0d1")), - TestCase("010203e8 02080000010000020003 03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002", 3, ByteVector32.fromValidHex("ab2e79b1283b0b31e0b035258de23782df6b89a38cfa7237bde69aed1a658c5d")), - TestCase("0008000000000000000006035553440801640a1741204d617468656d61746963616c205472656174697365162102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661958210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", 6, ByteVector32.fromValidHex("608407c18ad9a94d9ea2bcdbe170b6c20c462a7833a197621c916f78cf18e624")), + TestCase( + "010203e8", + 1, + ByteVector32.fromValidHex("b013756c8fee86503a0b4abdab4cddeb1af5d344ca6fc2fa8b6c08938caa6f93") + ), + TestCase( + "010203e8 02080000010000020003", + 2, + ByteVector32.fromValidHex("c3774abbf4815aa54ccaa026bff6581f01f3be5fe814c620a252534f434bc0d1") + ), + TestCase( + "010203e8 02080000010000020003 03310266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c0351800000000000000010000000000000002", + 3, + ByteVector32.fromValidHex("ab2e79b1283b0b31e0b035258de23782df6b89a38cfa7237bde69aed1a658c5d") + ), + TestCase( + "0008000000000000000006035553440801640a1741204d617468656d61746963616c205472656174697365162102eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661958210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c", + 6, + ByteVector32.fromValidHex("608407c18ad9a94d9ea2bcdbe170b6c20c462a7833a197621c916f78cf18e624") + ), // Additional test vectors. - TestCase("010100", 1, ByteVector32.fromValidHex("14ffa5e1e5d861059abff167dad6e632c45483006f7d4dc4355586062a3da30d")), - TestCase("010100 020100", 2, ByteVector32.fromValidHex("ec0584e764b71cb49ebe60ce7edbab8387e42da20b6077031bd27ff345b38ff8")), - TestCase("010100 020100 030100", 3, ByteVector32.fromValidHex("cc68aea3dc863832ef6828b3da8689cce3478c934cc50a68522477506a35feb2")), - TestCase("010100 020100 030100 040100", 4, ByteVector32.fromValidHex("b531eaa1ca71956148a6756cf8f46bdf231879e6c392019877f23e56acb7b956")), - TestCase("010100 020100 030100 040100 050100", 5, ByteVector32.fromValidHex("104e383bfdcb620cd8cefa95245332e8bd32ffd8d974fffdafe1488b1f4a1fbd")), - TestCase("010100 020100 030100 040100 050100 060100", 6, ByteVector32.fromValidHex("d96f0769702cb3440abbe683d7211fd20bd152699352f09f45d2695a89d18cdc")), - TestCase("010100 020100 030100 040100 050100 060100 070100", 7, ByteVector32.fromValidHex("30b8886e306c97dbc7b730a2e99138c1ea4fdf5c2f71e2a31e434f63f5eed228")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100", 8, ByteVector32.fromValidHex("783262efe5eeef4ec96bcee8d7cf5149ea44e0c28a78f4b1cb73d6cec9a0b378")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100", 9, ByteVector32.fromValidHex("6fd20b65a0097aff2bcc70753612a296edc27933ea335bac5df2e4c724cdb43c")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100", 10, ByteVector32.fromValidHex("9a3cf7785e9c84e03d6bc7fc04226a1cb19f158a69f16684663aa710bd90a14b")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100", 11, ByteVector32.fromValidHex("ace50a04d9dc82ce123c6ac6c2449fa607054560a9a7b8229cd2d47c01b94953")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100", 12, ByteVector32.fromValidHex("1a8e85042447a10ec312b35db34d0c8722caba4aaf6a170c4506d1fdb520aa66")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100", 13, ByteVector32.fromValidHex("8c3b8d9ba90eb9a4a34c890a7a24ba6ddc873529c5fd7c95f33a5b9ba589f54b")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100", 14, ByteVector32.fromValidHex("ed9e3694bbad2fca636576cc69af4c63ad64023bfeb788fe0f40b3533b248a6a")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100", 15, ByteVector32.fromValidHex("bab201e05786ae1eae4d685b4f815134158720ba297ea0f46a9420ffe5e94b16")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100", 16, ByteVector32.fromValidHex("44438261bb64672f374d8782e92dc9616e900378ce4bd64442753722bc2a1acb")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100", 17, ByteVector32.fromValidHex("bb6fbcd5cf426ec0b7e49d9f9ccc6c15319e01f007cce8f16fa802016718b9f7")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100", 18, ByteVector32.fromValidHex("64d8639e76af096223cad2c448d68fabf751d1c6a939bc86e1015b19188202dc")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100", 19, ByteVector32.fromValidHex("bcb88f8e06886a6d422d14bc2ed4e7fc06c0ad2adeedf630a73972c5b15538ca")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100", 20, ByteVector32.fromValidHex("9deddd5f0ab909e6a161fd4b9d44ed7384ee0a7fe8d3fbb637872767eab82f1e")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100", 21, ByteVector32.fromValidHex("4a32a2325bbd1c2b5b4915c6bec6b3e3d734d956e0c123f1fa6d70f7a8609dcd")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100", 22, ByteVector32.fromValidHex("a3ec28f0f9cb64db8d96dd7b9039fbf2240438401ea992df802d7bb70b3d02af")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100", 23, ByteVector32.fromValidHex("d025f268ec4f09baf51c4b94287e76707d9353e8cab31dc586ae47742ba0b266")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100", 24, ByteVector32.fromValidHex("cd5a2086a3919d67d0617da1e6e293f115bed8d8306498ed814c6c109ad370a4")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100", 25, ByteVector32.fromValidHex("f64113810b52f4d6a55380a3d84e59e34d26c145448121c2113a023cb63de71b")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100", 26, ByteVector32.fromValidHex("b99d7332ea2db048093a7bc0aaa85f82ccfa9da2b734fc0a14b79c5dac5a3a1c")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100", 27, ByteVector32.fromValidHex("fab01a3ce6e878942dc5c9c862cb18e88202d50e6026d2266748f7eda5f9db7f")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100", 28, ByteVector32.fromValidHex("2dc8b24a0e142d1ed36a144ed35ef0d4b7d0d1b51e198b2282248e45ebaf0417")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100", 29, ByteVector32.fromValidHex("3693a858cc97762d69d05b2191d3e5254c29ddb5abac5b9fe52b227fa216aa4c")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100", 30, ByteVector32.fromValidHex("db8787d4509265e764e60b7a81cf38efb9d3a7910d67c4ae68a1232436e1cd3b")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100", 31, ByteVector32.fromValidHex("af49f35e5b2565cb229f342405783d330c56031f005a4a6ca01f87e5637d4614")), - TestCase("010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100 200100", 32, ByteVector32.fromValidHex("2e9f8a8542576197650f61c882625f0f6838f962f9fa24ce809b687784a8a7de")), + TestCase( + "010100", + 1, + ByteVector32.fromValidHex("14ffa5e1e5d861059abff167dad6e632c45483006f7d4dc4355586062a3da30d") + ), + TestCase( + "010100 020100", + 2, + ByteVector32.fromValidHex("ec0584e764b71cb49ebe60ce7edbab8387e42da20b6077031bd27ff345b38ff8") + ), + TestCase( + "010100 020100 030100", + 3, + ByteVector32.fromValidHex("cc68aea3dc863832ef6828b3da8689cce3478c934cc50a68522477506a35feb2") + ), + TestCase( + "010100 020100 030100 040100", + 4, + ByteVector32.fromValidHex("b531eaa1ca71956148a6756cf8f46bdf231879e6c392019877f23e56acb7b956") + ), + TestCase( + "010100 020100 030100 040100 050100", + 5, + ByteVector32.fromValidHex("104e383bfdcb620cd8cefa95245332e8bd32ffd8d974fffdafe1488b1f4a1fbd") + ), + TestCase( + "010100 020100 030100 040100 050100 060100", + 6, + ByteVector32.fromValidHex("d96f0769702cb3440abbe683d7211fd20bd152699352f09f45d2695a89d18cdc") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100", + 7, + ByteVector32.fromValidHex("30b8886e306c97dbc7b730a2e99138c1ea4fdf5c2f71e2a31e434f63f5eed228") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100", + 8, + ByteVector32.fromValidHex("783262efe5eeef4ec96bcee8d7cf5149ea44e0c28a78f4b1cb73d6cec9a0b378") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100", + 9, + ByteVector32.fromValidHex("6fd20b65a0097aff2bcc70753612a296edc27933ea335bac5df2e4c724cdb43c") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100", + 10, + ByteVector32.fromValidHex("9a3cf7785e9c84e03d6bc7fc04226a1cb19f158a69f16684663aa710bd90a14b") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100", + 11, + ByteVector32.fromValidHex("ace50a04d9dc82ce123c6ac6c2449fa607054560a9a7b8229cd2d47c01b94953") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100", + 12, + ByteVector32.fromValidHex("1a8e85042447a10ec312b35db34d0c8722caba4aaf6a170c4506d1fdb520aa66") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100", + 13, + ByteVector32.fromValidHex("8c3b8d9ba90eb9a4a34c890a7a24ba6ddc873529c5fd7c95f33a5b9ba589f54b") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100", + 14, + ByteVector32.fromValidHex("ed9e3694bbad2fca636576cc69af4c63ad64023bfeb788fe0f40b3533b248a6a") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100", + 15, + ByteVector32.fromValidHex("bab201e05786ae1eae4d685b4f815134158720ba297ea0f46a9420ffe5e94b16") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100", + 16, + ByteVector32.fromValidHex("44438261bb64672f374d8782e92dc9616e900378ce4bd64442753722bc2a1acb") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100", + 17, + ByteVector32.fromValidHex("bb6fbcd5cf426ec0b7e49d9f9ccc6c15319e01f007cce8f16fa802016718b9f7") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100", + 18, + ByteVector32.fromValidHex("64d8639e76af096223cad2c448d68fabf751d1c6a939bc86e1015b19188202dc") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100", + 19, + ByteVector32.fromValidHex("bcb88f8e06886a6d422d14bc2ed4e7fc06c0ad2adeedf630a73972c5b15538ca") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100", + 20, + ByteVector32.fromValidHex("9deddd5f0ab909e6a161fd4b9d44ed7384ee0a7fe8d3fbb637872767eab82f1e") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100", + 21, + ByteVector32.fromValidHex("4a32a2325bbd1c2b5b4915c6bec6b3e3d734d956e0c123f1fa6d70f7a8609dcd") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100", + 22, + ByteVector32.fromValidHex("a3ec28f0f9cb64db8d96dd7b9039fbf2240438401ea992df802d7bb70b3d02af") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100", + 23, + ByteVector32.fromValidHex("d025f268ec4f09baf51c4b94287e76707d9353e8cab31dc586ae47742ba0b266") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100", + 24, + ByteVector32.fromValidHex("cd5a2086a3919d67d0617da1e6e293f115bed8d8306498ed814c6c109ad370a4") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100", + 25, + ByteVector32.fromValidHex("f64113810b52f4d6a55380a3d84e59e34d26c145448121c2113a023cb63de71b") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100", + 26, + ByteVector32.fromValidHex("b99d7332ea2db048093a7bc0aaa85f82ccfa9da2b734fc0a14b79c5dac5a3a1c") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100", + 27, + ByteVector32.fromValidHex("fab01a3ce6e878942dc5c9c862cb18e88202d50e6026d2266748f7eda5f9db7f") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100", + 28, + ByteVector32.fromValidHex("2dc8b24a0e142d1ed36a144ed35ef0d4b7d0d1b51e198b2282248e45ebaf0417") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100", + 29, + ByteVector32.fromValidHex("3693a858cc97762d69d05b2191d3e5254c29ddb5abac5b9fe52b227fa216aa4c") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100", + 30, + ByteVector32.fromValidHex("db8787d4509265e764e60b7a81cf38efb9d3a7910d67c4ae68a1232436e1cd3b") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100", + 31, + ByteVector32.fromValidHex("af49f35e5b2565cb229f342405783d330c56031f005a4a6ca01f87e5637d4614") + ), + TestCase( + "010100 020100 030100 040100 050100 060100 070100 080100 090100 0a0100 0b0100 0c0100 0d0100 0e0100 0f0100 100100 110100 120100 130100 140100 150100 160100 170100 180100 190100 1a0100 1b0100 1c0100 1d0100 1e0100 1f0100 200100", + 32, + ByteVector32.fromValidHex("2e9f8a8542576197650f61c882625f0f6838f962f9fa24ce809b687784a8a7de") + ), ) - testCases.forEach { - (tlvStream, tlvCount, expectedRoot) -> + testCases.forEach { (tlvStream, tlvCount, expectedRoot) -> val tlvs = genericTlvSerializer.read(ByteVector.fromHex(tlvStream).toByteArray()) assertEquals(tlvCount, tlvs.records.size) val root = rootHash(tlvs) @@ -295,22 +463,61 @@ class OfferTypesTestsCommon : LightningTestSuite() { data class TestCase(val encoded: ByteVector, val decoded: BlindedPath) val testCases = listOf( - TestCase(ByteVector.fromHex("00 00000000000004d2 0379b470d00b78ded936f8972a0f3ecda2bb6e6df40dcd581dbaeb3742b30008ff 01 02fba71b72623187dd24670110eec870e28b848f255ba2edc0486d3a8e89ec44b7 0002 1dea"), - BlindedPath(RouteBlinding.BlindedRoute(EncodedNodeId.ShortChannelIdDir(isNode1 = true, ShortChannelId(1234)), PublicKey.fromHex("0379b470d00b78ded936f8972a0f3ecda2bb6e6df40dcd581dbaeb3742b30008ff"), listOf(RouteBlinding.BlindedNode(PublicKey.fromHex("02fba71b72623187dd24670110eec870e28b848f255ba2edc0486d3a8e89ec44b7"), ByteVector.fromHex("1dea")))))), - TestCase(ByteVector.fromHex("01 000000000000ddd5 0353a081bb02d6e361be3df3e92b41b788ca65667f6ea0c01e2bfa03664460ef86 01 03bce3f0cdb4172caac82ec8a9251eb35df1201bdcb977c5a03f3624ec4156a65f 0003 c0ffee"), - BlindedPath(RouteBlinding.BlindedRoute(EncodedNodeId.ShortChannelIdDir(isNode1 = false, ShortChannelId(56789)), PublicKey.fromHex("0353a081bb02d6e361be3df3e92b41b788ca65667f6ea0c01e2bfa03664460ef86"), listOf(RouteBlinding.BlindedNode(PublicKey.fromHex("03bce3f0cdb4172caac82ec8a9251eb35df1201bdcb977c5a03f3624ec4156a65f"), ByteVector.fromHex("c0ffee")))))), - TestCase(ByteVector.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73 0379a3b6e4bceb7519d09db776994b1f82cf6a9fa4d3ec2e52314c5938f2f9f966 01 02b446aaa523df82a992ab468e5298eabb6168e2c466455c210d8c97dbb8981328 0002 cafe"), - BlindedPath(RouteBlinding.BlindedRoute(EncodedNodeId.Plain(PublicKey.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73")), PublicKey.fromHex("0379a3b6e4bceb7519d09db776994b1f82cf6a9fa4d3ec2e52314c5938f2f9f966"), listOf(RouteBlinding.BlindedNode(PublicKey.fromHex("02b446aaa523df82a992ab468e5298eabb6168e2c466455c210d8c97dbb8981328"), ByteVector.fromHex("cafe")))))), - TestCase(ByteVector.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922 028aa5d1a10463d598a0a0ab7296af21619049f94fe03ef664a87561009e58c3dd 01 02988d7381d0434cfebbe521031505fb9987ae6cefd0bab0e5927852eb96bb6cc2 0003 ec1a13"), - BlindedPath(RouteBlinding.BlindedRoute(EncodedNodeId.Plain(PublicKey.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922")), PublicKey.fromHex("028aa5d1a10463d598a0a0ab7296af21619049f94fe03ef664a87561009e58c3dd"), listOf(RouteBlinding.BlindedNode(PublicKey.fromHex("02988d7381d0434cfebbe521031505fb9987ae6cefd0bab0e5927852eb96bb6cc2"), ByteVector.fromHex("ec1a13")))))), + TestCase( + ByteVector.fromHex("00 00000000000004d2 0379b470d00b78ded936f8972a0f3ecda2bb6e6df40dcd581dbaeb3742b30008ff 01 02fba71b72623187dd24670110eec870e28b848f255ba2edc0486d3a8e89ec44b7 0002 1dea"), + BlindedPath( + RouteBlinding.BlindedRoute( + EncodedNodeId.ShortChannelIdDir(isNode1 = true, ShortChannelId(1234)), + PublicKey.fromHex("0379b470d00b78ded936f8972a0f3ecda2bb6e6df40dcd581dbaeb3742b30008ff"), + listOf(RouteBlinding.BlindedNode(PublicKey.fromHex("02fba71b72623187dd24670110eec870e28b848f255ba2edc0486d3a8e89ec44b7"), ByteVector.fromHex("1dea"))) + ) + ) + ), + TestCase( + ByteVector.fromHex("01 000000000000ddd5 0353a081bb02d6e361be3df3e92b41b788ca65667f6ea0c01e2bfa03664460ef86 01 03bce3f0cdb4172caac82ec8a9251eb35df1201bdcb977c5a03f3624ec4156a65f 0003 c0ffee"), + BlindedPath( + RouteBlinding.BlindedRoute( + EncodedNodeId.ShortChannelIdDir(isNode1 = false, ShortChannelId(56789)), + PublicKey.fromHex("0353a081bb02d6e361be3df3e92b41b788ca65667f6ea0c01e2bfa03664460ef86"), + listOf(RouteBlinding.BlindedNode(PublicKey.fromHex("03bce3f0cdb4172caac82ec8a9251eb35df1201bdcb977c5a03f3624ec4156a65f"), ByteVector.fromHex("c0ffee"))) + ) + ) + ), + TestCase( + ByteVector.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73 0379a3b6e4bceb7519d09db776994b1f82cf6a9fa4d3ec2e52314c5938f2f9f966 01 02b446aaa523df82a992ab468e5298eabb6168e2c466455c210d8c97dbb8981328 0002 cafe"), + BlindedPath( + RouteBlinding.BlindedRoute( + EncodedNodeId.Plain(PublicKey.fromHex("022d3b15cea00ee4a8e710b082bef18f0f3409cc4e7aff41c26eb0a4d3ab20dd73")), + PublicKey.fromHex("0379a3b6e4bceb7519d09db776994b1f82cf6a9fa4d3ec2e52314c5938f2f9f966"), + listOf(RouteBlinding.BlindedNode(PublicKey.fromHex("02b446aaa523df82a992ab468e5298eabb6168e2c466455c210d8c97dbb8981328"), ByteVector.fromHex("cafe"))) + ) + ) + ), + TestCase( + ByteVector.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922 028aa5d1a10463d598a0a0ab7296af21619049f94fe03ef664a87561009e58c3dd 01 02988d7381d0434cfebbe521031505fb9987ae6cefd0bab0e5927852eb96bb6cc2 0003 ec1a13"), + BlindedPath( + RouteBlinding.BlindedRoute( + EncodedNodeId.Plain(PublicKey.fromHex("03ba3c458e3299eb19d2e07ae86453f4290bcdf8689707f0862f35194397c45922")), + PublicKey.fromHex("028aa5d1a10463d598a0a0ab7296af21619049f94fe03ef664a87561009e58c3dd"), + listOf(RouteBlinding.BlindedNode(PublicKey.fromHex("02988d7381d0434cfebbe521031505fb9987ae6cefd0bab0e5927852eb96bb6cc2"), ByteVector.fromHex("ec1a13"))) + ) + ) + ), ) - testCases.forEach { - (encoded, decoded) -> + testCases.forEach { (encoded, decoded) -> val out = ByteArrayOutput() writePath(decoded, out) assertEquals(encoded, out.toByteArray().toByteVector()) assertEquals(decoded, readPath(ByteArrayInput(encoded.toByteArray()))) } } + + @Test + fun `generate deterministic blinded offer through trampoline node`() { + val pathId = ByteVector32.fromValidHex("8fe8758518872aa45287e18e613326bccc6d72e5bc4049b0353137bc6d83320a") + val offer = Offer.createBlindedOffer(amount = null, "default offer", TestConstants.Alice.nodeParams, TestConstants.Alice.walletParams.trampolineNode, pathId) + val expectedOffer = Offer.decode("lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrc2p4jx2enpw4k8ggr0venx2usvqvpqqqqs65pk9vv6swfs8zd5g697gqcga7elx54jx9p2uf0x4wsyvk5zyru4kpszvhkjfgd788sjgf5y6dqyvdq9s7lu68v97ad96cvsmzg99sgmcu0qyq6q20hxu4sp9gddmd0x7waap9wux94cm0246dxrjjw60qcparljtsqp5elqhdxerpqcfcup9ntxvrnpl50n226m7sm2n9jpvmqrfcnce7mdygk7wnhyl6y84nfypplcm3v25smd40lcjyemhvnvp2eqqv3ceeyp46we7d6vlfxfqggczrg55qj89nhaqzt8ymhddf2gmpcjz99dkszxp0kkupcf0dpnwpwsm52klvckyyp5ufuvldkjyt08fmj0azr6e5jqsludck92gdk6hlufzvamkfkq4vs").get() + assertEquals(expectedOffer, offer) + } } \ No newline at end of file