Skip to content

Commit 830335f

Browse files
authored
Avoid unusable channels after a large splice (#2761)
Splicing (and dual funding as well) introduce a new scenario that could not happen before, where the channel initiator (who pays the fees for the commit tx) can end up below the channel reserve, or right above it. In that case it means that most of the channels funds are on the non initiator side, so we should allow HTLCs from the non-initiator to the initiator to move funds towards the initiator. We allow slightly dipping into the channel reserve in that case, for at most 5 pending HTLCs.
1 parent 12adf87 commit 830335f

File tree

2 files changed

+106
-57
lines changed

2 files changed

+106
-57
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

+13
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,19 @@ case class Commitment(fundingTxIndex: Long,
458458
} else if (missingForReceiver < 0.msat) {
459459
if (params.localParams.isInitiator) {
460460
// 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
461+
} else if (reduced.toLocal > fees && reduced.htlcs.size < 5) {
462+
// Receiver is the channel initiator; we usually don't want to let them dip into their channel reserve, because
463+
// that may give them a commitment transaction where they have nothing at stake, which would create an incentive
464+
// for them to force-close using that commitment after it has been revoked.
465+
// But we let them dip slightly into their channel reserve to pay the fees, to ensure that the channel is not
466+
// stuck and unusable, because we can end up in that state in the following scenario:
467+
// - they were above their channel reserve
468+
// - we spliced a lot of funds into the channel, which increased the reserve requirements
469+
// - they are now below the new reserve, but if we don't allow htlcs to them, they have no way of increasing their balance
470+
// Since we only allow a limited number of htlcs, that doesn't let them dip into their reserve much.
471+
// We could also keep track of the previous channel reserve, but this is additional state that is awkward to
472+
// store and not trivial to correctly keep up-to-date. This simpler solution has a similar result with less
473+
// complexity.
461474
} else {
462475
return Left(RemoteCannotAffordFeesForNewHtlc(params.channelId, amount = amount, missing = -missingForReceiver.truncateToSatoshi, reserve = remoteChannelReserve(params), fees = fees))
463476
}

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalSplicesStateSpec.scala

+93-57
Original file line numberDiff line numberDiff line change
@@ -68,98 +68,103 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
6868

6969
private val defaultSpliceOutScriptPubKey = hex"0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
7070

71-
private def useQuiescence(f: FixtureParam): Boolean = f.alice.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.useQuiescence
71+
private def useQuiescence(s: TestFSMRef[ChannelState, ChannelData, Channel]): Boolean = s.stateData.asInstanceOf[ChannelDataWithCommitments].commitments.params.useQuiescence
7272

73-
private def initiateSpliceWithoutSigs(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None): TestProbe = {
74-
import f._
73+
private def useQuiescence(f: FixtureParam): Boolean = useQuiescence(f.alice)
7574

75+
private def initiateSpliceWithoutSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]): TestProbe = {
7676
val sender = TestProbe()
7777
val cmd = CMD_SPLICE(sender.ref, spliceIn_opt, spliceOut_opt)
78-
alice ! cmd
79-
if (useQuiescence(f)) {
80-
exchangeStfu(f)
78+
s ! cmd
79+
if (useQuiescence(s)) {
80+
exchangeStfu(s, r, s2r, r2s)
8181
}
82-
alice2bob.expectMsgType[SpliceInit]
83-
alice2bob.forward(bob)
84-
bob2alice.expectMsgType[SpliceAck]
85-
bob2alice.forward(alice)
86-
87-
alice2bob.expectMsgType[TxAddInput]
88-
alice2bob.forward(bob)
89-
bob2alice.expectMsgType[TxComplete]
90-
bob2alice.forward(alice)
82+
s2r.expectMsgType[SpliceInit]
83+
s2r.forward(r)
84+
r2s.expectMsgType[SpliceAck]
85+
r2s.forward(s)
86+
87+
s2r.expectMsgType[TxAddInput]
88+
s2r.forward(r)
89+
r2s.expectMsgType[TxComplete]
90+
r2s.forward(s)
9191
if (spliceIn_opt.isDefined) {
92-
alice2bob.expectMsgType[TxAddInput]
93-
alice2bob.forward(bob)
94-
bob2alice.expectMsgType[TxComplete]
95-
bob2alice.forward(alice)
96-
alice2bob.expectMsgType[TxAddOutput]
97-
alice2bob.forward(bob)
98-
bob2alice.expectMsgType[TxComplete]
99-
bob2alice.forward(alice)
92+
s2r.expectMsgType[TxAddInput]
93+
s2r.forward(r)
94+
r2s.expectMsgType[TxComplete]
95+
r2s.forward(s)
96+
s2r.expectMsgType[TxAddOutput]
97+
s2r.forward(r)
98+
r2s.expectMsgType[TxComplete]
99+
r2s.forward(s)
100100
}
101101
if (spliceOut_opt.isDefined) {
102-
alice2bob.expectMsgType[TxAddOutput]
103-
alice2bob.forward(bob)
104-
bob2alice.expectMsgType[TxComplete]
105-
bob2alice.forward(alice)
102+
s2r.expectMsgType[TxAddOutput]
103+
s2r.forward(r)
104+
r2s.expectMsgType[TxComplete]
105+
r2s.forward(s)
106106
}
107-
alice2bob.expectMsgType[TxAddOutput]
108-
alice2bob.forward(bob)
109-
bob2alice.expectMsgType[TxComplete]
110-
bob2alice.forward(alice)
111-
alice2bob.expectMsgType[TxComplete]
112-
alice2bob.forward(bob)
107+
s2r.expectMsgType[TxAddOutput]
108+
s2r.forward(r)
109+
r2s.expectMsgType[TxComplete]
110+
r2s.forward(s)
111+
s2r.expectMsgType[TxComplete]
112+
s2r.forward(r)
113113
sender
114114
}
115115

116-
private def exchangeSpliceSigs(f: FixtureParam, sender: TestProbe): Transaction = {
117-
import f._
116+
private def initiateSpliceWithoutSigs(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None): TestProbe = initiateSpliceWithoutSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt)
118117

119-
val commitSigBob = bob2alice.fishForMessage() {
118+
private def exchangeSpliceSigs(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, sender: TestProbe): Transaction = {
119+
val commitSigR = r2s.fishForMessage() {
120120
case _: CommitSig => true
121121
case _: ChannelReady => false
122122
}
123-
bob2alice.forward(alice, commitSigBob)
124-
val commitSigAlice = alice2bob.fishForMessage() {
123+
r2s.forward(s, commitSigR)
124+
val commitSigS = s2r.fishForMessage() {
125125
case _: CommitSig => true
126126
case _: ChannelReady => false
127127
}
128-
alice2bob.forward(bob, commitSigAlice)
128+
s2r.forward(r, commitSigS)
129129

130-
val txSigsBob = bob2alice.fishForMessage() {
130+
val txSigsR = r2s.fishForMessage() {
131131
case _: TxSignatures => true
132132
case _: ChannelUpdate => false
133133
}
134-
bob2alice.forward(alice, txSigsBob)
135-
val txSigsAlice = alice2bob.fishForMessage() {
134+
r2s.forward(s, txSigsR)
135+
val txSigsS = s2r.fishForMessage() {
136136
case _: TxSignatures => true
137137
case _: ChannelUpdate => false
138138
}
139-
alice2bob.forward(bob, txSigsAlice)
139+
s2r.forward(r, txSigsS)
140140

141141
sender.expectMsgType[RES_SPLICE]
142142

143-
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice)
144-
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice)
145-
awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx].sharedTx.isInstanceOf[FullySignedSharedTransaction])
146-
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx].sharedTx.isInstanceOf[FullySignedSharedTransaction])
147-
alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get
143+
awaitCond(s.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice)
144+
awaitCond(r.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice)
145+
awaitCond(s.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx].sharedTx.isInstanceOf[FullySignedSharedTransaction])
146+
awaitCond(r.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.asInstanceOf[DualFundedUnconfirmedFundingTx].sharedTx.isInstanceOf[FullySignedSharedTransaction])
147+
s.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localFundingStatus.signedTx_opt.get
148148
}
149149

150-
private def initiateSplice(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None): Transaction = {
151-
val sender = initiateSpliceWithoutSigs(f, spliceIn_opt, spliceOut_opt)
152-
exchangeSpliceSigs(f, sender)
150+
private def exchangeSpliceSigs(f: FixtureParam, sender: TestProbe): Transaction = exchangeSpliceSigs(f.alice, f.bob, f.alice2bob, f.bob2alice, sender)
151+
152+
private def initiateSplice(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe, spliceIn_opt: Option[SpliceIn], spliceOut_opt: Option[SpliceOut]): Transaction = {
153+
val sender = initiateSpliceWithoutSigs(s, r, s2r, r2s, spliceIn_opt, spliceOut_opt)
154+
exchangeSpliceSigs(s, r, s2r, r2s, sender)
153155
}
154156

155-
private def exchangeStfu(f: FixtureParam): Unit = {
156-
import f._
157-
alice2bob.expectMsgType[Stfu]
158-
alice2bob.forward(bob)
159-
bob2alice.expectMsgType[Stfu]
160-
bob2alice.forward(alice)
157+
private def initiateSplice(f: FixtureParam, spliceIn_opt: Option[SpliceIn] = None, spliceOut_opt: Option[SpliceOut] = None): Transaction = initiateSplice(f.alice, f.bob, f.alice2bob, f.bob2alice, spliceIn_opt, spliceOut_opt)
158+
159+
private def exchangeStfu(s: TestFSMRef[ChannelState, ChannelData, Channel], r: TestFSMRef[ChannelState, ChannelData, Channel], s2r: TestProbe, r2s: TestProbe): Unit = {
160+
s2r.expectMsgType[Stfu]
161+
s2r.forward(r)
162+
r2s.expectMsgType[Stfu]
163+
r2s.forward(s)
161164
}
162165

166+
private def exchangeStfu(f: FixtureParam): Unit = exchangeStfu(f.alice, f.bob, f.alice2bob, f.bob2alice)
167+
163168
case class TestHtlcs(aliceToBob: Seq[(ByteVector32, UpdateAddHtlc)], bobToAlice: Seq[(ByteVector32, UpdateAddHtlc)])
164169

165170
private def setupHtlcs(f: FixtureParam): TestHtlcs = {
@@ -416,6 +421,37 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
416421
awaitCond(bob.stateData.asInstanceOf[DATA_NORMAL].spliceStatus == SpliceStatus.NoSplice)
417422
}
418423

424+
test("recv CMD_SPLICE (remote splices in which takes us below reserve)", Tag(ChannelStateTestsTags.NoMaxHtlcValueInFlight)) { f =>
425+
import f._
426+
427+
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toLocal == 800_000_000.msat)
428+
val (r1, htlc1) = addHtlc(750_000_000 msat, alice, bob, alice2bob, bob2alice)
429+
crossSign(alice, bob, alice2bob, bob2alice)
430+
fulfillHtlc(htlc1.id, r1, bob, alice, bob2alice, alice2bob)
431+
crossSign(bob, alice, bob2alice, alice2bob)
432+
433+
// Bob makes a large splice: Alice doesn't meet the new reserve requirements, but she met the previous one, so we allow this.
434+
initiateSplice(bob, alice, bob2alice, alice2bob, spliceIn_opt = Some(SpliceIn(4_000_000 sat)), spliceOut_opt = None)
435+
val postSpliceState = alice.stateData.asInstanceOf[DATA_NORMAL]
436+
assert(postSpliceState.commitments.latest.localCommit.spec.toLocal < postSpliceState.commitments.latest.localChannelReserve)
437+
438+
// Since Alice is below the reserve and most of the funds are on Bob's side, Alice cannot send HTLCs.
439+
val probe = TestProbe()
440+
val (_, cmd) = makeCmdAdd(5_000_000 msat, bob.nodeParams.nodeId, bob.nodeParams.currentBlockHeight)
441+
alice ! cmd.copy(replyTo = probe.ref)
442+
probe.expectMsgType[RES_ADD_FAILED[InsufficientFunds]]
443+
444+
// But Bob can send HTLCs to take Alice above the reserve.
445+
val (r2, htlc2) = addHtlc(50_000_000 msat, bob, alice, bob2alice, alice2bob)
446+
crossSign(bob, alice, bob2alice, alice2bob)
447+
fulfillHtlc(htlc2.id, r2, alice, bob, alice2bob, bob2alice)
448+
crossSign(alice, bob, alice2bob, bob2alice)
449+
450+
// Alice can now send HTLCs as well.
451+
addHtlc(10_000_000 msat, alice, bob, alice2bob, bob2alice)
452+
crossSign(alice, bob, alice2bob, bob2alice)
453+
}
454+
419455
def testSpliceInAndOutCmd(f: FixtureParam): Unit = {
420456
val htlcs = setupHtlcs(f)
421457

0 commit comments

Comments
 (0)