Skip to content

Commit 270ad26

Browse files
committed
Restrict payment metadata to 128 bytes
We can't let unbounded payment metadata be transmitted over the network, otherwise we may not have enough space available for other onion fields (especially for trampoline and route blinding).
1 parent e956923 commit 270ad26

File tree

3 files changed

+32
-12
lines changed

3 files changed

+32
-12
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ object PaymentError {
2828
case class UnsupportedFeatures(features: Features) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" }
2929
/** The invoice is missing a payment secret. */
3030
case object PaymentSecretMissing extends InvalidInvoice { override def getMessage: String = "invalid invoice: payment secret is missing" }
31+
/** The invoice contains too much payment metadata. */
32+
case object PaymentMetadataTooLong extends InvalidInvoice { override def getMessage: String = "invalid invoice: payment metatada must be at most 128 bytes" }
3133
// @formatter:on
3234

3335
// @formatter:off

eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentInitiator.scala

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,22 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
5252
}
5353
val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), Some(r.paymentRequest), storeInDb = true, publishEvent = true, recordPathFindingMetrics = true, Nil)
5454
val finalExpiry = r.finalExpiry(nodeParams.currentBlockHeight)
55-
r.paymentRequest.paymentSecret match {
56-
case _ if !r.paymentRequest.features.areSupported(nodeParams) =>
57-
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.paymentRequest.features.features)) :: Nil)
58-
case None =>
59-
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil)
60-
case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) =>
61-
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
62-
fsm ! SendMultiPartPayment(sender(), paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.paymentRequest.paymentMetadata, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs)
63-
case Some(paymentSecret) =>
64-
val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.paymentRequest.paymentMetadata, r.userCustomTlvs)
65-
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
66-
fsm ! PaymentLifecycle.SendPaymentToNode(sender(), r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams)
55+
if (!r.paymentRequest.features.areSupported(nodeParams)) {
56+
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, UnsupportedFeatures(r.paymentRequest.features.features)) :: Nil)
57+
} else if (r.paymentRequest.paymentMetadata.exists(m => m.length > 128)) {
58+
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentMetadataTooLong) :: Nil)
59+
} else {
60+
r.paymentRequest.paymentSecret match {
61+
case None =>
62+
sender() ! PaymentFailed(paymentId, r.paymentHash, LocalFailure(r.recipientAmount, Nil, PaymentSecretMissing) :: Nil)
63+
case Some(paymentSecret) if r.paymentRequest.features.allowMultiPart && nodeParams.features.hasFeature(BasicMultiPartPayment) =>
64+
val fsm = outgoingPaymentFactory.spawnOutgoingMultiPartPayment(context, paymentCfg)
65+
fsm ! SendMultiPartPayment(sender(), paymentSecret, r.recipientNodeId, r.recipientAmount, finalExpiry, r.maxAttempts, r.paymentRequest.paymentMetadata, r.assistedRoutes, r.routeParams, userCustomTlvs = r.userCustomTlvs)
66+
case Some(paymentSecret) =>
67+
val finalPayload = PaymentOnion.createSinglePartPayload(r.recipientAmount, finalExpiry, paymentSecret, r.paymentRequest.paymentMetadata, r.userCustomTlvs)
68+
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
69+
fsm ! PaymentLifecycle.SendPaymentToNode(sender(), r.recipientNodeId, finalPayload, r.maxAttempts, r.assistedRoutes, r.routeParams)
70+
}
6771
}
6872

6973
case r: SendSpontaneousPayment =>

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

Lines changed: 14 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.{PaymentMetadataTooLong, 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,19 @@ 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))
127+
}
128+
129+
test("reject payment with long payment metadata") { f =>
130+
import f._
131+
val longPaymentMetadata = Some(hex"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
132+
val pr = PaymentRequest(Block.LivenetGenesisBlock.hash, Some(finalAmount), paymentHash, randomKey(), Left("Some invoice"), CltvExpiryDelta(18), paymentMetadata = longPaymentMetadata, features = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory, Features.PaymentMetadata.optional))
133+
sender.send(initiator, SendPaymentToNode(finalAmount, pr, 1, CltvExpiryDelta(42), routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams))
134+
val id = sender.expectMsgType[UUID]
135+
val fail = sender.expectMsgType[PaymentFailed]
136+
assert(fail.id === id)
137+
assert(fail.failures.head.isInstanceOf[LocalFailure])
138+
assert(fail.failures.head.asInstanceOf[LocalFailure].t === PaymentMetadataTooLong)
125139
}
126140

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

0 commit comments

Comments
 (0)