lightningd: re-xmit funding txs on startup.
What changed, and why it matters
This change makes Core Lightning re-broadcast funding transactions when the node restarts. Previously, only closing transactions were re-sent on startup. If a funding transaction was lost by the Bitcoin network (for example, due to a restart or mempool eviction), a new channel could be stuck waiting indefinitely. The fix improves reliability of opening channels, and the included test that was previously expected to fail now passes.
No immediate security action required. Treat as a normal reliability fix. Reviewers may want to confirm that re-broadcasting only occurs for channels with depth==0 and a finalized PSBT, to avoid duplicate broadcasts for already-confirmed channels.
Security signals we found
Funds availability / liveness issue: a stuck funding transaction could leave a user unable to use funds committed to a channel opening
No cryptographic, memory-safety, or authentication change
Change is defensive/robustness: re-transmission reduces reliance on a single broadcast surviving
Evidence from the diff
The commit adds resend_opening_transactions() in lightningd/peer_control.c, called from main() in lightningd.c alongside the existing resend_closing_transactions(). It iterates over peers and channels, skips uncommitted states and channels without a funding PSBT or with non-zero depth, then extracts the final transaction from the PSBT and submits it via bitcoind_sendrawtx(). A callback logs success or failure. Test stubs are updated, and tests/test_opening.py removes the xfail marker from test_opening_crash, indicating the crash-recovery scenario is now expected to pass.
Changed components
lightningd/peer_control.clightningd/lightningd.clightningd/peer_control.htests/test_opening.pyInspect captured patch +70 / −3
diff --git a/lightningd/bitcoind.h b/lightningd/bitcoind.h
index 3daa5090..462817c9 100644
--- a/lightningd/bitcoind.h
+++ b/lightningd/bitcoind.h
@@ -77,7 +77,7 @@ void bitcoind_sendrawtx_(const tal_t *ctx,
const char *id_prefix TAKES,
const char *hextx,
bool allowhighfees,
- void (*cb)(struct bitcoind *bitcoind,
+ void (*cb)(struct bitcoind *,
bool success, const char *msg, void *),
void *arg);
#define bitcoind_sendrawtx(ctx, bitcoind_, id_prefix, hextx, allowhighfees, cb, arg) \
diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c
index 7e0bf26c..e3eff22d 100644
--- a/lightningd/lightningd.c
+++ b/lightningd/lightningd.c
@@ -1447,9 +1447,10 @@ int main(int argc, char *argv[])
plugin_hook_call_recover(ld, NULL, payload);
}
- /*~ If we have channels closing, make sure we re-xmit the last
+ /*~ If we have channels closing or opening, make sure we re-xmit the last
* transaction, in case bitcoind lost it. */
db_begin_transaction(ld->wallet->db);
+ resend_opening_transactions(ld);
resend_closing_transactions(ld);
db_commit_transaction(ld->wallet->db);
diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c
index e5fbc74a..e14f5978 100644
--- a/lightningd/peer_control.c
+++ b/lightningd/peer_control.c
@@ -478,6 +478,47 @@ void resend_closing_transactions(struct lightningd *ld)
}
}
+static void resend_funding_done(struct bitcoind *bitcoind,
+ bool success,
+ const char *msg,
+ struct channel *channel)
+{
+ if (success)
+ log_info(channel->log, "Successfully rexmitted funding tx");
+ else
+ log_unusual(channel->log, "Failed to re-transmit funding tx: %s", msg);
+}
+
+void resend_opening_transactions(struct lightningd *ld)
+{
+ struct peer *peer;
+ struct channel *channel;
+ struct peer_node_id_map_iter it;
+
+ for (peer = peer_node_id_map_first(ld->peers, &it);
+ peer;
+ peer = peer_node_id_map_next(ld->peers, &it)) {
+ list_for_each(&peer->channels, channel, list) {
+ struct wally_tx *wtx;
+ if (channel_state_uncommitted(channel->state))
+ continue;
+ if (!channel->funding_psbt)
+ continue;
+ if (channel->depth != 0)
+ continue;
+ wtx = psbt_final_tx(tmpctx, channel->funding_psbt);
+ if (!wtx)
+ continue;
+ bitcoind_sendrawtx(channel,
+ ld->topology->bitcoind,
+ NULL,
+ tal_hex(tmpctx,
+ linearize_wtx(tmpctx, wtx)),
+ false, resend_funding_done, channel);
+ }
+ }
+}
+
void channel_errmsg(struct channel *channel,
struct peer_fd *peer_fd,
const char *desc,
diff --git a/lightningd/peer_control.h b/lightningd/peer_control.h
index 63826f5b..70016d6d 100644
--- a/lightningd/peer_control.h
+++ b/lightningd/peer_control.h
@@ -119,6 +119,9 @@ void peer_set_dbid(struct peer *peer, u64 dbid);
/* At startup, re-send any transactions we want bitcoind to have */
void resend_closing_transactions(struct lightningd *ld);
+/* At startup, re-send any funding transactions we want bitcoind to have */
+void resend_opening_transactions(struct lightningd *ld);
+
/* Initiate the close of a channel, maybe broadcast. If we've seen a
* unilateral close, pass it here (means we don't need to broadcast
* our own, or any anchors). */
diff --git a/lightningd/test/run-find_my_abspath.c b/lightningd/test/run-find_my_abspath.c
index 14a3998e..b03fcfda 100644
--- a/lightningd/test/run-find_my_abspath.c
+++ b/lightningd/test/run-find_my_abspath.c
@@ -184,6 +184,9 @@ void plugins_set_builtin_plugins_dir(struct plugins *plugins UNNEEDED,
/* Generated stub for resend_closing_transactions */
void resend_closing_transactions(struct lightningd *ld UNNEEDED)
{ fprintf(stderr, "resend_closing_transactions called!\n"); abort(); }
+/* Generated stub for resend_opening_transactions */
+void resend_opening_transactions(struct lightningd *ld UNNEEDED)
+{ fprintf(stderr, "resend_opening_transactions called!\n"); abort(); }
/* Generated stub for runes_early_init */
struct runes *runes_early_init(struct lightningd *ld UNNEEDED)
{ fprintf(stderr, "runes_early_init called!\n"); abort(); }
diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c
index b2721ea8..1f4ff87f 100644
--- a/lightningd/test/run-invoice-select-inchan.c
+++ b/lightningd/test/run-invoice-select-inchan.c
@@ -29,6 +29,16 @@ void bitcoind_getutxout_(const tal_t *ctx UNNEEDED,
void *) UNNEEDED,
void *arg UNNEEDED)
{ fprintf(stderr, "bitcoind_getutxout_ called!\n"); abort(); }
+/* Generated stub for bitcoind_sendrawtx_ */
+void bitcoind_sendrawtx_(const tal_t *ctx UNNEEDED,
+ struct bitcoind *bitcoind UNNEEDED,
+ const char *id_prefix TAKES UNNEEDED,
+ const char *hextx UNNEEDED,
+ bool allowhighfees UNNEEDED,
+ void (*cb)(struct bitcoind * UNNEEDED,
+ bool success UNNEEDED, const char *msg UNNEEDED, void *) UNNEEDED,
+ void *arg UNNEEDED)
+{ fprintf(stderr, "bitcoind_sendrawtx_ called!\n"); abort(); }
/* Generated stub for broadcast_tx_ */
void broadcast_tx_(const tal_t *ctx UNNEEDED,
struct chain_topology *topo UNNEEDED,
diff --git a/tests/test_opening.py b/tests/test_opening.py
index 69e71a26..3e9e587a 100644
--- a/tests/test_opening.py
+++ b/tests/test_opening.py
@@ -2827,7 +2827,6 @@ def test_opening_below_min_capacity_sat(bitcoind, node_factory):
assert not l2.daemon.is_in_log('peer_in WIRE_ERROR')
-@pytest.mark.xfail(strict=True)
@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
def test_opening_crash(bitcoind, node_factory):
diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c
index 86ae310a..f559a764 100644
--- a/wallet/test/run-wallet.c
+++ b/wallet/test/run-wallet.c
@@ -67,6 +67,16 @@ void bitcoind_getutxout_(const tal_t *ctx UNNEEDED,
void *) UNNEEDED,
void *arg UNNEEDED)
{ fprintf(stderr, "bitcoind_getutxout_ called!\n"); abort(); }
+/* Generated stub for bitcoind_sendrawtx_ */
+void bitcoind_sendrawtx_(const tal_t *ctx UNNEEDED,
+ struct bitcoind *bitcoind UNNEEDED,
+ const char *id_prefix TAKES UNNEEDED,
+ const char *hextx UNNEEDED,
+ bool allowhighfees UNNEEDED,
+ void (*cb)(struct bitcoind * UNNEEDED,
+ bool success UNNEEDED, const char *msg UNNEEDED, void *) UNNEEDED,
+ void *arg UNNEEDED)
+{ fprintf(stderr, "bitcoind_sendrawtx_ called!\n"); abort(); }
/* Generated stub for broadcast_tx_ */
void broadcast_tx_(const tal_t *ctx UNNEEDED,
struct chain_topology *topo UNNEEDED,
Why this scored 31/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.