Skip to content

Commit 0318fb7

Browse files
authored
Unwatch previous funding tx after splice (#3218)
After a splice transaction confirms, we don't need to keep watching the previous funding output: it has been irrevocably spent and needlessly consumes resources in the `Watcher` actor. Those watches are cleaned up when the `Channel` actor dies, which does not happen if the channel isn't closed and the `Peer` actor is kept alive while disconnected. Whenever we receive a new block, we log the number of watches we have. This lets us detect whether we're missing some clean-up of old watches in edge cases.
1 parent 38dc407 commit 0318fb7

4 files changed

Lines changed: 101 additions & 9 deletions

File tree

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ object ZmqWatcher {
132132

133133
case class WatchFundingSpent(replyTo: ActorRef[WatchFundingSpentTriggered], txId: TxId, outputIndex: Int, hints: Set[TxId]) extends WatchSpent[WatchFundingSpentTriggered]
134134
case class WatchFundingSpentTriggered(spendingTx: Transaction) extends WatchSpentTriggered
135+
case class UnwatchFundingSpent(txId: TxId, outputIndex: Int) extends Command
135136

136137
case class WatchOutputSpent(replyTo: ActorRef[WatchOutputSpentTriggered], txId: TxId, outputIndex: Int, amount: Satoshi, hints: Set[TxId]) extends WatchSpent[WatchOutputSpentTriggered]
137138
case class WatchOutputSpentTriggered(amount: Satoshi, spendingTx: Transaction) extends WatchSpentTriggered
@@ -321,6 +322,12 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
321322
blockHeight.set(currentHeight.toLong)
322323
context.system.eventStream ! EventStream.Publish(CurrentBlockHeight(currentHeight))
323324
// TODO: should we try to mitigate the herd effect and not check all watches immediately?
325+
val watchExternalChannelCount = watches.keySet.count(_.isInstanceOf[WatchExternalChannelSpent])
326+
val watchFundingSpentCount = watches.keySet.count(_.isInstanceOf[WatchFundingSpent])
327+
val watchOutputSpentCount = watches.keySet.count(_.isInstanceOf[WatchOutputSpent])
328+
val watchPublishedCount = watches.keySet.count(_.isInstanceOf[WatchPublished])
329+
val watchConfirmedCount = watches.keySet.count(_.isInstanceOf[WatchConfirmed[_]])
330+
log.info("{} watched utxos: external-channels={}, funding-spent={}, output-spent={}, tx-published={}, tx-confirmed={}", watchedUtxos.size, watchExternalChannelCount, watchFundingSpentCount, watchOutputSpentCount, watchPublishedCount, watchConfirmedCount)
324331
KamonExt.timeFuture(Metrics.NewBlockCheckConfirmedDuration.withoutTags()) {
325332
Future.sequence(watches.collect {
326333
case (w: WatchPublished, _) => checkPublished(w)
@@ -401,6 +408,11 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
401408
val watchedUtxos1 = deprecatedWatches.foldLeft(watchedUtxos) { case (m, w) => removeWatchedUtxos(m, w) }
402409
watching(watches -- deprecatedWatches, watchedUtxos1, analyzedBlocks)
403410

411+
case UnwatchFundingSpent(txId, outputIndex) =>
412+
val deprecatedWatches = watches.keySet.collect { case w: WatchFundingSpent if w.txId == txId && w.outputIndex == outputIndex => w }
413+
val watchedUtxos1 = deprecatedWatches.foldLeft(watchedUtxos) { case (m, w) => removeWatchedUtxos(m, w) }
414+
watching(watches -- deprecatedWatches, watchedUtxos1, analyzedBlocks)
415+
404416
case ValidateRequest(replyTo, ann) =>
405417
client.validate(ann).map(replyTo ! _)
406418
Behaviors.same

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonFundingHandlers.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ trait CommonFundingHandlers extends CommonHandlers {
102102
// Children splice transactions may already spend that confirmed funding transaction.
103103
val spliceSpendingTxs = commitments1.all.collect { case c if c.fundingTxIndex == commitment.fundingTxIndex + 1 => c.fundingTxId }
104104
watchFundingSpent(commitment, additionalKnownSpendingTxs = spliceSpendingTxs.toSet, None)
105+
// We can unwatch the previous funding transaction(s), which have been spent by this splice transaction.
106+
d.commitments.all.collect { case c if c.fundingTxIndex < commitment.fundingTxIndex => blockchain ! UnwatchFundingSpent(c.fundingTxId, c.fundingInput.index.toInt) }
105107
// In the dual-funding/splicing case we can forget all other transactions (RBF attempts), they have been
106108
// double-spent by the tx that just confirmed.
107109
val conflictingTxs = d.commitments.active // note how we use the unpruned original commitments

eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,32 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind
370370
})
371371
}
372372

373+
test("unwatch funding spent") {
374+
withWatcher(f => {
375+
import f._
376+
377+
val (priv, address) = createExternalAddress()
378+
val tx = sendToAddress(address, Btc(1), probe)
379+
val outputIndex = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(priv.publicKey)))
380+
val (tx1, _) = createUnspentTxChain(tx, priv)
381+
382+
watcher ! WatchFundingSpent(probe.ref, tx.txid, outputIndex, Set.empty)
383+
probe.expectNoMessage(100 millis)
384+
385+
bitcoinClient.publishTransaction(tx1)
386+
probe.expectMsg(WatchFundingSpentTriggered(tx1))
387+
probe.expectNoMessage(100 millis)
388+
389+
watcher ! UnwatchFundingSpent(tx.txid, outputIndex)
390+
probe.expectNoMessage(100 millis)
391+
// Let's confirm tx and tx1: seeing tx1 in a block should trigger both WatchSpentTriggered events again, but we unwatched the transaction.
392+
bitcoinClient.getBlockHeight().pipeTo(probe.ref)
393+
probe.expectMsgType[BlockHeight]
394+
generateBlocks(1)
395+
probe.expectNoMessage(100 millis)
396+
})
397+
}
398+
373399
test("watch for unknown spent transactions") {
374400
withWatcher(f => {
375401
import f._

0 commit comments

Comments
 (0)