Skip to content

Commit d272226

Browse files
committed
Change to safe resume supervision strategy
1 parent cc1e298 commit d272226

5 files changed

Lines changed: 66 additions & 49 deletions

File tree

plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapMaker.scala

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -210,35 +210,39 @@ private class SwapMaker(remoteNodeId: PublicKey, shortChannelId: ShortChannelId,
210210
}
211211
}
212212

213-
def createOpeningTx(request: SwapRequest, agreement: SwapAgreement, isInitiator: Boolean): Behavior[SwapCommand] = {
214-
val receivePayment = ReceiveStandardPayment(Some(toMilliSatoshi(Satoshi(request.amount))), Left("send-swap-in"))
215-
val createInvoice = context.spawnAnonymous(CreateInvoiceActor(nodeParams))
216-
createInvoice ! CreateInvoiceActor.CreateInvoice(context.messageAdapter[Bolt11Invoice](InvoiceResponse).toClassic, receivePayment)
217-
218-
receiveSwapMessage[CreateOpeningTxMessages](context, "createOpeningTx") {
219-
case InvoiceResponse(invoice: Bolt11Invoice) => fundOpening(wallet, feeRatePerKw)((request.amount + agreement.premium).sat, makerPubkey(request.swapId), takerPubkey(request, agreement, isInitiator), invoice)
220-
Behaviors.same
221-
case OpeningTxFunded(invoice, fundingResponse) =>
222-
commitOpening(wallet)(request.swapId, invoice, fundingResponse, "swap-in-sender-opening")
223-
Behaviors.same
224-
case OpeningTxCommitted(invoice, openingTxBroadcasted) =>
225-
db.add(SwapData(request, agreement, invoice, openingTxBroadcasted, Maker, isInitiator, remoteNodeId))
226-
awaitClaimPayment(request, agreement, invoice, openingTxBroadcasted, isInitiator)
227-
case OpeningTxFailed(error, None) => swapCanceled(InternalError(request.swapId, s"failed to fund swap open tx, error: $error"))
228-
case OpeningTxFailed(error, Some(r)) => rollback(wallet)(error, r.fundingTx)
229-
Behaviors.same
230-
case RollbackSuccess(error, value) => swapCanceled(InternalError(request.swapId, s"rollback: Success($value), error: $error"))
231-
case RollbackFailure(error, t) => swapCanceled(InternalError(request.swapId, s"rollback exception: $t, error: $error"))
232-
case SwapMessageReceived(_) => Behaviors.same // ignore
233-
case StateTimeout =>
234-
// TODO: are we sure the opening transaction has not yet been committed? should we rollback locked funding outputs?
235-
swapCanceled(InternalError(request.swapId, "timeout during CreateOpeningTx"))
236-
case CancelRequested(replyTo) => replyTo ! SwapError(request.swapId, "Can not cancel swap after opening tx committed.")
237-
Behaviors.same // ignore
238-
case GetStatus(replyTo) => replyTo ! SwapStatus(request.swapId, context.self.toString, "createOpeningTx", request, Some(agreement))
239-
Behaviors.same
213+
def createOpeningTx(request: SwapRequest, agreement: SwapAgreement, isInitiator: Boolean): Behavior[SwapCommand] =
214+
db.find(request.swapId) match {
215+
case Some(s: SwapData) =>
216+
awaitClaimPayment(request, agreement, s.invoice, s.openingTxBroadcasted, isInitiator)
217+
case None =>
218+
val receivePayment = ReceiveStandardPayment(Some(toMilliSatoshi(Satoshi(request.amount))), Left("send-swap-in"))
219+
val createInvoice = context.spawnAnonymous(CreateInvoiceActor(nodeParams))
220+
createInvoice ! CreateInvoiceActor.CreateInvoice(context.messageAdapter[Bolt11Invoice](InvoiceResponse).toClassic, receivePayment)
221+
222+
receiveSwapMessage[CreateOpeningTxMessages](context, "createOpeningTx") {
223+
case InvoiceResponse(invoice: Bolt11Invoice) => fundOpening(wallet, feeRatePerKw)((request.amount + agreement.premium).sat, makerPubkey(request.swapId), takerPubkey(request, agreement, isInitiator), invoice)
224+
Behaviors.same
225+
case OpeningTxFunded(invoice, fundingResponse) =>
226+
commitOpening(wallet)(request.swapId, invoice, fundingResponse, "swap-in-sender-opening")
227+
Behaviors.same
228+
case OpeningTxCommitted(invoice, openingTxBroadcasted) =>
229+
db.add(SwapData(request, agreement, invoice, openingTxBroadcasted, Maker, isInitiator, remoteNodeId))
230+
awaitClaimPayment(request, agreement, invoice, openingTxBroadcasted, isInitiator)
231+
case OpeningTxFailed(error, None) => swapCanceled(InternalError(request.swapId, s"failed to fund swap open tx, error: $error"))
232+
case OpeningTxFailed(error, Some(r)) => rollback(wallet)(error, r.fundingTx)
233+
Behaviors.same
234+
case RollbackSuccess(error, value) => swapCanceled(InternalError(request.swapId, s"rollback: Success($value), error: $error"))
235+
case RollbackFailure(error, t) => swapCanceled(InternalError(request.swapId, s"rollback exception: $t, error: $error"))
236+
case SwapMessageReceived(_) => Behaviors.same // ignore
237+
case StateTimeout =>
238+
// TODO: are we sure the opening transaction has not yet been committed? should we rollback locked funding outputs?
239+
swapCanceled(InternalError(request.swapId, "timeout during CreateOpeningTx"))
240+
case CancelRequested(replyTo) => replyTo ! SwapError(request.swapId, "Can not cancel swap after opening tx committed.")
241+
Behaviors.same // ignore
242+
case GetStatus(replyTo) => replyTo ! SwapStatus(request.swapId, context.self.toString, "createOpeningTx", request, Some(agreement))
243+
Behaviors.same
244+
}
240245
}
241-
}
242246

243247
def awaitClaimPayment(request: SwapRequest, agreement: SwapAgreement, invoice: Bolt11Invoice, openingTxBroadcasted: OpeningTxBroadcasted, isInitiator: Boolean): Behavior[SwapCommand] =
244248
nodeParams.db.payments.getIncomingPayment(invoice.paymentHash) match {

plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapRegister.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@ private class SwapRegister(context: ActorContext[Command], nodeParams: NodeParam
8888

8989
private def spawnSwap(swapRole: SwapRole, remoteNodeId: PublicKey, scid: String) = {
9090
swapRole match {
91-
case SwapRole.Maker => context.spawn(Behaviors.supervise(SwapMaker(remoteNodeId, nodeParams, watcher, switchboard, wallet, keyManager, db)).onFailure(typed.SupervisorStrategy.stop), "SwapMaker-" + scid)
92-
case SwapRole.Taker => context.spawn(Behaviors.supervise(SwapTaker(remoteNodeId, nodeParams, paymentInitiator, watcher, switchboard, wallet, keyManager, db)).onFailure(typed.SupervisorStrategy.stop), "SwapTaker-" + scid)
91+
// swap maker is safe to resume because an opening transaction will only be funded once
92+
case SwapRole.Maker => context.spawn(Behaviors.supervise(SwapMaker(remoteNodeId, nodeParams, watcher, switchboard, wallet, keyManager, db)).onFailure(typed.SupervisorStrategy.resume), "SwapMaker-" + scid)
93+
// swap taker is safe to resume because a payment will only be sent once
94+
case SwapRole.Taker => context.spawn(Behaviors.supervise(SwapTaker(remoteNodeId, nodeParams, paymentInitiator, watcher, switchboard, wallet, keyManager, db)).onFailure(typed.SupervisorStrategy.resume), "SwapTaker-" + scid)
9395
}
9496
}
9597

plugins/peerswap/src/main/scala/fr/acinq/eclair/plugins/peerswap/SwapTaker.scala

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -260,26 +260,29 @@ private class SwapTaker(remoteNodeId: PublicKey, shortChannelId: ShortChannelId,
260260
}
261261
}
262262

263-
def validateOpeningTx(request: SwapRequest, agreement: SwapAgreement, openingTxBroadcasted: OpeningTxBroadcasted, openingTx: Transaction, isInitiator: Boolean): Behavior[SwapCommand] = {
264-
Bolt11Invoice.fromString(openingTxBroadcasted.payreq) match {
265-
case Failure(e) => sendCoopClose(request, s"Could not parse payreq: $e")
266-
case Success(invoice) if invoice.amount_opt.isDefined && invoice.amount_opt.get > request.amount.sat.toMilliSatoshi =>
267-
sendCoopClose(request, s"Invoice amount ${invoice.amount_opt.get} > requested on-chain amount ${request.amount.sat.toMilliSatoshi}")
268-
case Success(invoice) if invoice.routingInfo.flatten.exists(hop => hop.shortChannelId != shortChannelId) =>
269-
sendCoopClose(request, s"Channel hop other than $shortChannelId found in invoice hints ${invoice.routingInfo}")
270-
case Success(invoice) if invoice.isExpired() =>
271-
sendCoopClose(request, s"Invoice is expired.")
272-
case Success(invoice) if invoice.minFinalCltvExpiryDelta >= CltvExpiryDelta(claimByCsvDelta.toInt / 2) =>
273-
sendCoopClose(request, s"Invoice min-final-cltv-expiry delta too long.")
274-
case Success(invoice) if validOpeningTx(openingTx, openingTxBroadcasted.scriptOut, (request.amount + agreement.premium).sat, makerPubkey(request, agreement, isInitiator), takerPubkey(request.swapId), invoice.paymentHash) =>
275-
// save restore point before a payment is initiated
276-
db.add(SwapData(request, agreement, invoice, openingTxBroadcasted, Taker, isInitiator, remoteNodeId))
277-
payInvoice(nodeParams)(paymentInitiator, request.swapId, invoice)
278-
payClaimInvoice(request, agreement, openingTxBroadcasted, invoice, isInitiator)
279-
case Success(_) =>
280-
sendCoopClose(request, s"Invalid opening tx: $openingTx")
263+
def validateOpeningTx(request: SwapRequest, agreement: SwapAgreement, openingTxBroadcasted: OpeningTxBroadcasted, openingTx: Transaction, isInitiator: Boolean): Behavior[SwapCommand] =
264+
db.find(request.swapId) match {
265+
case Some(s: SwapData) => payClaimInvoice(request, agreement, openingTxBroadcasted, s.invoice, isInitiator)
266+
case None =>
267+
Bolt11Invoice.fromString(openingTxBroadcasted.payreq) match {
268+
case Failure(e) => sendCoopClose(request, s"Could not parse payreq: $e")
269+
case Success(invoice) if invoice.amount_opt.isDefined && invoice.amount_opt.get > request.amount.sat.toMilliSatoshi =>
270+
sendCoopClose(request, s"Invoice amount ${invoice.amount_opt.get} > requested on-chain amount ${request.amount.sat.toMilliSatoshi}")
271+
case Success(invoice) if invoice.routingInfo.flatten.exists(hop => hop.shortChannelId != shortChannelId) =>
272+
sendCoopClose(request, s"Channel hop other than $shortChannelId found in invoice hints ${invoice.routingInfo}")
273+
case Success(invoice) if invoice.isExpired() =>
274+
sendCoopClose(request, s"Invoice is expired.")
275+
case Success(invoice) if invoice.minFinalCltvExpiryDelta >= CltvExpiryDelta(claimByCsvDelta.toInt / 2) =>
276+
sendCoopClose(request, s"Invoice min-final-cltv-expiry delta too long.")
277+
case Success(invoice) if validOpeningTx(openingTx, openingTxBroadcasted.scriptOut, (request.amount + agreement.premium).sat, makerPubkey(request, agreement, isInitiator), takerPubkey(request.swapId), invoice.paymentHash) =>
278+
// save restore point before a payment is initiated
279+
db.add(SwapData(request, agreement, invoice, openingTxBroadcasted, Taker, isInitiator, remoteNodeId))
280+
payInvoice(nodeParams)(paymentInitiator, request.swapId, invoice)
281+
payClaimInvoice(request, agreement, openingTxBroadcasted, invoice, isInitiator)
282+
case Success(_) =>
283+
sendCoopClose(request, s"Invalid opening tx: $openingTx")
284+
}
281285
}
282-
}
283286

284287
def payClaimInvoice(request: SwapRequest, agreement: SwapAgreement, openingTxBroadcasted: OpeningTxBroadcasted, invoice: Bolt11Invoice, isInitiator: Boolean): Behavior[SwapCommand] = {
285288
watchForPayment(watch = true) // subscribe to payment event notifications

plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapInReceiverSpec.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ case class SwapInReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory.
128128

129129
// the swap result has been recorded in the db
130130
assert(db.list().head.result.contains("Coop close offered to peer: Lightning payment not sent."))
131+
db.remove(swapId)
131132
}
132133

133134
test("send cooperative close after a restore with the payment already marked as failed") { f =>
@@ -153,6 +154,7 @@ case class SwapInReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory.
153154

154155
// the swap result has been recorded in the db
155156
assert(db.list().head.result.contains("Coop close offered to peer: Lightning payment failed"))
157+
db.remove(swapId)
156158
}
157159

158160
test("claim by invoice after a restore with the payment already marked as paid") { f =>
@@ -189,6 +191,7 @@ case class SwapInReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory.
189191

190192
// the swap result has been recorded in the db
191193
assert(db.list().head.result.contains("Claimed by paid invoice:"))
194+
db.remove(swapId)
192195
}
193196

194197
test("claim by invoice after a restore with the payment marked as pending and later paid") { f =>
@@ -233,6 +236,7 @@ case class SwapInReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory.
233236

234237
// the swap result has been recorded in the db
235238
assert(db.list().head.result.contains("Claimed by paid invoice:"))
239+
db.remove(swapId)
236240
}
237241

238242
test("happy path for new swap in") { f =>
@@ -289,6 +293,7 @@ case class SwapInReceiverSpec() extends ScalaTestWithActorTestKit(ConfigFactory.
289293

290294
// the swap result has been recorded in the db
291295
assert(db.list().head.result.contains("Claimed by paid invoice:"))
296+
db.remove(swapId)
292297
}
293298

294299
test("invalid invoice, min_final-cltv-expiry of invoice greater than the claim-by-csv delta") { f =>

plugins/peerswap/src/test/scala/fr/acinq/eclair/plugins/peerswap/SwapInSenderSpec.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ case class SwapInSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.lo
137137

138138
// the swap result has been recorded in the db
139139
assert(db.list().head.result.contains("Invoice payment received"))
140+
db.remove(swapId)
140141
}
141142

142143
test("happy path for new swap") { f =>
@@ -177,6 +178,7 @@ case class SwapInSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.lo
177178

178179
// the swap result has been recorded in the db
179180
assert(db.list().head.result.contains("Invoice payment received"))
181+
db.remove(swapId)
180182
}
181183

182184
test("claim refund by coop close path from restored swap") { f =>
@@ -217,6 +219,7 @@ case class SwapInSenderSpec() extends ScalaTestWithActorTestKit(ConfigFactory.lo
217219

218220
// the swap result has been recorded in the db
219221
assert(db.list().head.result.contains("Claimed by coop"))
222+
db.remove(swapId)
220223
}
221224

222225
test("claim refund by csv path from restored swap") { f =>

0 commit comments

Comments
 (0)