From 7fed2a7a8e23efbc6c293ee2716aaa2c7f1ceb9b Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 17 Jun 2025 09:59:41 +0200 Subject: [PATCH] Increase channel spent delay to 72 blocks We previously waited for 12 blocks before removing spent channels from our graph: the spec updates this delay to 72 blocks to allow more time for splice announcement in https://github.com/lightning/bolts/pull/1270. We also make this configurable, which can simplify testing and allows nodes to wait even longer if they wish. --- eclair-core/src/main/resources/reference.conf | 3 +++ eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala | 1 + .../fr/acinq/eclair/remote/EclairInternalsSerializer.scala | 1 + .../src/main/scala/fr/acinq/eclair/router/Router.scala | 6 ++---- .../src/test/scala/fr/acinq/eclair/TestConstants.scala | 2 ++ .../scala/fr/acinq/eclair/integration/IntegrationSpec.scala | 1 + 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 5eba55be24..1b4f896302 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -415,6 +415,9 @@ eclair { router { watch-spent-window = 60 minutes // at startup watches on public channels will be put back within that window to reduce herd effect; must be > 0s + // when we detect that a remote channel has been spent on-chain, we wait for 72 blocks before removing it from the graph + // if this was a splice instead of a close, we will be able to simply update the channel in our graph and keep its reputation + channel-spent-splice-delay = 72 channel-exclude-duration = 60 seconds // when a temporary channel failure is returned, we exclude the channel from our payment routes for this duration broadcast-interval = 60 seconds // see BOLT #7 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 2360688332..0e3378ef07 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -664,6 +664,7 @@ object NodeParams extends Logging { ), routerConf = RouterConf( watchSpentWindow = watchSpentWindow, + channelSpentSpliceDelay = config.getInt("router.channel-spent-splice-delay"), channelExcludeDuration = FiniteDuration(config.getDuration("router.channel-exclude-duration").getSeconds, TimeUnit.SECONDS), routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS), syncConf = Router.SyncConf( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala index 4538eed438..f04a14322a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/remote/EclairInternalsSerializer.scala @@ -109,6 +109,7 @@ object EclairInternalsSerializer { val routerConfCodec: Codec[RouterConf] = ( ("watchSpentWindow" | finiteDurationCodec) :: + ("channelSpentSpliceDelay" | int32) :: ("channelExcludeDuration" | finiteDurationCodec) :: ("routerBroadcastInterval" | finiteDurationCodec) :: ("syncConf" | syncConfCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index ce67a7c96a..3cf10e4aea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -266,7 +266,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm case Event(WatchExternalChannelSpentTriggered(shortChannelId, spendingTx), d) if d.channels.contains(shortChannelId) || d.prunedChannels.contains(shortChannelId) => val fundingTxId = d.channels.get(shortChannelId).orElse(d.prunedChannels.get(shortChannelId)).get.fundingTxId log.info("funding tx txId={} of channelId={} has been spent by txId={}: waiting for the spending tx to have enough confirmations before removing the channel from the graph", fundingTxId, shortChannelId, spendingTx.txid) - watcher ! WatchTxConfirmed(self, spendingTx.txid, ANNOUNCEMENTS_MINCONF * 2) + watcher ! WatchTxConfirmed(self, spendingTx.txid, nodeParams.routerConf.channelSpentSpliceDelay) stay() using d.copy(spentChannels = d.spentChannels.updated(spendingTx.txid, d.spentChannels.getOrElse(spendingTx.txid, Set.empty) + shortChannelId)) case Event(WatchTxConfirmedTriggered(_, _, spendingTx), d) => @@ -345,9 +345,6 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm object Router { - // see https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements - val ANNOUNCEMENTS_MINCONF = 6 - def props(nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Command], initialized: Option[Promise[Done]] = None) = Props(new Router(nodeParams, watcher, initialized)) case class SearchBoundaries(maxFeeFlat: MilliSatoshi, @@ -382,6 +379,7 @@ object Router { } case class RouterConf(watchSpentWindow: FiniteDuration, + channelSpentSpliceDelay: Int, channelExcludeDuration: FiniteDuration, routerBroadcastInterval: FiniteDuration, syncConf: SyncConf, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index d348a2ed9d..b46d18b45f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -200,6 +200,7 @@ object TestConstants { ), routerConf = RouterConf( watchSpentWindow = 1 second, + channelSpentSpliceDelay = 12, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 1 day, // "disables" rebroadcast syncConf = Router.SyncConf( @@ -389,6 +390,7 @@ object TestConstants { ), routerConf = RouterConf( watchSpentWindow = 1 second, + channelSpentSpliceDelay = 12, channelExcludeDuration = 60 seconds, routerBroadcastInterval = 1 day, // "disables" rebroadcast syncConf = Router.SyncConf( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 04cbfa7097..36043db74f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -85,6 +85,7 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit "eclair.channel.max-htlc-value-in-flight-percent" -> 100, "eclair.channel.max-block-processing-delay" -> "2 seconds", "eclair.channel.to-remote-delay-blocks" -> 24, + "eclair.router.channel-spent-splice-delay" -> 12, "eclair.router.broadcast-interval" -> "2 seconds", "eclair.auto-reconnect" -> false, "eclair.multi-part-payment-expiry" -> "20 seconds",