diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index e09963daf6..6104729160 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -190,6 +190,8 @@ object Commitments { } else if (missingForReceiver < 0.msat) { if (commitments.localParams.isFunder) { // receiver is fundee; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment + } else if (missingForReceiver + commitments1.localParams.channelReserve > 0.msat && outgoingHtlcs.size <= 1) { + // receiver is funder; it is allowed to dip into its channel reserve to pay the fee for a single non-dust HTLC } else { return Left(RemoteCannotAffordFeesForNewHtlc(commitments.channelId, amount = cmd.amount, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees)) } @@ -230,10 +232,12 @@ object Commitments { if (missingForSender < 0.sat) { throw InsufficientFunds(commitments.channelId, amount = add.amountMsat, missing = -missingForSender.truncateToSatoshi, reserve = commitments1.localParams.channelReserve, fees = if (commitments1.localParams.isFunder) 0.sat else fees) } else if (missingForReceiver < 0.sat) { - if (commitments.localParams.isFunder) { - throw CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees) - } else { + if (!commitments.localParams.isFunder) { // receiver is fundee; it is ok if it can't maintain its channel_reserve for now, as long as its balance is increasing, which is the case if it is receiving a payment + } else if (missingForReceiver + commitments1.remoteParams.channelReserve > 0.msat && incomingHtlcs.size <= 1) { + // receiver is funder; it is allowed to dip into its channel reserve to pay the fee for a single non-dust HTLC + } else { + throw CannotAffordFees(commitments.channelId, missing = -missingForReceiver.truncateToSatoshi, reserve = commitments1.remoteParams.channelReserve, fees = fees) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index f66bfbdd0d..a7bfb5acb7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -216,10 +216,21 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test begins // at this point alice has the minimal amount to sustain a channel (29000 sat ~= alice reserve + commit fee) - val add = CMD_ADD_HTLC(120000000 msat, randomBytes32, CltvExpiry(400144), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID())) + // alice should be allowed to dip into her reserve to pay for the commit tx fee increase (see https://github.com/lightningnetwork/lightning-rfc/issues/728) + sender.send(bob, CMD_ADD_HTLC(85000000 msat, randomBytes32, CltvExpiry(400144), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID()))) + sender.expectMsg("ok") + val htlc = bob2alice.expectMsgType[UpdateAddHtlc] + bob2alice.forward(alice) + + // but only one pending HTLC is allowed to dip into alice's reserve; bob must wait for that HTLC to settle before sending another one + val add = CMD_ADD_HTLC(80000000 msat, randomBytes32, CltvExpiry(400144), TestConstants.emptyOnionPacket, Upstream.Local(UUID.randomUUID())) sender.send(bob, add) - val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), add.amount, missing = 1680 sat, 10000 sat, 10680 sat) + val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), add.amount, missing = 3400 sat, 10000 sat, 12400 sat) sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Origin.Local(add.upstream.asInstanceOf[Upstream.Local].id, Some(sender.ref)), Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate), Some(add)))) + bob2alice.expectNoMsg(100 millis) + + // alice should accept the first incoming htlc + awaitCond(alice.stateData.asInstanceOf[HasCommitments].commitments.remoteChanges.proposed.contains(htlc)) } test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)") { f =>