Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ object RouteBlinding {
return privateKey * PrivateKey(Sphinx.generateKey("blinded_node_id", sharedSecret))
}

data class DecryptedPayload(val data: ByteVector, val nextPathKey: PublicKey, val useCompactRoute: Boolean)

/**
* Decrypt the encrypted payload (usually found in the onion) that contains instructions to locate the next node.
*
Expand All @@ -119,12 +121,13 @@ object RouteBlinding {
fun decryptPayload(
privateKey: PrivateKey,
pathKey: PublicKey,
encryptedPayload: ByteVector
): Either<InvalidTlvPayload, Pair<ByteVector, PublicKey>> {
encryptedPayload: ByteVector,
allowCompactFormat: Boolean = false
): Either<InvalidTlvPayload, DecryptedPayload> {
val sharedSecret = Sphinx.computeSharedSecret(pathKey, privateKey)
val nextPathKey = Sphinx.blind(pathKey, Sphinx.computeBlindingFactor(pathKey, sharedSecret))
if (encryptedPayload.isEmpty()) {
return Either.Right(Pair(encryptedPayload, nextPathKey))
if (encryptedPayload.isEmpty() && allowCompactFormat) {
return Either.Right(DecryptedPayload(encryptedPayload, nextPathKey, useCompactRoute = true))
} else {
return try {
val rho = Sphinx.generateKey("rho", sharedSecret)
Expand All @@ -135,7 +138,7 @@ object RouteBlinding {
byteArrayOf(),
encryptedPayload.takeRight(16).toByteArray()
)
Either.Right(Pair(ByteVector(decrypted), nextPathKey))
Either.Right(DecryptedPayload(ByteVector(decrypted), nextPathKey, useCompactRoute = false))
} catch (_: Throwable) {
Either.Left(CannotDecodeTlv(OnionPaymentPayloadTlv.EncryptedRecipientData.tag))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ object OnionMessages {
* @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?)
data class DecryptedMessage(val content: MessageOnion, val blindedPrivateKey: PrivateKey, val pathId: ByteVector?, val useCompactRoute: Boolean)

fun decryptMessage(privateKey: PrivateKey, msg: OnionMessage, logger: MDCLogger): DecryptedMessage? {
val blindedPrivateKey = RouteBlinding.derivePrivateKey(privateKey, msg.pathKey)
Expand All @@ -166,13 +166,13 @@ object OnionMessages {
logger.warning { "ignoring onion message that couldn't be decoded: ${e.message}" }
return null
}
when (val payload = RouteBlinding.decryptPayload(privateKey, msg.pathKey, message.encryptedData)) {
when (val payload = RouteBlinding.decryptPayload(privateKey, msg.pathKey, message.encryptedData, allowCompactFormat = true)) {
is Either.Left -> {
logger.warning { "ignoring onion message that couldn't be decrypted: ${payload.value}" }
null
}
is Either.Right -> {
val (decryptedPayload, nextPathKey) = payload.value
val (decryptedPayload, nextPathKey, useCompactRoute) = payload.value
when (val relayInfo = RouteBlindingEncryptedData.read(decryptedPayload.toByteArray())) {
is Either.Left -> {
logger.warning { "ignoring onion message with invalid relay info: ${relayInfo.value}" }
Expand All @@ -184,7 +184,7 @@ object OnionMessages {
val nextMessage = OnionMessage(relayInfo.value.nextPathKeyOverride ?: nextPathKey, decrypted.value.nextPacket)
decryptMessage(privateKey, nextMessage, logger)
}
decrypted.value.isLastPacket -> DecryptedMessage(message, blindedPrivateKey, relayInfo.value.pathId)
decrypted.value.isLastPacket -> DecryptedMessage(message, blindedPrivateKey, relayInfo.value.pathId, useCompactRoute)
else -> {
logger.warning { "ignoring onion message for which we're not the destination (next_node_id=${relayInfo.value.nextNodeId}, path_id=${relayInfo.value.pathId?.toHex()})" }
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ object IncomingPaymentPacket {
}

private fun decryptRecipientData(privateKey: PrivateKey, pathKey: PublicKey, data: ByteVector, tlvs: TlvStream<OnionPaymentPayloadTlv>): Either<InvalidTlvPayload, PaymentOnion.FinalPayload.Blinded> {
return RouteBlinding.decryptPayload(privateKey, pathKey, data)
return RouteBlinding.decryptPayload(privateKey, pathKey, data, allowCompactFormat = false)
.flatMap { (decryptedRecipientData, _) -> RouteBlindingEncryptedData.read(decryptedRecipientData.toByteArray()) }
.flatMap { blindedTlvs -> PaymentOnion.FinalPayload.Blinded.validate(tlvs, blindedTlvs) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
return OnionMessages.decryptMessage(nodeParams.nodePrivateKey, msg, logger)?.let { decrypted ->
val invoiceRequestTlvs = decrypted.content.records.get<OnionMessagePayloadTlv.InvoiceRequest>()?.tlvs
when {
decrypted.useCompactRoute -> {
val cardPaymentRequestTlvs = decrypted.content.records.get<OnionMessagePayloadTlv.CardPaymentRequest>()?.tlvs
if (cardPaymentRequestTlvs != null && nodeParams.compactOfferKeys.value.contains(decrypted.blindedPrivateKey.publicKey())) {
receiveCardPaymentRequest(cardPaymentRequestTlvs)
} else {
logger.warning { "ignoring unexpected message to compact route" }
null
}
}
invoiceRequestTlvs != null -> when (val invoiceRequest = OfferTypes.InvoiceRequest.validate(invoiceRequestTlvs)) {
is Left -> {
logger.warning { "received invalid invoice_request: ${invoiceRequest.value}" }
Expand All @@ -101,6 +110,18 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
}
}

private fun receiveCardPaymentRequest(offerTlvs: TlvStream<OfferTypes.OfferTlv>): OnionMessageAction? {
return when (val offer = OfferTypes.Offer.validate(offerTlvs)) {
is Left -> {
logger.warning { "received invalid card payment request: ${offer.value}" }
null
}
is Right -> {
TODO("check authentication from the card and pay offer if we didn't exceed the daily limit")
}
}
}

private suspend fun receiveInvoiceResponse(content: MessageOnion, payOffer: PayOffer, request: OfferTypes.InvoiceRequest): OnionMessageAction.PayInvoice? {
return when (val res = Bolt12Invoice.extract(content.records)) {
is Bolt12Invoice.Companion.Bolt12ParsingResult.Failure.Malformed -> {
Expand Down Expand Up @@ -236,9 +257,6 @@ class OfferManager(val nodeParams: NodeParams, val walletParams: WalletParams, v
/** This function verifies that the offer provided was generated by us. */
private fun isOurOffer(offer: OfferTypes.Offer, pathId: ByteVector?, blindedPrivateKey: PrivateKey): Boolean = when {
pathId != null && pathId.size() != 32 -> false
// Compact offers are randomly generated, but they don't store the nonce in the path_id to save space.
// It is instead the wallet's responsibility to store the corresponding blinded public key(s).
pathId == null && nodeParams.compactOfferKeys.value.contains(blindedPrivateKey.publicKey()) -> true
else -> {
val expected = deterministicOffer(nodeParams.chainHash, nodeParams.nodePrivateKey, walletParams.trampolineNode.id, offer.amount, offer.description, pathId?.let { ByteVector32(it) })
expected == OfferTypes.OfferAndKey(offer, blindedPrivateKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ sealed class OnionMessagePayloadTlv : Tlv {
InvoiceError(tlvSerializer.read(input))
}
}

data class CardPaymentRequest(val tlvs: TlvStream<OfferTypes.OfferTlv>) : OnionMessagePayloadTlv() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robbiehanson in your last email you wrote that CardPaymentRequest includes an Invoice. I think it makes more sense to include an Offer and reuse the existing offer payment pipeline.
I'll let you add the authentication data from the card.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes more sense to include an Offer and reuse the existing offer payment pipeline.

The community does NOT want to go this route.

One of the most important benchmarks for card payments is speed. If the CardPaymentRequest directly includes an invoice, then the entire process is only 2 steps (send CardPaymentRequest, receive payment). Changing this to an offer increases the process to 4 steps, thus doubling the payment time.

Also, a lot of software is built on top of LND, and LND doesn't have full Bolt 12 support yet. So the community is asking for flexibility. Specifically, they want CardPaymentRequest to support sending either a Bolt 12 or 11 invoice.

I have a branch called card-payment which has a working proof-of-concept. But the spec is still very much a work in progress. So I was hoping to avoid dealing with the CardPaymentRequest details in this PR. Is it possible to skip including this for now, and circle back to it in a later PR ?

override val tag: Long get() = CardPaymentRequest.tag
override fun write(out: Output) = OfferTypes.Offer.tlvSerializer.write(tlvs, out)

companion object : TlvValueReader<CardPaymentRequest> {
const val tag: Long = 70

override fun read(input: Input): CardPaymentRequest =
CardPaymentRequest(OfferTypes.Offer.tlvSerializer.read(input))
}
}
}

data class MessageOnion(val records: TlvStream<OnionMessagePayloadTlv>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class OnionMessagesTestsCommon {
assertEquals(blindedAlice, blindedRoute.blindedHops.first().blindedPublicKey)
assertEquals("bae3d9ea2b06efd1b7b9b49b6cdcaad0e789474a6939ffa54ff5ec9224d5b76c", Crypto.sha256(blindingKey.value + sharedSecret).toHexString())
assertEquals("6970e870b473ddbc27e3098bfa45bb1aa54f1f637f803d957e6271d8ffeba89da2665d62123763d9b634e30714144a1c165ac9", blindedRoute.blindedHops.first().encryptedPayload.toHex())
val decryptedPayload = RouteBlindingEncryptedData.read(RouteBlinding.decryptPayload(alice, blindingKey, blindedRoute.blindedHops.first().encryptedPayload).right!!.first.toByteArray()).right!!
val decryptedPayload = RouteBlindingEncryptedData.read(RouteBlinding.decryptPayload(alice, blindingKey, blindedRoute.blindedHops.first().encryptedPayload).right!!.data.toByteArray()).right!!
assertEquals(blindedPayload, decryptedPayload)
}

Expand All @@ -176,7 +176,7 @@ class OnionMessagesTestsCommon {
assertEquals(blindedBob, blindedRoute.blindedHops.first().blindedPublicKey)
assertEquals("9afb8b2ebc174dcf9e270be24771da7796542398d29d4ff6a4e7b6b4b9205cfe", Crypto.sha256(blindingKey.value + sharedSecret).toHexString())
assertEquals("1630da85e8759b8f3b94d74a539c6f0d870a87cf03d4986175865a2985553c997b560c32613bd9184c1a6d41a37027aabdab5433009d8409a1b638eb90373778a05716af2c2140b3196dca23997cdad4cfa7a7adc8d4", blindedRoute.blindedHops.first().encryptedPayload.toHex())
val decryptedPayload = RouteBlindingEncryptedData.read(RouteBlinding.decryptPayload(bob, blindingKey, blindedRoute.blindedHops.first().encryptedPayload).right!!.first.toByteArray()).right!!
val decryptedPayload = RouteBlindingEncryptedData.read(RouteBlinding.decryptPayload(bob, blindingKey, blindedRoute.blindedHops.first().encryptedPayload).right!!.data.toByteArray()).right!!
assertEquals(blindedPayload, decryptedPayload)
assertEquals(blindingOverride, decryptedPayload.nextPathKeyOverride)
}
Expand All @@ -200,7 +200,7 @@ class OnionMessagesTestsCommon {
assertEquals(blindedCarol, blindedRoute.blindedHops.first().blindedPublicKey)
assertEquals("cc3b918cda6b1b049bdbe469c4dd952935e7c1518dd9c7ed0cd2cd5bc2742b82", Crypto.sha256(blindingKey.value + sharedSecret).toHexString())
assertEquals("8285acbceb37dfb38b877a888900539be656233cd74a55c55344fb068f9d8da365340d21db96fb41b76123207daeafdfb1f571e3fea07a22e10da35f03109a0380b3c69fcbed9c698086671809658761cf65ecbc3c07a2e5", blindedRoute.blindedHops.first().encryptedPayload.toHex())
val decryptedPayload = RouteBlindingEncryptedData.read(RouteBlinding.decryptPayload(carol, blindingKey, blindedRoute.blindedHops.first().encryptedPayload).right!!.first.toByteArray()).right!!
val decryptedPayload = RouteBlindingEncryptedData.read(RouteBlinding.decryptPayload(carol, blindingKey, blindedRoute.blindedHops.first().encryptedPayload).right!!.data.toByteArray()).right!!
assertEquals(blindedPayload, decryptedPayload)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class PaymentPacketTestsCommon : LightningTestSuite() {
val pathKey = payload.get<OnionPaymentPayloadTlv.PathKey>()?.publicKey ?: add.pathKey
assertNotNull(pathKey)
val encryptedData = payload.get<OnionPaymentPayloadTlv.EncryptedRecipientData>()!!.data
val nextPathKey = RouteBlinding.decryptPayload(privateKey, pathKey, encryptedData).map { it.second }.right
val nextPathKey = RouteBlinding.decryptPayload(privateKey, pathKey, encryptedData).map { it.nextPathKey }.right
assertNotNull(nextPathKey)
return Pair(decrypted.nextPacket, nextPathKey)
}
Expand Down