Skip to content

Commit e0263f4

Browse files
committed
Include payment metadata in all invoices
We include a payment metadata in every invoice we generate. This lets us see whether our payers support it or not, which is important data to have before we make it mandatory and use it for storage-less invoices.
1 parent f5feb72 commit e0263f4

File tree

3 files changed

+28
-4
lines changed

3 files changed

+28
-4
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,10 @@ object OutgoingPaymentPacket {
213213
* - the trampoline onion to include in final payload of a normal onion
214214
*/
215215
def buildTrampolineToLegacyPacket(invoice: PaymentRequest, hops: Seq[NodeHop], finalPayload: PaymentOnion.FinalPayload): (MilliSatoshi, CltvExpiry, Sphinx.PacketAndSecrets) = {
216-
val (firstAmount, firstExpiry, payloads) = hops.drop(1).reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PaymentOnion.PerHopPayload](finalPayload))) {
216+
// NB: the final payload will never reach the recipient, since the next-to-last trampoline hop will convert that to a legacy payment
217+
// We use the smallest final payload possible, otherwise we may overflow the trampoline onion size.
218+
val dummyFinalPayload = PaymentOnion.createSinglePartPayload(finalPayload.amount, finalPayload.expiry, finalPayload.paymentSecret, None)
219+
val (firstAmount, firstExpiry, payloads) = hops.drop(1).reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[PaymentOnion.PerHopPayload](dummyFinalPayload))) {
217220
case ((amount, expiry, payloads), hop) =>
218221
// The next-to-last trampoline hop must include invoice data to indicate the conversion to a legacy payment.
219222
val payload = if (payloads.length == 1) {

eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/MultiPartHandler.scala

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import akka.actor.typed.scaladsl.Behaviors
2222
import akka.actor.typed.scaladsl.adapter.ClassicActorContextOps
2323
import akka.actor.{ActorContext, ActorRef, PoisonPill, Status}
2424
import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter}
25-
import fr.acinq.bitcoin.{ByteVector32, Crypto}
25+
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto}
2626
import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC, RES_SUCCESS}
2727
import fr.acinq.eclair.db._
2828
import fr.acinq.eclair.payment.Monitoring.{Metrics, Tags}
@@ -71,7 +71,13 @@ class MultiPartHandler(nodeParams: NodeParams, register: ActorRef, db: IncomingP
7171
Metrics.PaymentFailed.withTag(Tags.Direction, Tags.Directions.Received).withTag(Tags.Failure, Tags.FailureType(cmdFail)).increment()
7272
PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, p.add.channelId, cmdFail)
7373
case None =>
74-
log.info("received payment for amount={} totalAmount={}", p.add.amountMsat, p.payload.totalAmount)
74+
// We log whether the sender included the payment metadata field.
75+
// We always set it in our invoices to test whether senders support it.
76+
// Once all incoming payments correctly set that field, we can make it mandatory.
77+
p.payload.paymentMetadata match {
78+
case Some(paymentMetadata) => log.info("received payment for amount={} totalAmount={} paymentMetadata={}", p.add.amountMsat, p.payload.totalAmount, paymentMetadata.toHex)
79+
case None => log.info("received payment for amount={} totalAmount={} without payment metadata", p.add.amountMsat, p.payload.totalAmount)
80+
}
7581
pendingPayments.get(p.add.paymentHash) match {
7682
case Some((_, handler)) =>
7783
handler ! MultiPartPaymentFSM.HtlcPart(p.payload.totalAmount, p.add)
@@ -223,12 +229,25 @@ object MultiPartHandler {
223229
val paymentPreimage = paymentPreimage_opt.getOrElse(randomBytes32())
224230
val paymentHash = Crypto.sha256(paymentPreimage)
225231
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds)
232+
val paymentMetadata = Some(ByteVector64.Zeroes.bytes)
226233
val invoiceFeatures = {
227234
val activatedInvoiceFeatures = nodeParams.features.invoiceFeatures().activated.map { case (f, s) => f.supportBit(s) }.toSet
228235
val allInvoiceFeatures = if (nodeParams.enableTrampolinePayment) activatedInvoiceFeatures + Features.TrampolinePayment.optional else activatedInvoiceFeatures
229236
allInvoiceFeatures.toSeq
230237
}
231-
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, description, nodeParams.minFinalExpiryDelta, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops, features = PaymentRequestFeatures(invoiceFeatures: _*))
238+
val paymentRequest = PaymentRequest(
239+
nodeParams.chainHash,
240+
amount_opt,
241+
paymentHash,
242+
nodeParams.privateKey,
243+
description,
244+
nodeParams.minFinalExpiryDelta,
245+
fallbackAddress_opt,
246+
expirySeconds = Some(expirySeconds),
247+
extraHops = extraHops,
248+
paymentMetadata = paymentMetadata,
249+
features = PaymentRequestFeatures(invoiceFeatures: _*)
250+
)
232251
context.log.debug("generated payment request={} from amount={}", PaymentRequest.write(paymentRequest), amount_opt)
233252
nodeParams.db.payments.addIncomingPayment(paymentRequest, paymentPreimage, paymentType)
234253
paymentRequest

eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentInitiatorSpec.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import fr.acinq.eclair.payment.OutgoingPaymentPacket.Upstream
2828
import fr.acinq.eclair.payment.PaymentPacketSpec._
2929
import fr.acinq.eclair.payment.PaymentRequest.{ExtraHop, PaymentRequestFeatures}
3030
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.SendMultiPartPayment
31+
import fr.acinq.eclair.payment.send.PaymentError.UnsupportedFeatures
3132
import fr.acinq.eclair.payment.send.PaymentInitiator._
3233
import fr.acinq.eclair.payment.send.{PaymentError, PaymentInitiator, PaymentLifecycle}
3334
import fr.acinq.eclair.router.RouteNotFound
@@ -122,6 +123,7 @@ class PaymentInitiatorSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike
122123
val fail = sender.expectMsgType[PaymentFailed]
123124
assert(fail.id === id)
124125
assert(fail.failures.head.isInstanceOf[LocalFailure])
126+
assert(fail.failures.head.asInstanceOf[LocalFailure].t === UnsupportedFeatures(pr.features.features))
125127
}
126128

127129
test("forward payment with pre-defined route") { f =>

0 commit comments

Comments
 (0)