Skip to content

Commit

Permalink
Enable paying Bolt12Invoice (#606)
Browse files Browse the repository at this point in the history
Payments can now be sent to Bolt12Invoice. The trampoline onion now uses variable length to accommodate large blinded routes.
  • Loading branch information
thomash-acinq authored Feb 21, 2024
1 parent 0567020 commit fceec18
Show file tree
Hide file tree
Showing 15 changed files with 330 additions and 221 deletions.
25 changes: 14 additions & 11 deletions src/commonMain/kotlin/fr/acinq/lightning/crypto/sphinx/Sphinx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ object Sphinx {
* @param keyType type of key used (depends on the onion we're building).
* @param sharedSecrets shared secrets for all the hops.
* @param payloads payloads for all the hops.
* @param packetLength length of the onion-encrypted payload (1300 for payment onions, 400 for trampoline onions).
* @param packetLength length of the onion-encrypted payload (1300 for payment onions, variable for trampoline onions).
* @return filler bytes.
*/
fun generateFiller(keyType: String, sharedSecrets: List<ByteVector32>, payloads: List<ByteArray>, packetLength: Int): ByteArray {
Expand All @@ -138,15 +138,14 @@ object Sphinx {
* @param privateKey this node's private key.
* @param associatedData associated data.
* @param packet packet received by this node.
* @param packetLength length of the onion-encrypted payload (1300 for payment onions, 400 for trampoline onions).
* @return a DecryptedPacket(payload, packet, shared secret) object where:
* - payload is the per-hop payload for this node.
* - packet is the next packet, to be forwarded using the info that is given in the payload.
* - shared secret is the secret we share with the node that sent the packet. We need it to propagate
* failure messages upstream.
* or a BadOnion error containing the hash of the invalid onion.
*/
fun peel(privateKey: PrivateKey, associatedData: ByteVector, packet: OnionRoutingPacket, packetLength: Int): Either<FailureMessage, DecryptedPacket> = when (packet.version) {
fun peel(privateKey: PrivateKey, associatedData: ByteVector, packet: OnionRoutingPacket): Either<FailureMessage, DecryptedPacket> = when (packet.version) {
0 -> {
when (val result = runTrying {
val pub = PublicKey(packet.publicKey)
Expand All @@ -159,6 +158,7 @@ object Sphinx {
val mu = generateKey("mu", sharedSecret)
val check = mac(mu, packet.payload + associatedData)
if (check == packet.hmac) {
val packetLength = packet.payload.size()
val rho = generateKey("rho", sharedSecret)
// Since we don't know the length of the per-hop payload (we will learn it once we decode the first bytes),
// we have to pessimistically generate a long cipher stream.
Expand Down Expand Up @@ -198,19 +198,22 @@ object Sphinx {
* @param ephemeralPublicKey ephemeral key shared with the target node.
* @param sharedSecret shared secret with this hop.
* @param packet current packet or random bytes if the packet hasn't been initialized.
* @param packetLength length of the onion-encrypted payload (1300 for payment onions, 400 for trampoline onions).
* @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet.
* @return the next packet.
*/
private fun wrap(
payload: ByteArray,
associatedData: ByteVector32,
associatedData: ByteVector32?,
ephemeralPublicKey: PublicKey,
sharedSecret: ByteVector32,
packet: Either<ByteVector, OnionRoutingPacket>,
packetLength: Int,
onionPayloadFiller: ByteVector = ByteVector.empty
): OnionRoutingPacket {
val packetLength = when (packet) {
is Either.Left -> packet.value.size()
is Either.Right -> packet.value.payload.size()
}

require(payload.size <= packetLength - MacLength) { "packet payload cannot exceed ${packetLength - MacLength} bytes" }

val (currentMac, currentPayload) = when (packet) {
Expand All @@ -229,7 +232,7 @@ object Sphinx {
onionPayload2.dropLast(onionPayloadFiller.size()).toByteArray() + onionPayloadFiller.toByteArray()
}

val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload.toByteVector() + associatedData)
val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload.toByteVector() + (associatedData ?: ByteVector.empty))
return OnionRoutingPacket(0, ephemeralPublicKey.value, nextOnionPayload.toByteVector(), nextHmac)
}

Expand All @@ -240,21 +243,21 @@ object Sphinx {
* @param publicKeys node public keys (one per node).
* @param payloads payloads (one per node).
* @param associatedData associated data.
* @param packetLength length of the onion-encrypted payload (1300 for payment onions, 400 for trampoline onions).
* @param packetLength length of the onion-encrypted payload (1300 for payment onions, variable for trampoline onions).
* @return An onion packet with all shared secrets. The onion packet can be sent to the first node in the list, and
* the shared secrets (one per node) can be used to parse returned failure messages if needed.
*/
fun create(sessionKey: PrivateKey, publicKeys: List<PublicKey>, payloads: List<ByteArray>, associatedData: ByteVector32, packetLength: Int): PacketAndSecrets {
fun create(sessionKey: PrivateKey, publicKeys: List<PublicKey>, payloads: List<ByteArray>, associatedData: ByteVector32?, packetLength: Int): PacketAndSecrets {
val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys)
val filler = generateFiller("rho", sharedsecrets.dropLast(1), payloads.dropLast(1), packetLength)

// We deterministically-derive the initial payload bytes: see https://github.com/lightningnetwork/lightning-rfc/pull/697
val startingBytes = generateStream(generateKey("pad", sessionKey.value), packetLength)
val lastPacket = wrap(payloads.last(), associatedData, ephemeralPublicKeys.last(), sharedsecrets.last(), Either.Left(startingBytes.toByteVector()), packetLength, filler.toByteVector())
val lastPacket = wrap(payloads.last(), associatedData, ephemeralPublicKeys.last(), sharedsecrets.last(), Either.Left(startingBytes.toByteVector()), filler.toByteVector())

tailrec fun loop(hopPayloads: List<ByteArray>, ephKeys: List<PublicKey>, sharedSecrets: List<ByteVector32>, packet: OnionRoutingPacket): OnionRoutingPacket {
return if (hopPayloads.isEmpty()) packet else {
val nextPacket = wrap(hopPayloads.last(), associatedData, ephKeys.last(), sharedSecrets.last(), Either.Right(packet), packetLength)
val nextPacket = wrap(hopPayloads.last(), associatedData, ephKeys.last(), sharedSecrets.last(), Either.Right(packet))
loop(hopPayloads.dropLast(1), ephKeys.dropLast(1), sharedSecrets.dropLast(1), nextPacket)
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ data class LightningOutgoingPayment(
) : OutgoingPayment() {

/** Create an outgoing payment in a pending status, without any parts yet. */
constructor(id: UUID, amount: MilliSatoshi, recipient: PublicKey, invoice: Bolt11Invoice) : this(id, amount, recipient, Details.Normal(invoice), listOf(), Status.Pending)
constructor(id: UUID, amount: MilliSatoshi, recipient: PublicKey, invoice: PaymentRequest) : this(id, amount, recipient, Details.Normal(invoice), listOf(), Status.Pending)

val paymentHash: ByteVector32 = details.paymentHash

Expand Down Expand Up @@ -285,7 +285,7 @@ data class LightningOutgoingPayment(
abstract val paymentHash: ByteVector32

/** A normal lightning payment. */
data class Normal(val paymentRequest: Bolt11Invoice) : Details() {
data class Normal(val paymentRequest: PaymentRequest) : Details() {
override val paymentHash: ByteVector32 = paymentRequest.paymentHash
}

Expand Down
2 changes: 1 addition & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ data object Disconnected : PeerCommand()
sealed class PaymentCommand : PeerCommand()
private data object CheckPaymentsTimeout : PaymentCommand()
data class PayToOpenResponseCommand(val payToOpenResponse: PayToOpenResponse) : PeerCommand()
data class SendPayment(val paymentId: UUID, val amount: MilliSatoshi, val recipient: PublicKey, val paymentRequest: Bolt11Invoice, val trampolineFeesOverride: List<TrampolineFees>? = null) : PaymentCommand() {
data class SendPayment(val paymentId: UUID, val amount: MilliSatoshi, val recipient: PublicKey, val paymentRequest: PaymentRequest, val trampolineFeesOverride: List<TrampolineFees>? = null) : PaymentCommand() {
val paymentHash: ByteVector32 = paymentRequest.paymentHash
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ class IncomingPaymentHandler(val nodeParams: NodeParams, val db: IncomingPayment
* This is very similar to the processing of a htlc, except that we only have a packet, to decrypt into a final payload.
*/
private fun toPaymentPart(privateKey: PrivateKey, payToOpenRequest: PayToOpenRequest): Either<ProcessAddResult.Rejected, PayToOpenPart> {
return when (val decrypted = IncomingPaymentPacket.decryptOnion(payToOpenRequest.paymentHash, payToOpenRequest.finalPacket, payToOpenRequest.finalPacket.payload.size(), privateKey)) {
return when (val decrypted = IncomingPaymentPacket.decryptOnion(payToOpenRequest.paymentHash, payToOpenRequest.finalPacket, privateKey)) {
is Either.Left -> {
val failureMsg = decrypted.value
val action = actionForPayToOpenFailure(privateKey, failureMsg, payToOpenRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ object IncomingPaymentPacket {
* - or a Bolt4 failure message that can be returned to the sender if the HTLC is invalid
*/
fun decrypt(add: UpdateAddHtlc, privateKey: PrivateKey): Either<FailureMessage, PaymentOnion.FinalPayload> {
return when (val decrypted = decryptOnion(add.paymentHash, add.onionRoutingPacket, OnionRoutingPacket.PaymentPacketLength, privateKey)) {
return when (val decrypted = decryptOnion(add.paymentHash, add.onionRoutingPacket, privateKey)) {
is Either.Left -> Either.Left(decrypted.value)
is Either.Right -> {
val outer = decrypted.value
when (val trampolineOnion = outer.records.get<OnionPaymentPayloadTlv.TrampolineOnion>()) {
null -> validate(add, outer)
else -> {
when (val inner = decryptOnion(add.paymentHash, trampolineOnion.packet, OnionRoutingPacket.TrampolinePacketLength, privateKey)) {
when (val inner = decryptOnion(add.paymentHash, trampolineOnion.packet, privateKey)) {
is Either.Left -> Either.Left(inner.value)
is Either.Right -> validate(add, outer, inner.value)
}
Expand All @@ -36,8 +36,8 @@ object IncomingPaymentPacket {
}
}

fun decryptOnion(paymentHash: ByteVector32, packet: OnionRoutingPacket, packetLength: Int, privateKey: PrivateKey): Either<FailureMessage, PaymentOnion.FinalPayload> {
return when (val decrypted = Sphinx.peel(privateKey, paymentHash, packet, packetLength)) {
fun decryptOnion(paymentHash: ByteVector32, packet: OnionRoutingPacket, privateKey: PrivateKey): Either<FailureMessage, PaymentOnion.FinalPayload> {
return when (val decrypted = Sphinx.peel(privateKey, paymentHash, packet)) {
is Either.Left -> Either.Left(decrypted.value)
is Either.Right -> run {
if (!decrypted.value.isLastPacket) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,17 +344,27 @@ class OutgoingPaymentHandler(val nodeParams: NodeParams, val walletParams: Walle
)
}

val minFinalExpiryDelta = request.paymentRequest.minFinalExpiryDelta ?: Channel.MIN_CLTV_EXPIRY_DELTA
val finalExpiry = nodeParams.paymentRecipientExpiryParams.computeFinalExpiry(currentBlockHeight, minFinalExpiryDelta)
val finalPayload = PaymentOnion.FinalPayload.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)) {
OutgoingPaymentPacket.buildPacket(request.paymentHash, trampolineRoute, finalPayload, OnionRoutingPacket.TrampolinePacketLength)
} else {
OutgoingPaymentPacket.buildTrampolineToLegacyPacket(request.paymentRequest, trampolineRoute, finalPayload)
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.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)) {
OutgoingPaymentPacket.buildPacket(request.paymentHash, trampolineRoute, finalPayload, 400)
} else {
OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(request.paymentRequest, trampolineRoute, finalPayload)
}
return Triple(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
}
is Bolt12Invoice -> {
val finalExpiry = nodeParams.paymentRecipientExpiryParams.computeFinalExpiry(currentBlockHeight, CltvExpiryDelta(0))
val dummyFinalPayload = PaymentOnion.FinalPayload.createSinglePartPayload(request.amount, finalExpiry, ByteVector32.Zeroes, null)
val (trampolineAmount, trampolineExpiry, trampolineOnion) = OutgoingPaymentPacket.buildTrampolineToNonTrampolinePacket(request.paymentRequest, trampolineRoute, dummyFinalPayload)
return Triple(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
}
}
return Triple(trampolineAmount, trampolineExpiry, trampolineOnion.packet)
}

sealed class PaymentAttempt {
Expand Down
Loading

0 comments on commit fceec18

Please sign in to comment.