Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate payment secret when decoding #1840

Merged
merged 4 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 25 additions & 31 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment
import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance}
import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentRequest, SendPaymentToRouteRequest, SendPaymentToRouteResponse}
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPayment, SendPaymentToRoute, SendPaymentToRouteResponse, SendSpontaneousPayment}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router.{NetworkStats, RouteCalculation, Router}
import fr.acinq.eclair.wire.protocol._
Expand Down Expand Up @@ -107,9 +107,9 @@ trait Eclair {

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

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]
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]

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]]
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]]

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]

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


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

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] = {
val recipientAmount = recipientAmount_opt.getOrElse(invoice.amount.getOrElse(amount))
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)
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)
if (invoice.isExpired) {
Future.failed(new IllegalArgumentException("invoice has expired"))
} else if (route.isEmpty) {
Expand All @@ -296,7 +295,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
}
}

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] = {
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] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
val routeParams = defaultRouteParams.copy(
Expand All @@ -306,26 +305,23 @@ class EclairImpl(appKit: Kit) extends Eclair {

externalId_opt match {
case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
case _ => invoice_opt match {
case Some(invoice) if invoice.isExpired => Left(new IllegalArgumentException("invoice has expired"))
case Some(invoice) => invoice.minFinalCltvExpiryDelta match {
case Some(minFinalCltvExpiryDelta) => Right(SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, minFinalCltvExpiryDelta, invoice_opt, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
case None => Right(SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, paymentRequest = invoice_opt, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
}
case None => Right(SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts = maxAttempts, externalId = externalId_opt, routeParams = Some(routeParams)))
case _ if invoice.isExpired => Left(new IllegalArgumentException("invoice has expired"))
case _ => invoice.minFinalCltvExpiryDelta match {
case Some(minFinalCltvExpiryDelta) => Right(SendPayment(amount, invoice, maxAttempts, minFinalCltvExpiryDelta, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
case None => Right(SendPayment(amount, invoice, maxAttempts, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams)))
}
}
}

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] = {
createPaymentRequest(externalId_opt, recipientNodeId, amount, paymentHash, invoice_opt, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
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] = {
createPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
case Left(ex) => Future.failed(ex)
case Right(req) => (appKit.paymentInitiator ? req).mapTo[UUID]
}
}

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]] = {
createPaymentRequest(externalId_opt, recipientNodeId, amount, paymentHash, invoice_opt, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
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]] = {
createPaymentRequest(externalId_opt, amount, invoice, maxAttempts_opt, feeThreshold_opt, maxFeePct_opt) match {
case Left(ex) => Future.failed(ex)
case Right(req) => (appKit.paymentInitiator ? req.copy(blockUntilComplete = true)).map {
case e: PreimageReceived => Left(e)
Expand All @@ -334,6 +330,17 @@ class EclairImpl(appKit: Kit) extends Eclair {
}
}

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] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
val routeParams = defaultRouteParams.copy(
maxFeePct = maxFeePct_opt.getOrElse(defaultRouteParams.maxFeePct),
maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase)
)
val sendPayment = SendSpontaneousPayment(amount, recipientNodeId, paymentPreimage, maxAttempts, externalId_opt, Some(routeParams))
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
}

override def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]] = Future {
id match {
case Left(uuid) => appKit.nodeParams.db.payments.listOutgoingPayments(uuid)
Expand Down Expand Up @@ -421,19 +428,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
override def usableBalances()(implicit timeout: Timeout): Future[Iterable[UsableBalance]] =
(appKit.relayer ? GetOutgoingChannels()).mapTo[OutgoingChannels].map(_.channels.map(_.toUsableBalance))

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] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)
val defaultRouteParams = RouteCalculation.getDefaultRouteParams(appKit.nodeParams.routerConf)
val routeParams = defaultRouteParams.copy(
maxFeePct = maxFeePct_opt.getOrElse(defaultRouteParams.maxFeePct),
maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase)
)
val paymentHash: ByteVector32 = Crypto.sha256(paymentPreimage)
val keySendTlvRecords = Seq(GenericTlv(UInt64(5482373484L), paymentPreimage))
val sendPayment = SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, externalId = externalId_opt, routeParams = Some(routeParams), userCustomTlvs = keySendTlvRecords)
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
}

override def signMessage(message: ByteVector): SignedMessage = {
val bytesToSign = SignedMessage.signedBytes(message)
val (signature, recoveryId) = appKit.nodeParams.nodeKeyManager.signDigest(bytesToSign)
Expand All @@ -445,6 +439,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
val signature = ByteVector64(recoverableSignature.tail)
val recoveryId = recoverableSignature.head.toInt - 31
val pubKeyFromSignature = Crypto.recoverPublicKey(signature, signedBytes, recoveryId)
VerifiedMessage(true, pubKeyFromSignature)
VerifiedMessage(valid = true, pubKeyFromSignature)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

package fr.acinq.eclair.payment

import java.util.UUID

import akka.actor.ActorRef
import akka.event.LoggingAdapter
import fr.acinq.bitcoin.ByteVector32
Expand All @@ -30,6 +28,7 @@ import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64, rando
import scodec.bits.ByteVector
import scodec.{Attempt, DecodeResult}

import java.util.UUID
import scala.reflect.ClassTag

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

Expand Down Expand Up @@ -174,8 +170,7 @@ object OutgoingPacket {
hops.reverse.foldLeft((finalPayload.amount, finalPayload.expiry, Seq[Onion.PerHopPayload](finalPayload))) {
case ((amount, expiry, payloads), hop) =>
val payload = hop match {
// Since we don't have any scenario where we add tlv data for intermediate hops, we use legacy payloads.
case hop: ChannelHop => Onion.RelayLegacyPayload(hop.lastUpdate.shortChannelId, amount, expiry)
case hop: ChannelHop => Onion.ChannelRelayTlvPayload(hop.lastUpdate.shortChannelId, amount, expiry)
case hop: NodeHop => Onion.createNodeRelayPayload(amount, expiry, hop.nextNodeId)
}
(amount + hop.fee(amount), expiry + hop.cltvExpiryDelta, payload +: payloads)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ object PaymentRequest {
val prefixes = Map(
Block.RegtestGenesisBlock.hash -> "lnbcrt",
Block.TestnetGenesisBlock.hash -> "lntb",
Block.LivenetGenesisBlock.hash -> "lnbc")
Block.LivenetGenesisBlock.hash -> "lnbc"
)

def apply(chainHash: ByteVector32,
amount: Option[MilliSatoshi],
Expand All @@ -135,30 +136,31 @@ object PaymentRequest {
expirySeconds: Option[Long] = None,
extraHops: List[List[ExtraHop]] = Nil,
timestamp: Long = System.currentTimeMillis() / 1000L,
features: Option[PaymentRequestFeatures] = Some(PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory))): PaymentRequest = {

paymentSecret: ByteVector32 = randomBytes32(),
features: PaymentRequestFeatures = PaymentRequestFeatures(Features.VariableLengthOnion.mandatory, Features.PaymentSecret.mandatory)): PaymentRequest = {
require(features.requirePaymentSecret, "invoices must require a payment secret")
pm47 marked this conversation as resolved.
Show resolved Hide resolved
val prefix = prefixes(chainHash)
val tags = {
val defaultTags = List(
Some(PaymentHash(paymentHash)),
Some(Description(description)),
Some(PaymentSecret(paymentSecret)),
fallbackAddress.map(FallbackAddress(_)),
expirySeconds.map(Expiry(_)),
Some(MinFinalCltvExpiry(minFinalCltvExpiryDelta.toInt)),
features).flatten
val paymentSecretTag = if (features.exists(_.allowPaymentSecret)) PaymentSecret(randomBytes32()) :: Nil else Nil
Some(features)
).flatten
val routingInfoTags = extraHops.map(RoutingInfo)
defaultTags ++ paymentSecretTag ++ routingInfoTags
defaultTags ++ routingInfoTags
}

PaymentRequest(
prefix = prefix,
amount = amount,
timestamp = timestamp,
nodeId = privateKey.publicKey,
tags = tags,
signature = ByteVector.empty)
.sign(privateKey)
signature = ByteVector.empty
).sign(privateKey)
}

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

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

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