1616
1717package fr .acinq .eclair .payment .send
1818
19+ import akka .actor .typed .eventstream .EventStream
1920import akka .actor .typed .scaladsl .adapter ._
2021import akka .actor .typed .scaladsl .{ActorContext , Behaviors }
2122import akka .actor .typed .{ActorRef , Behavior }
@@ -27,6 +28,8 @@ import fr.acinq.eclair.io.Peer
2728import fr .acinq .eclair .payment .OutgoingPaymentPacket .{NodePayload , buildOnion }
2829import fr .acinq .eclair .payment .PaymentSent .PartialPayment
2930import fr .acinq .eclair .payment ._
31+ import fr .acinq .eclair .payment .send .TrampolinePayment .{buildOutgoingPayment , computeFees }
32+ import fr .acinq .eclair .router .Router
3033import fr .acinq .eclair .router .Router .RouteParams
3134import fr .acinq .eclair .wire .protocol .{PaymentOnion , PaymentOnionCodecs }
3235import fr .acinq .eclair .{CltvExpiry , CltvExpiryDelta , Features , Logs , MilliSatoshi , NodeParams , randomBytes32 }
@@ -54,9 +57,9 @@ object TrampolinePaymentLifecycle {
5457 require(invoice.amount_opt.nonEmpty, " amount-less invoices are not supported in trampoline tests" )
5558 }
5659 private case class TrampolinePeerNotFound (trampolineNodeId : PublicKey ) extends Command
60+ private case class CouldntAddHtlc (failure : Throwable ) extends Command
61+ private case class HtlcSettled (result : HtlcResult , part : PartialPayment , holdTimes : Seq [Sphinx .HoldTime ]) extends Command
5762 private case class WrappedPeerChannels (channels : Seq [Peer .ChannelInfo ]) extends Command
58- private case class WrappedAddHtlcResponse (response : CommandResponse [CMD_ADD_HTLC ]) extends Command
59- private case class WrappedHtlcSettled (result : RES_ADD_SETTLED [Origin .Hot , HtlcResult ]) extends Command
6063 // @formatter:on
6164
6265 def apply (nodeParams : NodeParams , register : ActorRef [Register .ForwardNodeId [Peer .GetPeerChannels ]]): Behavior [Command ] =
@@ -75,6 +78,79 @@ object TrampolinePaymentLifecycle {
7578 }
7679 }
7780
81+ object PartHandler {
82+ sealed trait Command
83+
84+ private case class WrappedAddHtlcResponse (response : CommandResponse [CMD_ADD_HTLC ]) extends Command
85+
86+ private case class WrappedHtlcSettled (result : RES_ADD_SETTLED [Origin .Hot , HtlcResult ]) extends Command
87+
88+ def apply (parent : ActorRef [TrampolinePaymentLifecycle .Command ],
89+ cmd : TrampolinePaymentLifecycle .SendPayment ,
90+ amount : MilliSatoshi ,
91+ channelInfo : Peer .ChannelInfo ,
92+ expiry : CltvExpiry ,
93+ trampolinePaymentSecret : ByteVector32 ,
94+ attemptNumber : Int ): Behavior [Command ] =
95+ Behaviors .setup { context =>
96+ new PartHandler (context, parent, cmd).start(amount, channelInfo, expiry, trampolinePaymentSecret, attemptNumber)
97+ }
98+ }
99+
100+ class PartHandler (context : ActorContext [PartHandler .Command ], parent : ActorRef [Command ], cmd : TrampolinePaymentLifecycle .SendPayment ) {
101+
102+ import PartHandler ._
103+
104+ private val paymentHash = cmd.invoice.paymentHash
105+
106+ private val addHtlcAdapter = context.messageAdapter[CommandResponse [CMD_ADD_HTLC ]](WrappedAddHtlcResponse )
107+ private val htlcSettledAdapter = context.messageAdapter[RES_ADD_SETTLED [Origin .Hot , HtlcResult ]](WrappedHtlcSettled )
108+
109+ def start (amount : MilliSatoshi , channelInfo : Peer .ChannelInfo , expiry : CltvExpiry , trampolinePaymentSecret : ByteVector32 , attemptNumber : Int ): Behavior [PartHandler .Command ] = {
110+ val origin = Origin .Hot (htlcSettledAdapter.toClassic, Upstream .Local (cmd.paymentId))
111+ val outgoing = buildOutgoingPayment(cmd.trampolineNodeId, cmd.invoice, amount, expiry, Some (trampolinePaymentSecret), attemptNumber)
112+ val add = CMD_ADD_HTLC (addHtlcAdapter.toClassic, outgoing.trampolineAmount, paymentHash, outgoing.trampolineExpiry, outgoing.onion.packet, None , 1.0 , None , origin, commit = true )
113+ channelInfo.channel ! add
114+ val channelId = channelInfo.data.asInstanceOf [DATA_NORMAL ].channelId
115+ val part = PartialPayment (cmd.paymentId, amount, computeFees(amount, attemptNumber), channelId, None )
116+ waitForSettlement(part, outgoing.onion.sharedSecrets, outgoing.trampolineOnion.sharedSecrets)
117+ }
118+
119+ def waitForSettlement (part : PartialPayment , outerOnionSecrets : Seq [Sphinx .SharedSecret ], trampolineOnionSecrets : Seq [Sphinx .SharedSecret ]): Behavior [PartHandler .Command ] = {
120+ Behaviors .receiveMessagePartial {
121+ case WrappedAddHtlcResponse (response) => response match {
122+ case _ : CommandSuccess [_] =>
123+ // HTLC was correctly sent out.
124+ Behaviors .same
125+ case failure : CommandFailure [_, Throwable ] =>
126+ parent ! CouldntAddHtlc (failure.t)
127+ Behaviors .stopped
128+ }
129+ case WrappedHtlcSettled (result) => result.result match {
130+ case fulfill : HtlcResult .Fulfill =>
131+ val holdTimes = fulfill match {
132+ case HtlcResult .RemoteFulfill (updateFulfill) =>
133+ updateFulfill.attribution_opt match {
134+ case Some (attribution) =>
135+ Sphinx .Attribution .fulfillHoldTimes(attribution, outerOnionSecrets).holdTimes
136+ case None => Nil
137+ }
138+ case _ : HtlcResult .OnChainFulfill => Nil
139+ }
140+ parent ! HtlcSettled (fulfill, part, holdTimes)
141+ Behaviors .stopped
142+ case fail : HtlcResult .Fail =>
143+ val holdTimes = fail match {
144+ case HtlcResult .RemoteFail (updateFail) =>
145+ Sphinx .FailurePacket .decrypt(updateFail.reason, updateFail.attribution_opt, outerOnionSecrets).holdTimes
146+ case _ => Nil
147+ }
148+ parent ! HtlcSettled (fail, part, holdTimes)
149+ Behaviors .stopped
150+ }
151+ }
152+ }
153+ }
78154}
79155
80156class TrampolinePaymentLifecycle private (nodeParams : NodeParams ,
@@ -90,8 +166,6 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
90166
91167 private val forwardNodeIdFailureAdapter = context.messageAdapter[Register .ForwardNodeIdFailure [Peer .GetPeerChannels ]](_ => TrampolinePeerNotFound (cmd.trampolineNodeId))
92168 private val peerChannelsResponseAdapter = context.messageAdapter[Peer .PeerChannels ](c => WrappedPeerChannels (c.channels))
93- private val addHtlcAdapter = context.messageAdapter[CommandResponse [CMD_ADD_HTLC ]](WrappedAddHtlcResponse )
94- private val htlcSettledAdapter = context.messageAdapter[RES_ADD_SETTLED [Origin .Hot , HtlcResult ]](WrappedHtlcSettled )
95169
96170 def start (): Behavior [Command ] = listChannels(attemptNumber = 0 )
97171
@@ -117,7 +191,6 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
117191 case _ => None
118192 }
119193 })
120- val origin = Origin .Hot (htlcSettledAdapter.toClassic, Upstream .Local (cmd.paymentId))
121194 val expiry = CltvExpiry (nodeParams.currentBlockHeight) + CltvExpiryDelta (36 )
122195 if (filtered.isEmpty) {
123196 context.log.warn(" no usable channel with trampoline node {}" , cmd.trampolineNodeId)
@@ -131,54 +204,49 @@ class TrampolinePaymentLifecycle private(nodeParams: NodeParams,
131204 // We generate a random secret to avoid leaking the invoice secret to the trampoline node.
132205 val trampolinePaymentSecret = randomBytes32()
133206 context.log.info(" sending trampoline payment parts: {}->{}, {}->{}" , channel1.data.channelId, amount1, channel2.data.channelId, amount2)
134- val parts = Seq ((amount1, channel1), (amount2, channel2)).map { case (amount, channelInfo) =>
135- val outgoing = buildOutgoingPayment(cmd.trampolineNodeId, cmd.invoice, amount, expiry, Some (trampolinePaymentSecret), attemptNumber)
136- val add = CMD_ADD_HTLC (addHtlcAdapter.toClassic, outgoing.trampolineAmount, paymentHash, outgoing.trampolineExpiry, outgoing.onion.packet, None , 1.0 , None , origin, commit = true )
137- channelInfo.channel ! add
138- val channelId = channelInfo.data.asInstanceOf [DATA_NORMAL ].channelId
139- PartialPayment (cmd.paymentId, amount, computeFees(amount, attemptNumber), channelId, None )
207+ Seq ((amount1, channel1), (amount2, channel2)).foreach { case (amount, channelInfo) =>
208+ context.spawnAnonymous(PartHandler (context.self, cmd, amount, channelInfo, expiry, trampolinePaymentSecret, attemptNumber))
140209 }
141- waitForSettlement(remaining = 2 , attemptNumber, parts )
210+ waitForSettlement(remaining = 2 , attemptNumber, Nil )
142211 }
143212 }
144213
145- private def waitForSettlement (remaining : Int , attemptNumber : Int , parts : Seq [PartialPayment ]): Behavior [Command ] = {
214+ private def waitForSettlement (remaining : Int , attemptNumber : Int , fulfilledParts : Seq [PartialPayment ]): Behavior [Command ] = {
146215 Behaviors .receiveMessagePartial {
147- case WrappedAddHtlcResponse (response) => response match {
148- case _ : CommandSuccess [_] =>
149- // HTLC was correctly sent out.
150- Behaviors .same
151- case failure : CommandFailure [_, Throwable ] =>
152- context.log.warn(" HTLC could not be sent: {}" , failure.t.getMessage)
153- if (remaining > 1 ) {
154- context.log.info(" waiting for remaining HTLCs to complete" )
155- waitForSettlement(remaining - 1 , attemptNumber, parts)
156- } else {
157- context.log.warn(" trampoline payment failed" )
158- cmd.replyTo ! PaymentFailed (cmd.paymentId, paymentHash, LocalFailure (totalAmount, Nil , failure.t) :: Nil )
159- Behaviors .stopped
160- }
161- }
162- case WrappedHtlcSettled (result) => result.result match {
163- case fulfill : HtlcResult .Fulfill =>
164- context.log.info(" HTLC was fulfilled" )
165- if (remaining > 1 ) {
166- context.log.info(" waiting for remaining HTLCs to be fulfilled" )
167- waitForSettlement(remaining - 1 , attemptNumber, parts)
168- } else {
169- context.log.info(" trampoline payment succeeded" )
170- cmd.replyTo ! PaymentSent (cmd.paymentId, paymentHash, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, parts, None )
171- Behaviors .stopped
172- }
173- case fail : HtlcResult .Fail =>
174- context.log.warn(" received HTLC failure: {}" , fail)
175- if (remaining > 1 ) {
176- context.log.info(" waiting for remaining HTLCs to be failed" )
177- waitForSettlement(remaining - 1 , attemptNumber, parts)
178- } else {
179- retryOrStop(attemptNumber + 1 )
180- }
181- }
216+ case CouldntAddHtlc (failure) =>
217+ context.log.warn(" HTLC could not be sent: {}" , failure.getMessage)
218+ if (remaining > 1 ) {
219+ context.log.info(" waiting for remaining HTLCs to complete" )
220+ waitForSettlement(remaining - 1 , attemptNumber, fulfilledParts)
221+ } else {
222+ context.log.warn(" trampoline payment failed" )
223+ cmd.replyTo ! PaymentFailed (cmd.paymentId, paymentHash, LocalFailure (totalAmount, Nil , failure) :: Nil )
224+ Behaviors .stopped
225+ }
226+ case HtlcSettled (result : HtlcResult , part, holdTimes) =>
227+ if (holdTimes.nonEmpty) {
228+ context.system.eventStream ! EventStream .Publish (Router .ReportedHoldTimes (holdTimes))
229+ }
230+ result match {
231+ case fulfill : HtlcResult .Fulfill =>
232+ context.log.info(" HTLC was fulfilled" )
233+ if (remaining > 1 ) {
234+ context.log.info(" waiting for remaining HTLCs to be fulfilled" )
235+ waitForSettlement(remaining - 1 , attemptNumber, part +: fulfilledParts)
236+ } else {
237+ context.log.info(" trampoline payment succeeded" )
238+ cmd.replyTo ! PaymentSent (cmd.paymentId, paymentHash, fulfill.paymentPreimage, totalAmount, cmd.invoice.nodeId, part +: fulfilledParts, None )
239+ Behaviors .stopped
240+ }
241+ case fail : HtlcResult .Fail =>
242+ context.log.warn(" received HTLC failure: {}" , fail)
243+ if (remaining > 1 ) {
244+ context.log.info(" waiting for remaining HTLCs to be failed" )
245+ waitForSettlement(remaining - 1 , attemptNumber, fulfilledParts)
246+ } else {
247+ retryOrStop(attemptNumber + 1 )
248+ }
249+ }
182250 }
183251 }
184252
0 commit comments