Skip to content

Commit

Permalink
Add blinded payment onion tests (#632)
Browse files Browse the repository at this point in the history
And fix creation of trampoline-to-blinded-path payments.
  • Loading branch information
t-bast authored Apr 22, 2024
1 parent 9bd8045 commit fd2e313
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 140 deletions.
13 changes: 11 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/crypto/RouteBlinding.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ object RouteBlinding {
val encryptedPayloads: List<ByteVector> = blindedNodes.map { it.encryptedPayload }
}

/**
* @param route blinded route.
* @param lastBlinding blinding point for the last node, which can be used to derive the blinded private key.
*/
data class BlindedRouteDetails(val route: BlindedRoute, val lastBlinding: PublicKey) {
/** @param nodeKey private key associated with our non-blinded node_id. */
fun blindedPrivateKey(nodeKey: PrivateKey): PrivateKey = derivePrivateKey(nodeKey, lastBlinding)
}

/**
* Blind the provided route and encrypt intermediate nodes' payloads.
*
Expand All @@ -61,7 +70,7 @@ object RouteBlinding {
* @param payloads payloads that should be encrypted for each node on the route.
* @return a blinded route.
*/
fun create(sessionKey: PrivateKey, publicKeys: List<PublicKey>, payloads: List<ByteVector>): BlindedRoute {
fun create(sessionKey: PrivateKey, publicKeys: List<PublicKey>, payloads: List<ByteVector>): BlindedRouteDetails {
require(publicKeys.size == payloads.size) { "a payload must be provided for each node in the blinded path" }
var e = sessionKey
val (blindedHops, blindingKeys) = publicKeys.zip(payloads).map { pair ->
Expand All @@ -79,7 +88,7 @@ object RouteBlinding {
e *= PrivateKey(Crypto.sha256(blindingKey.value.toByteArray() + sharedSecret.toByteArray()))
Pair(BlindedNode(blindedPublicKey, ByteVector(encryptedPayload + mac)), blindingKey)
}.unzip()
return BlindedRoute(EncodedNodeId(publicKeys.first()), blindingKeys.first(), blindedHops)
return BlindedRouteDetails(BlindedRoute(EncodedNodeId(publicKeys.first()), blindingKeys.first(), blindedHops), blindingKeys.last())
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ object OnionMessages {
blindingSecret,
intermediateNodes.map { it.nodeId } + destination.nodeId,
intermediatePayloads + lastPayload
)
).route
}
is Destination.BlindedPath -> when {
intermediateNodes.isEmpty() -> destination.route
Expand All @@ -85,7 +85,7 @@ object OnionMessages {
blindingSecret,
intermediateNodes.map { it.nodeId },
intermediatePayloads
)
).route
RouteBlinding.BlindedRoute(
routePrefix.introductionNodeId,
routePrefix.blindingKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,15 +343,14 @@ class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: Walle
NodeHop(walletParams.trampolineNode.id, request.recipient, fees.cltvExpiryDelta, fees.calculateFees(request.amount))
)
}

when (request.paymentRequest) {
is Bolt11Invoice -> {
val minFinalExpiryDelta = request.paymentRequest.minFinalExpiryDelta ?: Channel.MIN_CLTV_EXPIRY_DELTA
val finalExpiry = nodeParams.paymentRecipientExpiryParams.computeFinalExpiry(currentBlockHeight, minFinalExpiryDelta)
val finalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(request.amount, finalExpiry, request.paymentRequest.paymentSecret, request.paymentRequest.paymentMetadata)

val invoiceFeatures = request.paymentRequest.features
val (trampolineAmount, trampolineExpiry, trampolineOnion) = if (invoiceFeatures.hasFeature(Feature.TrampolinePayment) || invoiceFeatures.hasFeature(Feature.ExperimentalTrampolinePayment)) {
// We may be paying an older version of lightning-kmp that only supports trampoline packets of size 400.
OutgoingPaymentPacket.buildPacket(request.paymentHash, trampolineRoute, finalPayload, 400)
} else {
OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(request.paymentRequest, trampolineRoute, finalPayload)
Expand All @@ -360,8 +359,7 @@ class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: Walle
}
is Bolt12Invoice -> {
val finalExpiry = nodeParams.paymentRecipientExpiryParams.computeFinalExpiry(currentBlockHeight, CltvExpiryDelta(0))
val dummyFinalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(request.amount, finalExpiry, ByteVector32.Zeroes, null)
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(request.paymentRequest, trampolineRoute, dummyFinalPayload)
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(request.paymentRequest, trampolineRoute.last(), request.amount, finalExpiry)
return Triple(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,35 +60,48 @@ object OutgoingPaymentPacket {
* Build an encrypted trampoline onion packet when the final recipient doesn't support trampoline.
* The next-to-last trampoline node payload will contain instructions to convert to a legacy payment.
*
* @param invoice an invoice (features and routing hints will be provided to the next-to-last node).
* @param invoice a Bolt11 invoice (features and routing hints will be provided to the next-to-last node).
* @param hops the trampoline hops (including ourselves in the first hop, and the non-trampoline final recipient in the last hop).
* @param finalPayload payload data for the final node (amount, expiry, etc)
* @return a (firstAmount, firstExpiry, onion) triple where:
* - firstAmount is the amount for the trampoline node in the route
* - firstExpiry is the cltv expiry for the first trampoline node in the route
* - the trampoline onion to include in final payload of a normal onion
*/
fun buildTrampolineToNonTrampolinePacket(invoice: PaymentRequest, hops: List<NodeHop>, finalPayload: PaymentOnion.FinalPayload.Standard): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
fun buildTrampolineToNonTrampolinePacket(invoice: Bolt11Invoice, hops: List<NodeHop>, finalPayload: PaymentOnion.FinalPayload.Standard): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
// NB: the final payload will never reach the recipient, since the next-to-last trampoline hop will convert that to a legacy payment
// We use the smallest final payload possible, otherwise we may overflow the trampoline onion size.
val dummyFinalPayload = PaymentOnion.FinalPayload.Standard.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, null)
val (firstAmount, firstExpiry, payloads) = hops.drop(1).reversed().fold(Triple(finalPayload.amount, finalPayload.expiry, listOf<PaymentOnion.PerHopPayload>(dummyFinalPayload))) { triple, hop ->
val (amount, expiry, payloads) = triple
val payload = when (payloads.size) {
// The next-to-last trampoline hop must include invoice data to indicate the conversion to a legacy payment.
1 -> when (invoice) {
is Bolt11Invoice -> PaymentOnion.RelayToNonTrampolinePayload.create(finalPayload.amount, finalPayload.totalAmount, finalPayload.expiry, hop.nextNodeId, invoice)
is Bolt12Invoice -> PaymentOnion.RelayToBlindedPayload.create(finalPayload.amount, finalPayload.expiry, invoice)
}
1 -> PaymentOnion.RelayToNonTrampolinePayload.create(finalPayload.amount, finalPayload.totalAmount, finalPayload.expiry, hop.nextNodeId, invoice)
else -> PaymentOnion.NodeRelayPayload.create(amount, expiry, hop.nextNodeId)
}
Triple(amount + hop.fee(amount), expiry + hop.cltvExpiryDelta, listOf(payload) + payloads)
}
val nodes = hops.map { it.nextNodeId }
val onion = buildOnion(nodes, payloads, invoice.paymentHash, 400) // TODO: remove the fixed payload length once eclair supports it
val onion = buildOnion(nodes, payloads, invoice.paymentHash, payloadLength = null)
return Triple(firstAmount, firstExpiry, onion)
}

/**
* Build an encrypted trampoline onion packet when the final recipient is using a blinded path.
* The trampoline payload will contain data from the invoice to allow the trampoline node to pay the blinded path.
* We only need a single trampoline node, who will find a route to the blinded path's introduction node without learning the recipient's identity.
*
* @param invoice a Bolt12 invoice (blinded path data will be provided to the trampoline node).
* @param hop the trampoline hop from the trampoline node to the recipient.
* @param finalAmount amount that should be received by the final recipient.
* @param finalExpiry cltv expiry that should be received by the final recipient.
*/
fun buildTrampolineToNonTrampolinePacket(invoice: Bolt12Invoice, hop: NodeHop, finalAmount: MilliSatoshi, finalExpiry: CltvExpiry): Triple<MilliSatoshi, CltvExpiry, PacketAndSecrets> {
val payload = PaymentOnion.RelayToBlindedPayload.create(finalAmount, finalExpiry, invoice)
val onion = buildOnion(listOf(hop.nodeId), listOf(payload), invoice.paymentHash, payloadLength = null)
return Triple(finalAmount + hop.fee(finalAmount), finalExpiry + hop.cltvExpiryDelta, onion)
}

/**
* Build an encrypted onion packet with the given final payload.
*
Expand Down
Loading

0 comments on commit fd2e313

Please sign in to comment.