lightningd: reject a channel that reuses an existing funding outpoint
What changed, and why it matters
This change prevents a peer from accidentally or maliciously opening a second Lightning channel using the exact same Bitcoin transaction output as an existing channel. Because channel IDs are derived from that funding output, reusing it would create two channels with the same ID, which could confuse the node and potentially lead to incorrect routing or state handling. The fix detects the duplicate and disconnects the peer before the second channel is committed.
Apply the patch. It is a targeted, low-risk defensive fix. Consider whether other code paths (e.g., fundee vs funder, dual-funding, splicing) also need duplicate-funding-outpoint checks, as the current patch only covers the fundee completion path.
Security signals we found
channel ID collision / duplicate identifier
funding outpoint reuse
state confusion between distinct channels
peer-triggered duplicate channel creation
defensive disconnect on duplicate detection
Evidence from the diff
Core Lightning previously allowed a peer to complete a second channel funding that reused a funding outpoint already funding an existing channel. Since v1 channel_id is derived solely from funding_txid:funding_vout, this produced colliding channel IDs. The patch adds find_channel_by_funding_outpoint() and, in opening_fundee_finished(), checks for an existing channel with the same funding outpoint; if found, it force-disconnects the peer with ‘Funding outpoint already in use’ and aborts the new channel before funding_signed.
Changed components
lightningd/channel.clightningd/channel.hlightningd/opening_control.cInspect captured patch +26 / −0
diff --git a/lightningd/channel.c b/lightningd/channel.c
index 1cd40871..692d8198 100644
--- a/lightningd/channel.c
+++ b/lightningd/channel.c
@@ -879,6 +879,18 @@ struct channel *find_channel_by_id(const struct peer *peer,
return NULL;
}
+struct channel *find_channel_by_funding_outpoint(const struct peer *peer,
+ const struct bitcoin_outpoint *outpoint)
+{
+ struct channel *c;
+
+ list_for_each(&peer->channels, c, list) {
+ if (bitcoin_outpoint_eq(&c->funding, outpoint))
+ return c;
+ }
+ return NULL;
+}
+
struct channel *find_channel_by_scid(const struct peer *peer,
struct short_channel_id scid)
{
diff --git a/lightningd/channel.h b/lightningd/channel.h
index 46add63f..8e3a499a 100644
--- a/lightningd/channel.h
+++ b/lightningd/channel.h
@@ -918,6 +918,11 @@ struct channel *channel_by_cid(struct lightningd *ld,
struct channel *find_channel_by_id(const struct peer *peer,
const struct channel_id *cid);
+/* Find a channel with this funding outpoint within peer (an outpoint
+ * funds at most one channel). */
+struct channel *find_channel_by_funding_outpoint(const struct peer *peer,
+ const struct bitcoin_outpoint *outpoint);
+
/* Find this channel within peer */
struct channel *find_channel_by_scid(const struct peer *peer,
struct short_channel_id scid);
diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c
index 74a3277d..6cc3d82f 100644
--- a/lightningd/opening_control.c
+++ b/lightningd/opening_control.c
@@ -533,6 +533,15 @@ static void opening_fundee_finished(struct subd *openingd,
derive_channel_id(&cid, &funding);
+ /* A funding outpoint funds at most one channel; don't accept a second
+ * channel reusing one we already have. Drop the connection so the
+ * peer's open fails cleanly instead of waiting for funding_signed. */
+ if (find_channel_by_funding_outpoint(uc->peer, &funding)) {
+ force_peer_disconnect(ld, uc->peer,
+ "Funding outpoint already in use");
+ return;
+ }
+
/* old_remote_per_commit not valid yet, copy valid one. */
channel_info.old_remote_per_commit = channel_info.remote_per_commit;
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.