Skip to content

Commit bbfbad5

Browse files
authored
Validate payment secret when decoding (#1840)
The `payment_secret` feature was made mandatory in #1810 and is the default in other implementations as well. We can thus force it to be available when decoding onion payloads, which simplifies downstream components (no need to handle the case where a `payment_secret` may be missing anymore). We also rename messages in `PaymentInitiator` to remove the confusion with Bolt 11 payment requests.
1 parent e750474 commit bbfbad5

31 files changed

+556
-618
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

+25-31
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import fr.acinq.eclair.payment._
3535
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
3636
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
3737
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
38-
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest, SendPaymentToRouteResponse}
38+
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPayment, SendPaymentToRoute, SendPaymentToRouteResponse, SendSpontaneousPayment}
3939
import fr.acinq.eclair.router.Router._
4040
import fr.acinq.eclair.router.{NetworkStats, RouteCalculation, Router}
4141
import fr.acinq.eclair.wire.protocol._
@@ -107,9 +107,9 @@ trait Eclair {
107107

108108
def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]]
109109

110-
def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
110+
def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
111111

112-
def sendBlocking(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]]
112+
def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]]
113113

114114
def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32 = randomBytes32(), maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
115115

@@ -272,15 +272,14 @@ class EclairImpl(appKit: Kit) extends Eclair {
272272
override def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] =
273273
findRouteBetween(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)
274274

275-
276275
override def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse] = {
277276
val maxFee = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf).getMaxFee(amount)
278277
(appKit.router ? RouteRequest(sourceNodeId, targetNodeId, amount, maxFee, assistedRoutes)).mapTo[RouteResponse]
279278
}
280279

281280
override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: PaymentRequest, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32], trampolineFees_opt: Option[MilliSatoshi], trampolineExpiryDelta_opt: Option[CltvExpiryDelta], trampolineNodes_opt: Seq[PublicKey])(implicit timeout: Timeout): Future[SendPaymentToRouteResponse] = {
282281
val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount.getOrElse(amount))
283-
val sendPayment = SendPaymentToRouteRequest(amount, recipientAmount, externalId_opt, parentId_opt, invoice, finalCltvExpiryDelta, route, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt)
282+
val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, finalCltvExpiryDelta, route, externalId_opt, parentId_opt, trampolineSecret_opt, trampolineFees_opt.getOrElse(0 msat), trampolineExpiryDelta_opt.getOrElse(CltvExpiryDelta(0)), trampolineNodes_opt)
284283
if (invoice.isExpired) {
285284
Future.failed(new IllegalArgumentException("invoice has expired"))
286285
} else if (route.isEmpty) {
@@ -296,7 +295,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
296295
}
297296
}
298297

299-
private def createPaymentRequest(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double]): Either[IllegalArgumentException, SendPaymentRequest] = {
298+
private def createPaymentRequest(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double]): Either[IllegalArgumentException, SendPayment] = {
300299
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
301300
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
302301
val routeParams = defaultRouteParams.copy(
@@ -306,26 +305,23 @@ class EclairImpl(appKit: Kit) extends Eclair {
306305

307306
externalId_opt match {
308307
case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
309-
case _ => invoice_opt match {
310-
case Some(invoice) if invoice.isExpired => Left(new IllegalArgumentException("invoice has expired"))
311-
case Some(invoice) => invoice.minFinalCltvExpiryDelta match {
312-
case Some(minFinalCltvExpiryDelta) => Right(SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, minFinalCltvExpiryDelta, invoice_opt, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
313-
case None => Right(SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, paymentRequest = invoice_opt, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
314-
}
315-
case None => Right(SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts = maxAttempts, externalId = externalId_opt, routeParams = Some(routeParams)))
308+
case _ if invoice.isExpired => Left(new IllegalArgumentException("invoice has expired"))
309+
case _ => invoice.minFinalCltvExpiryDelta match {
310+
case Some(minFinalCltvExpiryDelta) => Right(SendPayment(amount, invoice, maxAttempts, minFinalCltvExpiryDelta, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
311+
case None => Right(SendPayment(amount, invoice, maxAttempts, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
316312
}
317313
}
318314
}
319315

320-
override def send(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
321-
createPaymentRequest(externalId_opt, recipientNodeId, amount, paymentHash, invoice_opt, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
316+
override def send(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
317+
createPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
322318
case Left(ex) => Future.failed(ex)
323319
case Right(req) => (appKit.paymentInitiator ? req).mapTo[UUID]
324320
}
325321
}
326322

327-
override def sendBlocking(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] = {
328-
createPaymentRequest(externalId_opt, recipientNodeId, amount, paymentHash, invoice_opt, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
323+
override def sendBlocking(externalId_opt: Option[String], amount: MilliSatoshi, invoice: PaymentRequest, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]] = {
324+
createPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
329325
case Left(ex) => Future.failed(ex)
330326
case Right(req) => (appKit.paymentInitiator ? req.copy(blockUntilComplete = true)).map {
331327
case e: PreimageReceived => Left(e)
@@ -334,6 +330,17 @@ class EclairImpl(appKit: Kit) extends Eclair {
334330
}
335331
}
336332

333+
override def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
334+
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
335+
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
336+
val routeParams = defaultRouteParams.copy(
337+
maxFeePct = maxFeePct_opt.getOrElse(defaultRouteParams.maxFeePct),
338+
maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase)
339+
)
340+
val sendPayment = SendSpontaneousPayment(amount, recipientNodeId, paymentPreimage, maxAttempts, externalId_opt, Some(routeParams))
341+
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
342+
}
343+
337344
override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future {
338345
id match {
339346
case Left(uuid) => appKit.nodeParams.db.payments.listOutgoingPayments(uuid)
@@ -421,19 +428,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
421428
override def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]] =
422429
(appKit.relayer ? GetOutgoingChannels()).mapTo[OutgoingChannels].map(_.channels.map(_.toUsableBalance))
423430

424-
override def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32, maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
425-
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
426-
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
427-
val routeParams = defaultRouteParams.copy(
428-
maxFeePct = maxFeePct_opt.getOrElse(defaultRouteParams.maxFeePct),
429-
maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase)
430-
)
431-
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
432-
val keySendTlvRecords = Seq(GenericTlv(UInt64(5482373484L), paymentPreimage))
433-
val sendPayment = SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, externalId = externalId_opt, routeParams = Some(routeParams), userCustomTlvs = keySendTlvRecords)
434-
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
435-
}
436-
437431
override def signMessage(message: ByteVector): SignedMessage = {
438432
val bytesToSign = SignedMessage.signedBytes(message)
439433
val (signature, recoveryId) = appKit.nodeParams.nodeKeyManager.signDigest(bytesToSign)
@@ -445,6 +439,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
445439
val signature = ByteVector64(recoverableSignature.tail)
446440
val recoveryId = recoverableSignature.head.toInt - 31
447441
val pubKeyFromSignature = Crypto.recoverPublicKey(signature, signedBytes, recoveryId)
448-
VerifiedMessage(true, pubKeyFromSignature)
442+
VerifiedMessage(valid = true, pubKeyFromSignature)
449443
}
450444
}

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

+3-8
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package fr.acinq.eclair.payment
1818

19-
import java.util.UUID
20-
2119
import akka.actor.ActorRef
2220
import akka.event.LoggingAdapter
2321
import fr.acinq.bitcoin.ByteVector32
@@ -30,6 +28,7 @@ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64, rando
3028
import scodec.bits.ByteVector
3129
import scodec.{Attempt, DecodeResult}
3230

31+
import java.util.UUID
3332
import scala.reflect.ClassTag
3433

3534
/**
@@ -86,7 +85,6 @@ object IncomingPacket {
8685
case Left(failure) => Left(failure)
8786
// NB: we don't validate the ChannelRelayPacket here because its fees and cltv depend on what channel we'll choose to use.
8887
case Right(DecodedOnionPacket(payload: Onion.ChannelRelayPayload, next)) => Right(ChannelRelayPacket(add, payload, next))
89-
case Right(DecodedOnionPacket(payload: Onion.FinalLegacyPayload, _)) => validateFinal(add, payload)
9088
case Right(DecodedOnionPacket(payload: Onion.FinalTlvPayload, _)) => payload.records.get[OnionTlv.TrampolineOnion] match {
9189
case Some(OnionTlv.TrampolineOnion(trampolinePacket)) => decryptOnion(add, privateKey)(trampolinePacket, Sphinx.TrampolinePacket) match {
9290
case Left(failure) => Left(failure)
@@ -117,12 +115,10 @@ object IncomingPacket {
117115
Left(FinalIncorrectCltvExpiry(add.cltvExpiry)) // previous trampoline didn't forward the right expiry
118116
} else if (outerPayload.totalAmount != innerPayload.amount) {
119117
Left(FinalIncorrectHtlcAmount(outerPayload.totalAmount)) // previous trampoline didn't forward the right amount
120-
} else if (innerPayload.paymentSecret.isEmpty) {
121-
Left(InvalidOnionPayload(UInt64(8), 0)) // trampoline recipients always provide a payment secret in the invoice
122118
} else {
123119
// We merge contents from the outer and inner payloads.
124120
// We must use the inner payload's total amount and payment secret because the payment may be split between multiple trampoline payments (#reckless).
125-
Right(FinalPacket(add, Onion.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret.get)))
121+
Right(FinalPacket(add, Onion.createMultiPartPayload(outerPayload.amount, innerPayload.totalAmount, outerPayload.expiry, innerPayload.paymentSecret)))
126122
}
127123
}
128124

@@ -174,8 +170,7 @@ object OutgoingPacket {
174170
hops.reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[Onion.PerHopPayload](finalPayload))) {
175171
case ((amount, expiry, payloads), hop) =>
176172
val payload = hop match {
177-
// Since we don't have any scenario where we add tlv data for intermediate hops, we use legacy payloads.
178-
case hop: ChannelHop => Onion.RelayLegacyPayload(hop.lastUpdate.shortChannelId, amount, expiry)
173+
case hop: ChannelHop => Onion.ChannelRelayTlvPayload(hop.lastUpdate.shortChannelId, amount, expiry)
179174
case hop: NodeHop => Onion.createNodeRelayPayload(amount, expiry, hop.nextNodeId)
180175
}
181176
(amount + hop.fee(amount), expiry + hop.cltvExpiryDelta, payload +: payloads)

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

+12-10
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ object PaymentRequest {
123123
val prefixes = Map(
124124
Block.RegtestGenesisBlock.hash -> "lnbcrt",
125125
Block.TestnetGenesisBlock.hash -> "lntb",
126-
Block.LivenetGenesisBlock.hash -> "lnbc")
126+
Block.LivenetGenesisBlock.hash -> "lnbc"
127+
)
127128

128129
def apply(chainHash: ByteVector32,
129130
amount: Option[MilliSatoshi],
@@ -135,30 +136,31 @@ object PaymentRequest {
135136
expirySeconds: Option[Long] = None,
136137
extraHops: List[List[ExtraHop]] = Nil,
137138
timestamp: Long = System.currentTimeMillis() / 1000L,
138-
features: Option[PaymentRequestFeatures] = Some(PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory))): PaymentRequest = {
139-
139+
paymentSecret: ByteVector32 = randomBytes32(),
140+
features: PaymentRequestFeatures = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory)): PaymentRequest = {
141+
require(features.requirePaymentSecret, "invoices must require a payment secret")
140142
val prefix = prefixes(chainHash)
141143
val tags = {
142144
val defaultTags = List(
143145
Some(PaymentHash(paymentHash)),
144146
Some(Description(description)),
147+
Some(PaymentSecret(paymentSecret)),
145148
fallbackAddress.map(FallbackAddress(_)),
146149
expirySeconds.map(Expiry(_)),
147150
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
148-
features).flatten
149-
val paymentSecretTag = if (features.exists(_.allowPaymentSecret)) PaymentSecret(randomBytes32()) :: Nil else Nil
151+
Some(features)
152+
).flatten
150153
val routingInfoTags = extraHops.map(RoutingInfo)
151-
defaultTags ++ paymentSecretTag ++ routingInfoTags
154+
defaultTags ++ routingInfoTags
152155
}
153-
154156
PaymentRequest(
155157
prefix = prefix,
156158
amount = amount,
157159
timestamp = timestamp,
158160
nodeId = privateKey.publicKey,
159161
tags = tags,
160-
signature = ByteVector.empty)
161-
.sign(privateKey)
162+
signature = ByteVector.empty
163+
).sign(privateKey)
162164
}
163165

164166
case class Bolt11Data(timestamp: Long, taggedFields: List[TaggedField], signature: ByteVector)
@@ -485,7 +487,7 @@ object PaymentRequest {
485487
}
486488

487489
// char -> 5 bits value
488-
val charToint5: Map[Char, BitVector] = Bech32.alphabet.zipWithIndex.toMap.mapValues(BitVector.fromInt(_, size = 5, ordering = ByteOrdering.BigEndian)).toMap
490+
val charToint5: Map[Char, BitVector] = Bech32.alphabet.zipWithIndex.toMap.view.mapValues(BitVector.fromInt(_, size = 5, ordering = ByteOrdering.BigEndian)).toMap
489491

490492
// TODO: could be optimized by preallocating the resulting buffer
491493
def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _)

0 commit comments

Comments
 (0)