Skip to content

Commit d04ed3d

Browse files
authored
Deduplicate closing balance during mutual close (#3182)
When a mutual close transaction is published or recently confirmed, we don't include the corresponding channel in our off-chain balance to avoid counting it twice (which would resolve itself after confirmations but is misleading). There are degenerate cases where the channel will actually end up being force-closed, even though we started with a mutual close, but that will be very infrequent and will resolve itself after confirmations.
1 parent 90778ca commit d04ed3d

File tree

2 files changed

+14
-0
lines changed

2 files changed

+14
-0
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ object CheckBalance {
200200
this.copy(closing = this.closing.copy(toLocal = this.closing.toLocal + localBalance))
201201
case None => this
202202
}
203+
// If we have a fully signed mutual close transaction and a closing transaction is in our mempool or recently
204+
// confirmed, the channel will most likely end up being mutual-closed (since the feerate is higher than any
205+
// force-close transaction). We thus ignore this channel in our off-chain balance to avoid counting it twice.
206+
case None if d.mutualClosePublished.nonEmpty && recentlySpentInputs.contains(d.commitments.latest.fundingInput) => this
203207
// We don't know yet which type of closing will confirm on-chain, so we use our default off-chain balance.
204208
case None => this.copy(closing = this.closing.addChannelBalance(d.commitments))
205209
}

eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
9898
assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_NEGOTIATING_SIMPLE]), recentlySpentInputs = Set(closingTxInput)).negotiating == expected)
9999
}
100100

101+
test("channel closing with published closing tx (without option_simple_close)") { f =>
102+
import f._
103+
104+
mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain)
105+
assert(alice.stateData.asInstanceOf[DATA_CLOSING].mutualClosePublished.nonEmpty)
106+
val closingTxInput = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.latest.fundingInput
107+
val expected = MainAndHtlcBalance(toLocal = 0 sat, htlcs = 0 sat)
108+
assert(CheckBalance.computeOffChainBalance(Seq(alice.stateData.asInstanceOf[DATA_CLOSING]), recentlySpentInputs = Set(closingTxInput)).closing == expected)
109+
}
110+
101111
test("channel closed with remote commit tx", Tag(ChannelStateTestsTags.StaticRemoteKey), Tag(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs)) { f =>
102112
import f._
103113

0 commit comments

Comments
 (0)