Skip to content

Commit

Permalink
Use minFinalExpiryDelta from spec
Browse files Browse the repository at this point in the history
An invoice that doesn't explicitly set min_final_cltv_expiry has an default min_final_cltv_expiry of 18.
This default allows invoices to be slightly shorter but does not mean that the min_final_cltv_expiry can be unknown and needs to be provided from somewhere else.
  • Loading branch information
thomash-acinq committed Mar 1, 2022
1 parent 068b139 commit b36d22f
Show file tree
Hide file tree
Showing 21 changed files with 64 additions and 77 deletions.
11 changes: 4 additions & 7 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ trait Eclair {

def findRouteBetween(sourceNodeId: PublicKey, targetNodeId: PublicKey, amount: MilliSatoshi, pathFindingExperimentName_opt: Option[String], assistedRoutes: Seq[Seq[Bolt11Invoice.ExtraHop]] = Seq.empty, includeLocalChannelCost: Boolean = false, ignoreNodeIds: Seq[PublicKey] = Seq.empty, ignoreShortChannelIds: Seq[ShortChannelId] = Seq.empty, maxFee_opt: Option[MilliSatoshi] = None)(implicit timeout: Timeout): Future[RouteResponse]

def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, finalCltvExpiryDelta: CltvExpiryDelta, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse]
def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, route: PredefinedRoute, trampolineSecret_opt: Option[ByteVector32] = None, trampolineFees_opt: Option[MilliSatoshi] = None, trampolineExpiryDelta_opt: Option[CltvExpiryDelta] = None, trampolineNodes_opt: Seq[PublicKey] = Nil)(implicit timeout: Timeout): Future[SendPaymentToRouteResponse]

def audit(from: TimestampSecond, to: TimestampSecond)(implicit timeout: Timeout): Future[AuditResponse]

Expand Down Expand Up @@ -309,9 +309,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
}
}

override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, 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] = {
override def sendToRoute(amount: MilliSatoshi, recipientAmount_opt: Option[MilliSatoshi], externalId_opt: Option[String], parentId_opt: Option[UUID], invoice: Bolt11Invoice, 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_opt.getOrElse(amount))
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)
val sendPayment = SendPaymentToRoute(amount, recipientAmount, invoice, 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 @@ -337,10 +337,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
externalId_opt match {
case Some(externalId) if externalId.length > externalIdMaxLength => Left(new IllegalArgumentException(s"externalId is too long: cannot exceed $externalIdMaxLength characters"))
case _ if invoice.isExpired() => Left(new IllegalArgumentException("invoice has expired"))
case _ => invoice.minFinalCltvExpiryDelta match {
case Some(minFinalCltvExpiryDelta) => Right(SendPaymentToNode(amount, invoice, maxAttempts, minFinalCltvExpiryDelta, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams))
case None => Right(SendPaymentToNode(amount, invoice, maxAttempts, externalId = externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams))
}
case _ => Right(SendPaymentToNode(amount, invoice, maxAttempts, externalId_opt, assistedRoutes = invoice.routingInfo, routeParams = routeParams))
}
case Left(t) => Left(t)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,6 @@ object Channel {
// we won't exchange more than this many signatures when negotiating the closing fee
val MAX_NEGOTIATION_ITERATIONS = 20

// this is defined in BOLT 11
val MIN_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18)
val MAX_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(7 * 144) // one week

// since BOLT 1.1, there is a max value for the refund delay of the main commitment tx
val MAX_TO_SELF_DELAY: CltvExpiryDelta = CltvExpiryDelta(2016)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import fr.acinq.eclair.channel.Helpers.Closing
import fr.acinq.eclair.channel.Monitoring.Metrics
import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
import fr.acinq.eclair.crypto.{Generators, ShaChain}
import fr.acinq.eclair.payment.OutgoingPaymentPacket
import fr.acinq.eclair.payment.{Bolt11Invoice, OutgoingPaymentPacket}
import fr.acinq.eclair.transactions.DirectedHtlc._
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
Expand Down Expand Up @@ -324,7 +324,7 @@ object Commitments {
return Left(ExpiryTooSmall(commitments.channelId, minimum = minExpiry, actual = cmd.cltvExpiry, blockHeight = currentHeight))
}
// we don't want to use too high a refund timeout, because our funds will be locked during that time if the payment is never fulfilled
val maxExpiry = Channel.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(currentHeight)
val maxExpiry = Bolt11Invoice.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(currentHeight)
if (cmd.cltvExpiry >= maxExpiry) {
return Left(ExpiryTooBig(commitments.channelId, maximum = maxExpiry, actual = cmd.cltvExpiry, blockHeight = currentHeight))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ object DirectedHtlcSerializer extends ConvertClassSerializer[DirectedHtlc](h =>
object InvoiceSerializer extends MinimalSerializer({
case p: Bolt11Invoice =>
val expiry = p.relativeExpiry_opt.map(ex => JField("expiry", JLong(ex))).toSeq
val minFinalCltvExpiry = p.minFinalCltvExpiryDelta.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq
val minFinalCltvExpiry = p.minFinalCltvExpiryDelta_opt.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq
val amount = p.amount_opt.map(msat => JField("amount", JLong(msat.toLong))).toSeq
val features = JField("features", Extraction.decompose(p.features)(
DefaultFormats +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat

lazy val relativeExpiry: FiniteDuration = FiniteDuration(relativeExpiry_opt.getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS)

lazy val minFinalCltvExpiryDelta: Option[CltvExpiryDelta] = tags.collectFirst {
lazy val minFinalCltvExpiryDelta_opt: Option[CltvExpiryDelta] = tags.collectFirst {
case cltvExpiry: Bolt11Invoice.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta
}

lazy val minFinalCltvExpiryDelta: CltvExpiryDelta = minFinalCltvExpiryDelta_opt.getOrElse(MIN_CLTV_EXPIRY_DELTA)

lazy val features: Features[InvoiceFeature] = tags.collectFirst { case f: InvoiceFeatures => f.features.invoiceFeatures() }.getOrElse(Features.empty[InvoiceFeature])

/**
Expand Down Expand Up @@ -134,6 +136,8 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat

object Bolt11Invoice {
val DEFAULT_EXPIRY_SECONDS: Long = 3600
val MIN_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(18)
val MAX_CLTV_EXPIRY_DELTA: CltvExpiryDelta = CltvExpiryDelta(7 * 144) // one week

val prefixes = Map(
Block.RegtestGenesisBlock.hash -> "lnbcrt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ trait Invoice {

val relativeExpiry: FiniteDuration

val minFinalCltvExpiryDelta: Option[CltvExpiryDelta]
val minFinalCltvExpiryDelta: CltvExpiryDelta

val features: Features[InvoiceFeature]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ object MultiPartHandler {
}

private def validatePaymentCltv(nodeParams: NodeParams, payment: IncomingPaymentPacket.FinalPacket, record: IncomingPayment)(implicit log: LoggingAdapter): Boolean = {
val minExpiry = record.invoice.minFinalCltvExpiryDelta.getOrElse(nodeParams.channelConf.minFinalExpiryDelta).toCltvExpiry(nodeParams.currentBlockHeight)
val minExpiry = record.invoice.minFinalCltvExpiryDelta.toCltvExpiry(nodeParams.currentBlockHeight)
if (payment.add.cltvExpiry < minExpiry) {
log.warning("received payment with expiry too small for amount={} totalAmount={}", payment.add.amountMsat, payment.payload.totalAmount)
false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class PaymentInitiator(nodeParams: NodeParams, outgoingPaymentFactory: PaymentIn
val paymentId = UUID.randomUUID()
sender() ! paymentId
val paymentCfg = SendPaymentConfig(paymentId, paymentId, r.externalId, r.paymentHash, r.recipientAmount, r.recipientNodeId, Upstream.Local(paymentId), None, storeInDb = true, publishEvent = true, recordPathFindingMetrics = r.recordPathFindingMetrics, Nil)
val finalExpiry = Channel.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)
val finalExpiry = Bolt11Invoice.MIN_CLTV_EXPIRY_DELTA.toCltvExpiry(nodeParams.currentBlockHeight + 1)
val finalPayload = PaymentOnion.FinalTlvPayload(TlvStream(Seq(OnionPaymentPayloadTlv.AmountToForward(r.recipientAmount), OnionPaymentPayloadTlv.OutgoingCltv(finalExpiry), OnionPaymentPayloadTlv.PaymentData(randomBytes32(), r.recipientAmount), OnionPaymentPayloadTlv.KeySend(r.paymentPreimage)), r.userCustomTlvs))
val fsm = outgoingPaymentFactory.spawnOutgoingPayment(context, paymentCfg)
fsm ! PaymentLifecycle.SendPaymentToNode(self, r.recipientNodeId, finalPayload, r.maxAttempts, routeParams = r.routeParams)
Expand Down Expand Up @@ -272,9 +272,8 @@ object PaymentInitiator {
def invoice: Invoice
def recipientNodeId: PublicKey = invoice.nodeId
def paymentHash: ByteVector32 = invoice.paymentHash
def fallbackFinalExpiryDelta: CltvExpiryDelta
// We add one block in order to not have our htlcs fail when a new block has just been found.
def finalExpiry(currentBlockHeight: BlockHeight): CltvExpiry = invoice.minFinalCltvExpiryDelta.getOrElse(fallbackFinalExpiryDelta).toCltvExpiry(currentBlockHeight + 1)
def finalExpiry(currentBlockHeight: BlockHeight): CltvExpiry = invoice.minFinalCltvExpiryDelta.toCltvExpiry(currentBlockHeight + 1)
// @formatter:on
}

Expand All @@ -290,21 +289,18 @@ object PaymentInitiator {
* the payment will automatically be retried in case of TrampolineFeeInsufficient errors.
* For example, [(10 msat, 144), (15 msat, 288)] will first send a payment with a fee of 10
* msat and cltv of 144, and retry with 15 msat and 288 in case an error occurs.
* @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[invoice]] doesn't specify it.
* @param routeParams (optional) parameters to fine-tune the routing algorithm.
*/
case class SendTrampolinePayment(recipientAmount: MilliSatoshi,
invoice: Invoice,
trampolineNodeId: PublicKey,
trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)],
fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
routeParams: RouteParams) extends SendRequestedPayment

/**
* @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice).
* @param invoice Bolt 11 invoice.
* @param maxAttempts maximum number of retries.
* @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[invoice]] doesn't specify it.
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
* @param assistedRoutes (optional) routing hints (usually from a Bolt 11 invoice).
* @param routeParams (optional) parameters to fine-tune the routing algorithm.
Expand All @@ -314,7 +310,6 @@ object PaymentInitiator {
case class SendPaymentToNode(recipientAmount: MilliSatoshi,
invoice: Invoice,
maxAttempts: Int,
fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
externalId: Option[String] = None,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
routeParams: RouteParams,
Expand Down Expand Up @@ -360,7 +355,6 @@ object PaymentInitiator {
* @param recipientAmount amount that should be received by the final recipient (usually from a Bolt 11 invoice).
* This amount may be split between multiple requests if using MPP.
* @param invoice Bolt 11 invoice.
* @param fallbackFinalExpiryDelta expiry delta for the final recipient when the [[invoice]] doesn't specify it.
* @param route route to use to reach either the final recipient or the first trampoline node.
* @param externalId (optional) externally-controlled identifier (to reconcile between application DB and eclair DB).
* @param parentId id of the whole payment. When manually sending a multi-part payment, you need to make
Expand All @@ -379,7 +373,6 @@ object PaymentInitiator {
case class SendPaymentToRoute(amount: MilliSatoshi,
recipientAmount: MilliSatoshi,
invoice: Invoice,
fallbackFinalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
route: PredefinedRoute,
externalId: Option[String],
parentId: Option[UUID],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
assert(send2.recipientAmount === 123.msat)
assert(send2.paymentHash === ByteVector32.Zeroes)
assert(send2.invoice === invoice2)
assert(send2.fallbackFinalExpiryDelta === CltvExpiryDelta(96))

// with custom route fees parameters
eclair.send(None, 123 msat, invoice0, maxFeeFlat_opt = Some(123 sat), maxFeePct_opt = Some(4.20))
Expand Down Expand Up @@ -309,8 +308,8 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I
val parentId = UUID.randomUUID()
val secret = randomBytes32()
val pr = Bolt11Invoice(Block.LivenetGenesisBlock.hash, Some(1234 msat), ByteVector32.One, randomKey(), Right(randomBytes32()), CltvExpiryDelta(18))
eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, CltvExpiryDelta(123), route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines)
paymentInitiator.expectMsg(SendPaymentToRoute(1000 msat, 1200 msat, pr, CltvExpiryDelta(123), route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines))
eclair.sendToRoute(1000 msat, Some(1200 msat), Some("42"), Some(parentId), pr, route, Some(secret), Some(100 msat), Some(CltvExpiryDelta(144)), trampolines)
paymentInitiator.expectMsg(SendPaymentToRoute(1000 msat, 1200 msat, pr, route, Some("42"), Some(parentId), Some(secret), 100 msat, CltvExpiryDelta(144), trampolines))
}

test("call sendWithPreimage, which generates a random preimage, to perform a KeySend payment") { f =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Channe
def buildCmdAdd(paymentHash: ByteVector32, dest: PublicKey, paymentSecret: ByteVector32): CMD_ADD_HTLC = {
// allow overpaying (no more than 2 times the required amount)
val amount = requiredAmount + Random.nextInt(requiredAmount.toLong.toInt).msat
val expiry = (Channel.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight = BlockHeight(400000))
val expiry = (Bolt11Invoice.MIN_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight = BlockHeight(400000))
OutgoingPaymentPacket.buildCommand(self, Upstream.Local(UUID.randomUUID()), paymentHash, ChannelHop(null, dest, null) :: Nil, PaymentOnion.createSinglePartPayload(amount, expiry, paymentSecret, None)).get._1
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import fr.acinq.eclair.channel.publish.TxPublisher.{PublishFinalTx, PublishRepla
import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.payment.OutgoingPaymentPacket
import fr.acinq.eclair.payment.{Bolt11Invoice, OutgoingPaymentPacket}
import fr.acinq.eclair.payment.relay.Relayer._
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing}
Expand Down Expand Up @@ -158,10 +158,10 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
import f._
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val expiryTooBig = (Channel.MAX_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight)
val expiryTooBig = (Bolt11Invoice.MAX_CLTV_EXPIRY_DELTA + 1).toCltvExpiry(currentBlockHeight)
val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32(), expiryTooBig, TestConstants.emptyOnionPacket, localOrigin(sender.ref))
alice ! add
val error = ExpiryTooBig(channelId(alice), maximum = Channel.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(currentBlockHeight), actual = expiryTooBig, blockHeight = currentBlockHeight)
val error = ExpiryTooBig(channelId(alice), maximum = Bolt11Invoice.MAX_CLTV_EXPIRY_DELTA.toCltvExpiry(currentBlockHeight), actual = expiryTooBig, blockHeight = currentBlockHeight)
sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate)))
alice2bob.expectNoMessage(200 millis)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
def send(amountMsat: MilliSatoshi, paymentHandler: ActorRef, paymentInitiator: ActorRef): UUID = {
sender.send(paymentHandler, ReceivePayment(Some(amountMsat), Left("1 coffee")))
val invoice = sender.expectMsgType[Invoice]
val sendReq = SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams)
val sendReq = SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams)
sender.send(paymentInitiator, sendReq)
sender.expectMsgType[UUID]
}
Expand Down Expand Up @@ -684,7 +684,7 @@ abstract class AnchorChannelIntegrationSpec extends ChannelIntegrationSpec {
val invoice = sender.expectMsgType[Invoice]

// then we make the actual payment
sender.send(nodes("C").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, fallbackFinalExpiryDelta = finalCltvExpiryDelta, routeParams = integrationTestRouteParams))
sender.send(nodes("C").paymentInitiator, SendPaymentToNode(amountMsat, invoice, maxAttempts = 1, routeParams = integrationTestRouteParams))
val paymentId = sender.expectMsgType[UUID]
val ps = sender.expectMsgType[PaymentSent](60 seconds)
assert(ps.id == paymentId)
Expand Down
Loading

0 comments on commit b36d22f

Please sign in to comment.