Skip to content

Commit

Permalink
Dip into remote initiator reserve only for splices (#2797)
Browse files Browse the repository at this point in the history
#2761 introduced the ability for the HTLC sender to let a remote initiator
dip into its reserve to unblock channels after a large splice. However, we
relaxed that condition for all channels, even those that don't use splice.
This creates compatibility issues with other implementations that are
stricter than what the specification requires, and will force-close in
those situations.
  • Loading branch information
t-bast committed Dec 8, 2023
1 parent be4ed3c commit 9c4aad0
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ case class Commitment(fundingTxIndex: Long,
} else if (missingForReceiver < 0.msat) {
if (params.localParams.isInitiator) {
// receiver is not the channel initiator; 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 (reduced.toLocal > fees && reduced.htlcs.size < 5) {
} else if (reduced.toLocal > fees && reduced.htlcs.size < 5 && fundingTxIndex > 0) {
// Receiver is the channel initiator; we usually don't want to let them dip into their channel reserve, because
// that may give them a commitment transaction where they have nothing at stake, which would create an incentive
// for them to force-close using that commitment after it has been revoked.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,35 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.proposed.size == proposedChanges + 1)
}

test("recv CMD_ADD_HTLC (HTLC dips into remote funder channel reserve)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
addHtlc(758_640_000 msat, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.availableBalanceForSend == 0.msat)
// We increase the feerate to get Alice's balance closer to her channel reserve.
bob.underlyingActor.nodeParams.setFeerates(FeeratesPerKw.single(FeeratePerKw(17_500 sat)))
updateFee(FeeratePerKw(17_500 sat), alice, bob, alice2bob, bob2alice)

// At this point alice has the minimal amount to sustain a channel.
// Alice maintains an extra reserve to accommodate for a one more HTLCs, so the first few HTLCs should be allowed.
bob ! CMD_ADD_HTLC(sender.ref, 25_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, localOrigin(sender.ref))
sender.expectMsgType[RES_SUCCESS[CMD_ADD_HTLC]]
val add = bob2alice.expectMsgType[UpdateAddHtlc]
bob2alice.forward(alice, add)

// But this one will dip alice below her reserve: we must wait for the previous HTLCs to settle before sending any more.
val failedAdd = CMD_ADD_HTLC(sender.ref, 25_000_000 msat, randomBytes32(), CltvExpiry(400144), TestConstants.emptyOnionPacket, None, localOrigin(sender.ref))
bob ! failedAdd
val error = RemoteCannotAffordFeesForNewHtlc(channelId(bob), failedAdd.amount, missing = 340 sat, 20_000 sat, 21_700 sat)
sender.expectMsg(RES_ADD_FAILED(failedAdd, error, Some(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate)))

// If Bob had sent this HTLC, Alice would have accepted dipping into her reserve.
val proposedChanges = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.proposed.size
alice ! add.copy(id = add.id + 1)
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.changes.remoteChanges.proposed.size == proposedChanges + 1)
}

test("recv CMD_ADD_HTLC (insufficient funds w/ pending htlcs and 0 balance)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
import f._
val sender = TestProbe()
Expand Down

0 comments on commit 9c4aad0

Please sign in to comment.