Skip to content

Commit f4efd64

Browse files
pm47t-bast
andauthored
Don't relay buggy extra payments (#2937)
If a payer is buggy and tries to pay the same invoice multiple times, it can lead to an edge case where the recipient accepted the first one and purchased liquidity for it, but didn't purchase additional liquidity and thus cannot receive the duplicate payments. Also, when replaying parts that were waiting for an on-the-fly funding, we set `commit = false` to all parts, instead of just the last one. This optimization caused the payment to be stuck if the last part was unexpectedly rejected (which would for example happen in the buggy payer case described above, before we rejected those extra payments). --------- Co-authored-by: t-bast <[email protected]>
1 parent 4ca8ea0 commit f4efd64

File tree

3 files changed

+17
-17
lines changed

3 files changed

+17
-17
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes
4444
import fr.acinq.eclair.router.Router
4545
import fr.acinq.eclair.wire.protocol
4646
import fr.acinq.eclair.wire.protocol.FailureMessageCodecs.createBadOnionFailure
47-
import fr.acinq.eclair.wire.protocol.{AddFeeCredit, ChannelTlv, CurrentFeeCredit, Error, HasChannelId, HasTemporaryChannelId, LightningMessage, LiquidityAds, NodeAddress, OnTheFlyFundingFailureMessage, OnionMessage, OnionRoutingPacket, RecommendedFeerates, RoutingMessage, SpliceInit, TlvStream, TxAbort, UnknownMessage, Warning, WillAddHtlc, WillFailHtlc, WillFailMalformedHtlc}
47+
import fr.acinq.eclair.wire.protocol.{AddFeeCredit, ChannelTlv, CurrentFeeCredit, Error, HasChannelId, HasTemporaryChannelId, LightningMessage, LiquidityAds, NodeAddress, OnTheFlyFundingFailureMessage, OnionMessage, OnionRoutingPacket, RecommendedFeerates, RoutingMessage, SpliceInit, TemporaryChannelFailure, TlvStream, TxAbort, UnknownMessage, Warning, WillAddHtlc, WillFailHtlc, WillFailMalformedHtlc}
4848

4949
/**
5050
* This actor represents a logical peer. There is one [[Peer]] per unique remote node id at all time.
@@ -276,8 +276,12 @@ class Peer(val nodeParams: NodeParams,
276276
proposal.createFulfillCommands(status.preimage).foreach { case (channelId, cmd) => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) }
277277
pending.copy(proposed = pending.proposed :+ proposal)
278278
case status: OnTheFlyFunding.Status.Funded =>
279-
log.info("received extra payment for on-the-fly funding that has already been funded with txId={} (payment_hash={}, amount={})", status.txId, cmd.paymentHash, cmd.amount)
280-
pending.copy(proposed = pending.proposed :+ OnTheFlyFunding.Proposal(htlc, cmd.upstream))
279+
log.info("rejecting extra payment for on-the-fly funding that has already been funded with txId={} (payment_hash={}, amount={})", status.txId, cmd.paymentHash, cmd.amount)
280+
// The payer is buggy and is paying the same payment_hash multiple times. We could simply claim that
281+
// extra payment for ourselves, but we're nice and instead immediately fail it.
282+
val proposal = OnTheFlyFunding.Proposal(htlc, cmd.upstream)
283+
proposal.createFailureCommands(None).foreach { case (channelId, cmd) => PendingCommandsDb.safeSend(register, nodeParams.db.pendingCommands, channelId, cmd) }
284+
pending
281285
}
282286
case None =>
283287
self ! Peer.OutgoingMessage(htlc, d.peerConnection)

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/OnTheFlyFunding.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,7 @@ object OnTheFlyFunding {
292292
// This lets us detect that this HTLC is an on-the-fly funded HTLC.
293293
val htlcFees = LiquidityAds.FundingFee(remainingFees.min(p.maxFees(htlcMinimum)), cmd.status.txId)
294294
val origin = Origin.Hot(htlcSettledAdapter.toClassic, p.upstream)
295-
// We only sign at the end of the whole batch.
296-
val commit = p.htlc.id == cmd.proposed.last.htlc.id
297-
val add = CMD_ADD_HTLC(cmdAdapter.toClassic, p.htlc.amount - htlcFees.amount, paymentHash, p.htlc.expiry, p.htlc.finalPacket, p.htlc.blinding_opt, 1.0, Some(htlcFees), origin, commit)
295+
val add = CMD_ADD_HTLC(cmdAdapter.toClassic, p.htlc.amount - htlcFees.amount, paymentHash, p.htlc.expiry, p.htlc.finalPacket, p.htlc.blinding_opt, 1.0, Some(htlcFees), origin, commit = true)
298296
cmd.channel ! add
299297
remainingFees - htlcFees.amount
300298
}

eclair-core/src/test/scala/fr/acinq/eclair/payment/relay/OnTheFlyFundingSpec.scala

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
381381
proposeFunding(40_000_000 msat, CltvExpiry(515), paymentHash2, upstream2.head)
382382
signLiquidityPurchase(100_000 sat, LiquidityAds.PaymentDetails.FromFutureHtlc(paymentHash2 :: Nil))
383383
proposeExtraFunding(50_000_000 msat, CltvExpiry(525), paymentHash2, upstream2.last)
384+
register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
384385

385386
// A third funding is signed coming from a trampoline payment.
386387
val paymentHash3 = randomBytes32()
@@ -396,16 +397,16 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
396397
val upstream4 = Upstream.Hot.Trampoline(List(upstreamChannel(60_000_000 msat, CltvExpiry(560), paymentHash4)))
397398
proposeFunding(50_000_000 msat, CltvExpiry(516), paymentHash4, upstream4)
398399

399-
// The first three proposals reach their CLTV expiry.
400+
// The first three proposals reach their CLTV expiry (the extra htlc was already failed).
400401
peer ! CurrentBlockHeight(BlockHeight(515))
401-
val fwds = (0 until 6).map(_ => register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]])
402+
val fwds = (0 until 5).map(_ => register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]])
402403
register.expectNoMessage(100 millis)
403404
fwds.foreach(fwd => {
404405
assert(fwd.message.reason == Right(UnknownNextPeer()))
405406
assert(fwd.message.commit)
406407
})
407-
assert(fwds.map(_.channelId).toSet == (upstream1 ++ upstream2 ++ upstream3.received).map(_.add.channelId).toSet)
408-
assert(fwds.map(_.message.id).toSet == (upstream1 ++ upstream2 ++ upstream3.received).map(_.add.id).toSet)
408+
assert(fwds.map(_.channelId).toSet == (upstream1 ++ upstream2.slice(0, 1) ++ upstream3.received).map(_.add.channelId).toSet)
409+
assert(fwds.map(_.message.id).toSet == (upstream1 ++ upstream2.slice(0, 1) ++ upstream3.received).map(_.add.id).toSet)
409410
awaitCond(nodeParams.db.liquidity.listPendingOnTheFlyFunding(remoteNodeId).isEmpty, interval = 100 millis)
410411
}
411412

@@ -848,6 +849,7 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
848849
val purchase = signLiquidityPurchase(200_000 sat, LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage(preimage :: Nil), channelId, fees, fundingTxIndex = 5, htlcMinimum)
849850
// We receive the last payment *after* signing the funding transaction.
850851
proposeExtraFunding(50_000_000 msat, expiryOut, paymentHash, upstream(2))
852+
register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]]
851853

852854
// Once the splice with the right funding index is locked, we forward HTLCs matching the proposed will_add_htlc.
853855
val channelData = makeChannelData(htlcMinimum)
@@ -858,17 +860,15 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
858860
val adds1 = Seq(
859861
channel.expectMsgType[CMD_ADD_HTLC],
860862
channel.expectMsgType[CMD_ADD_HTLC],
861-
channel.expectMsgType[CMD_ADD_HTLC],
862863
)
863864
adds1.foreach(add => {
864865
assert(add.paymentHash == paymentHash)
865866
assert(add.fundingFee_opt.nonEmpty)
866867
assert(add.fundingFee_opt.get.fundingTxId == purchase.txId)
867868
})
868-
adds1.take(2).foreach(add => assert(!add.commit))
869-
assert(adds1.last.commit)
869+
adds1.foreach(add => assert(add.commit))
870870
assert(adds1.map(_.fundingFee_opt.get.amount).sum == fees.total.toMilliSatoshi)
871-
assert(adds1.map(add => add.amount + add.fundingFee_opt.get.amount).sum == 160_000_000.msat)
871+
assert(adds1.map(add => add.amount + add.fundingFee_opt.get.amount).sum == 110_000_000.msat)
872872
channel.expectNoMessage(100 millis)
873873

874874
// The recipient fails the payments: we don't relay the failure upstream and will retry.
@@ -887,7 +887,6 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
887887
val adds2 = Seq(
888888
channel.expectMsgType[CMD_ADD_HTLC],
889889
channel.expectMsgType[CMD_ADD_HTLC],
890-
channel.expectMsgType[CMD_ADD_HTLC],
891890
)
892891
adds2.foreach(add => add.replyTo ! RES_SUCCESS(add, purchase.channelId))
893892
channel.expectNoMessage(100 millis)
@@ -900,9 +899,8 @@ class OnTheFlyFundingSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike {
900899
val fwds = Seq(
901900
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]],
902901
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]],
903-
register.expectMsgType[Register.Forward[CMD_FULFILL_HTLC]],
904902
)
905-
val (channelsIn, htlcsIn) = upstream.flatMap {
903+
val (channelsIn, htlcsIn) = upstream.take(2).flatMap {
906904
case u: Hot.Channel => Seq(u)
907905
case u: Hot.Trampoline => u.received
908906
case _: Upstream.Local => Nil

0 commit comments

Comments
 (0)