Skip to content

Commit

Permalink
Trampoline to blinded (#2811)
Browse files Browse the repository at this point in the history
Allow trampoline to pay a list of blinded paths instead of a node id.
Only the last trampoline hop can target blinded paths, trampoline nodes are still reached with their node id, not with blinded paths.

Co-authored-by: t-bast <bastien@acinq.fr>
  • Loading branch information
thomash-acinq and t-bast committed Jan 25, 2024
1 parent e66e6d2 commit aae16cf
Show file tree
Hide file tree
Showing 14 changed files with 516 additions and 196 deletions.
25 changes: 22 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Expand Up @@ -180,6 +180,8 @@ trait Eclair {

def payOfferBlocking(offer: Offer, amount: MilliSatoshi, quantity: Long, externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, connectDirectly: Boolean = false)(implicit timeout: Timeout): Future[PaymentEvent]

def payOfferTrampoline(offer: Offer, amount: MilliSatoshi, quantity: Long, trampolineNodeId: PublicKey, trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)], externalId_opt: Option[String] = None, maxAttempts_opt: Option[Int] = None, maxFeeFlat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None, pathFindingExperimentName_opt: Option[String] = None, connectDirectly: Boolean = false)(implicit timeout: Timeout): Future[UUID]

def getOnChainMasterPubKey(account: Long): String

def getDescriptors(account: Long): Descriptors
Expand Down Expand Up @@ -686,6 +688,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
private def payOfferInternal(offer: Offer,
amount: MilliSatoshi,
quantity: Long,
trampolineNodeId_opt: Option[PublicKey],
trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)],
externalId_opt: Option[String],
maxAttempts_opt: Option[Int],
maxFeeFlat_opt: Option[Satoshi],
Expand All @@ -703,7 +707,8 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
.modify(_.boundaries.maxFeeFlat).setToIfDefined(maxFeeFlat_opt.map(_.toMilliSatoshi))
case Left(t) => return Future.failed(t)
}
val sendPaymentConfig = OfferPayment.SendPaymentConfig(externalId_opt, connectDirectly, maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts), routeParams, blocking)
val trampoline = trampolineNodeId_opt.map(trampolineNodeId => OfferPayment.TrampolineConfig(trampolineNodeId, trampolineAttempts))
val sendPaymentConfig = OfferPayment.SendPaymentConfig(externalId_opt, connectDirectly, maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts), routeParams, blocking, trampoline)
val offerPayment = appKit.system.spawnAnonymous(OfferPayment(appKit.nodeParams, appKit.postman, appKit.router, appKit.paymentInitiator))
offerPayment.ask((ref: typed.ActorRef[Any]) => OfferPayment.PayOffer(ref.toClassic, offer, amount, quantity, sendPaymentConfig)).flatMap {
case f: OfferPayment.Failure => Future.failed(new Exception(f.toString))
Expand All @@ -720,7 +725,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
maxFeePct_opt: Option[Double],
pathFindingExperimentName_opt: Option[String],
connectDirectly: Boolean)(implicit timeout: Timeout): Future[UUID] = {
payOfferInternal(offer, amount, quantity, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = false).mapTo[UUID]
payOfferInternal(offer, amount, quantity, None, Nil, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = false).mapTo[UUID]
}

override def payOfferBlocking(offer: Offer,
Expand All @@ -732,7 +737,21 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
maxFeePct_opt: Option[Double],
pathFindingExperimentName_opt: Option[String],
connectDirectly: Boolean)(implicit timeout: Timeout): Future[PaymentEvent] = {
payOfferInternal(offer, amount, quantity, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = true).mapTo[PaymentEvent]
payOfferInternal(offer, amount, quantity, None, Nil, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = true).mapTo[PaymentEvent]
}

override def payOfferTrampoline(offer: Offer,
amount: MilliSatoshi,
quantity: Long,
trampolineNodeId: PublicKey,
trampolineAttempts: Seq[(MilliSatoshi, CltvExpiryDelta)],
externalId_opt: Option[String],
maxAttempts_opt: Option[Int],
maxFeeFlat_opt: Option[Satoshi],
maxFeePct_opt: Option[Double],
pathFindingExperimentName_opt: Option[String],
connectDirectly: Boolean)(implicit timeout: Timeout): Future[UUID] = {
payOfferInternal(offer, amount, quantity, Some(trampolineNodeId), trampolineAttempts, externalId_opt, maxAttempts_opt, maxFeeFlat_opt, maxFeePct_opt, pathFindingExperimentName_opt, connectDirectly, blocking = false).mapTo[UUID]
}

override def getDescriptors(account: Long): Descriptors = appKit.nodeParams.onChainKeyManager_opt match {
Expand Down
Expand Up @@ -55,7 +55,13 @@ object IncomingPaymentPacket {
val expiryDelta: CltvExpiryDelta = add.cltvExpiry - outgoingCltv
}
/** We must relay the payment to a remote node. */
case class NodeRelayPacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.Standard, nextPacket: OnionRoutingPacket) extends RelayPacket
sealed trait NodeRelayPacket extends RelayPacket {
def add: UpdateAddHtlc
def outerPayload: FinalPayload.Standard
def innerPayload: IntermediatePayload.NodeRelay
}
case class RelayToTrampolinePacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.Standard, nextPacket: OnionRoutingPacket) extends NodeRelayPacket
case class RelayToBlindedPathsPacket(add: UpdateAddHtlc, outerPayload: FinalPayload.Standard, innerPayload: IntermediatePayload.NodeRelay.ToBlindedPaths) extends NodeRelayPacket
// @formatter:on

case class DecodedOnionPacket(payload: TlvStream[OnionPaymentPayloadTlv], next_opt: Option[OnionRoutingPacket])
Expand Down Expand Up @@ -150,7 +156,7 @@ object IncomingPaymentPacket {
case DecodedOnionPacket(innerPayload, Some(next)) => validateNodeRelay(add, payload, innerPayload, next)
case DecodedOnionPacket(innerPayload, None) =>
if (innerPayload.get[OutgoingBlindedPaths].isDefined) {
Left(InvalidOnionPayload(UInt64(66102), 0)) // Trampoline to blinded paths is not yet supported.
validateTrampolineToBlindedPaths(add, payload, innerPayload)
} else {
validateTrampolineFinalPayload(add, payload, innerPayload)
}
Expand Down Expand Up @@ -209,13 +215,23 @@ object IncomingPaymentPacket {
}
}

private def validateNodeRelay(add: UpdateAddHtlc, outerPayload: TlvStream[OnionPaymentPayloadTlv], innerPayload: TlvStream[OnionPaymentPayloadTlv], next: OnionRoutingPacket): Either[FailureMessage, NodeRelayPacket] = {
private def validateNodeRelay(add: UpdateAddHtlc, outerPayload: TlvStream[OnionPaymentPayloadTlv], innerPayload: TlvStream[OnionPaymentPayloadTlv], next: OnionRoutingPacket): Either[FailureMessage, RelayToTrampolinePacket] = {
// The outer payload cannot use route blinding, but the inner payload may (but it's not supported yet).
FinalPayload.Standard.validate(outerPayload).left.map(_.failureMessage).flatMap { outerPayload =>
IntermediatePayload.NodeRelay.Standard.validate(innerPayload).left.map(_.failureMessage).flatMap {
case _ if add.amountMsat < outerPayload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat))
case _ if add.cltvExpiry != outerPayload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry))
case innerPayload => Right(NodeRelayPacket(add, outerPayload, innerPayload, next))
case innerPayload => Right(RelayToTrampolinePacket(add, outerPayload, innerPayload, next))
}
}
}

private def validateTrampolineToBlindedPaths(add: UpdateAddHtlc, outerPayload: TlvStream[OnionPaymentPayloadTlv], innerPayload: TlvStream[OnionPaymentPayloadTlv]): Either[FailureMessage, RelayToBlindedPathsPacket] = {
FinalPayload.Standard.validate(outerPayload).left.map(_.failureMessage).flatMap { outerPayload =>
IntermediatePayload.NodeRelay.ToBlindedPaths.validate(innerPayload).left.map(_.failureMessage).flatMap {
case _ if add.amountMsat < outerPayload.amount => Left(FinalIncorrectHtlcAmount(add.amountMsat))
case _ if add.cltvExpiry != outerPayload.expiry => Left(FinalIncorrectCltvExpiry(add.cltvExpiry))
case innerPayload => Right(RelayToBlindedPathsPacket(add, outerPayload, innerPayload))
}
}
}
Expand Down

0 comments on commit aae16cf

Please sign in to comment.