From a5746f95d6e8f35d0ad8ff706ff5b9f587ad9f26 Mon Sep 17 00:00:00 2001 From: t-bast Date: Tue, 7 Jul 2026 16:10:33 +0200 Subject: [PATCH] Reject `temporary_channel_id` duplicates early We previously rejected duplicate `temporary_channel_id` immediately, but we didn't immediately reject `temporary_channel_id`s that conflict with a *final* `channel_id` with the same peer: the rejection would happen later in the flow when transitioning from temporary to final IDs, which made debugging harder. --- .../main/scala/fr/acinq/eclair/io/Peer.scala | 4 ++-- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 73b319fa8c..b8d24608d8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -239,7 +239,7 @@ class Peer(val nodeParams: NodeParams, stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) case Event(open: protocol.OpenChannel, d: ConnectedData) => - d.channels.get(TemporaryChannelId(open.temporaryChannelId)) match { + d.channels.get(TemporaryChannelId(open.temporaryChannelId)).orElse(d.channels.get(FinalChannelId(open.temporaryChannelId))) match { case None => openChannelInterceptor ! OpenChannelNonInitiator(remoteNodeId, Left(open), d.localFeatures, d.remoteFeatures, d.peerConnection.toTyped, d.address) stay() @@ -249,7 +249,7 @@ class Peer(val nodeParams: NodeParams, } case Event(open: protocol.OpenDualFundedChannel, d: ConnectedData) => - d.channels.get(TemporaryChannelId(open.temporaryChannelId)) match { + d.channels.get(TemporaryChannelId(open.temporaryChannelId)).orElse(d.channels.get(FinalChannelId(open.temporaryChannelId))) match { case None if !Features.canUseFeature(d.localFeatures, d.remoteFeatures, Features.DualFunding) => log.info("rejecting open_channel2: dual funding is not supported") self ! Peer.OutgoingMessage(Error(open.temporaryChannelId, "dual funding is not supported"), d.peerConnection) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index f1bc5abc38..ac7e9edd7b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -448,6 +448,23 @@ class PeerSpec extends FixtureSpec { assert(peer.stateData.channels.size == 1) } + test("don't spawn a channel that reuses an existing channel id") { f => + import f._ + + val confirmedChannel = ChannelCodecsSpec.normal.withChannelKeys(nodeParams) + connect(remoteNodeId, peer, peerConnection, switchboard, channels = Set(confirmedChannel)) + channel.expectMsg(INPUT_RESTORED(confirmedChannel.channelData)) + channel.expectMsgType[INPUT_RECONNECTED] + + val open = createOpenChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()).copy(temporaryChannelId = confirmedChannel.channelId) + peerConnection.send(peer, open) + val open2 = createOpenDualFundedChannelMessage(ChannelTypes.AnchorOutputsZeroFeeHtlcTx()).copy(temporaryChannelId = confirmedChannel.channelId) + peerConnection.send(peer, open2) + channel.expectNoMessage(100 millis) + peerConnection.expectNoMessage(100 millis) + assert(peer.stateData.channels.size == 1) + } + test("send error when receiving message for unknown channel") { f => import f._