From f1866aaecb7b551ec66e7c193a8b6655f37a3ea7 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:47:36 +0930 Subject: [PATCH 01/15] lightningd: allow another gossip state transition. This can definitely happen with zeroconf and the about-to-be-implemented withheld=True: ``` lightningd-1 2025-09-12T13:17:50.848Z **BROKEN** 022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59-chan#1: Illegal gossip state transition: CGOSSIP_WAITING_FOR_SCID->CGOSSIP_CHANNEL_UNANNOUNCED_DYING ``` Signed-off-by: Rusty Russell --- lightningd/channel_gossip.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lightningd/channel_gossip.c b/lightningd/channel_gossip.c index d4153b207ad4..9587b4addb5c 100644 --- a/lightningd/channel_gossip.c +++ b/lightningd/channel_gossip.c @@ -95,6 +95,8 @@ static struct state_transition allowed_transitions[] = { "Channel usable (zeroconf) but no scid yet" }, { CGOSSIP_WAITING_FOR_SCID, CGOSSIP_CHANNEL_DEAD, "Zeroconf channel closed before funding tx mined" }, + { CGOSSIP_WAITING_FOR_SCID, CGOSSIP_CHANNEL_UNANNOUNCED_DYING, + "Zeroconf channel closing mutually before funding tx" }, { CGOSSIP_WAITING_FOR_USABLE, CGOSSIP_WAITING_FOR_MATCHING_PEER_SIGS, "Channel mined, but we haven't got matching announcment sigs from peer" }, { CGOSSIP_WAITING_FOR_USABLE, CGOSSIP_WAITING_FOR_ANNOUNCE_DEPTH, From 9885fba2b969f6391487e87d50c3e195ca6fddf7 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:47:37 +0930 Subject: [PATCH 02/15] pytest: test case where we crash before bitcoind gets the opening tx. Signed-off-by: Rusty Russell --- tests/test_opening.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_opening.py b/tests/test_opening.py index e533d1f873bf..b2aecdbd1f75 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2825,3 +2825,26 @@ def test_opening_below_min_capacity_sat(bitcoind, node_factory): # But we shouldn't have bothered l2 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): + """Stop transmission of initial funding tx, check it eventually opens""" + l1, l2 = node_factory.get_nodes(2) + + def censoring_sendrawtx(r): + return {'id': r['id'], 'result': {}} + + l1.daemon.rpcproxy.mock_rpc('sendrawtransaction', censoring_sendrawtx) + l2.daemon.rpcproxy.mock_rpc('sendrawtransaction', censoring_sendrawtx) + l1.fundwallet(3_000_000) + l1.connect(l2) + txid = l1.rpc.fundchannel(l2.info['id'], "2000000sat")['txid'] + + l1.stop() + l1.daemon.rpcproxy.mock_rpc('sendrawtransaction', None) + l1.start() + + bitcoind.generate_block(1, wait_for_mempool=txid) From 221fc2c975f9697928ab0a2a35d535736da0684e Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:47:37 +0930 Subject: [PATCH 03/15] lightningd: save funding_psbt in channel, database. Interestingly, @niftynei added a funding_psbt column to the db in 2020, but we don't use it (it was removed early 2021 with the "inflight" architecture). So we don't need to add a new column, just plumb it back in. Signed-off-by: Rusty Russell --- lightningd/channel.c | 6 ++++-- lightningd/channel.h | 6 +++++- lightningd/closed_channel.h | 1 + lightningd/dual_open_control.c | 7 +++++++ lightningd/opening_common.h | 3 +++ lightningd/opening_control.c | 17 ++++++++++++----- wallet/test/run-db.c | 3 ++- wallet/test/run-wallet.c | 3 ++- wallet/wallet.c | 23 +++++++++++++++++++++-- 9 files changed, 57 insertions(+), 12 deletions(-) diff --git a/lightningd/channel.c b/lightningd/channel.c index 354249a4c24c..c628dd8f2ab4 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -427,6 +427,7 @@ struct channel *new_unsaved_channel(struct peer *peer, /* channel->channel_gossip gets populated once we know if it's public. */ channel->channel_gossip = NULL; channel->forgets = tal_arr(channel, struct command *, 0); + channel->funding_psbt = NULL; list_add_tail(&peer->channels, &channel->list); channel->rr_number = peer->ld->rr_counter++; tal_add_destructor(channel, destroy_channel); @@ -548,7 +549,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid, struct peer_update *peer_update STEALS, u64 last_stable_connection, const struct channel_stats *stats, - struct channel_state_change **state_changes STEALS) + struct channel_state_change **state_changes STEALS, + const struct wally_psbt *funding_psbt STEALS) { struct channel *channel = tal(peer->ld, struct channel); struct amount_msat htlc_min, htlc_max; @@ -748,7 +750,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid, channel->error = towire_errorfmt(peer->ld, &channel->cid, "We can't be together anymore."); - + channel->funding_psbt = tal_steal(channel, funding_psbt); return channel; } diff --git a/lightningd/channel.h b/lightningd/channel.h index af2f18a54b8a..1fdcf289eade 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -359,6 +359,9 @@ struct channel { /* Our change history. */ struct channel_state_change **state_changes; + + /* Unsigned PSBT if we initiated the open channel */ + const struct wally_psbt *funding_psbt; }; /* Is channel owned (and should be talking to peer) */ @@ -444,7 +447,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid, struct peer_update *peer_update STEALS, u64 last_stable_connection, const struct channel_stats *stats, - struct channel_state_change **state_changes STEALS); + struct channel_state_change **state_changes STEALS, + const struct wally_psbt *funding_psbt STEALS); /* new_inflight - Create a new channel_inflight for a channel */ struct channel_inflight *new_inflight(struct channel *channel, diff --git a/lightningd/closed_channel.h b/lightningd/closed_channel.h index fc8382d0f142..cf7c91be7f79 100644 --- a/lightningd/closed_channel.h +++ b/lightningd/closed_channel.h @@ -30,6 +30,7 @@ struct closed_channel { u64 last_stable_connection; /* NULL for older closed channels */ const struct shachain *their_shachain; + const struct wally_psbt *funding_psbt; }; static inline const struct channel_id *keyof_closed_channel(const struct closed_channel *cc) diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 3ad5cfb1dbb7..f5806a891f88 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -1521,6 +1521,8 @@ wallet_commit_channel(struct lightningd *ld, channel->lease_chan_max_ppt = lease_chan_max_ppt; channel->htlc_minimum_msat = channel_info->their_config.htlc_minimum; channel->htlc_maximum_msat = htlc_max_possible_send(channel); + /* Filled in when we have PSBT for inflight */ + channel->funding_psbt = NULL; /* Now we finally put it in the database. */ wallet_channel_insert(ld->wallet, channel); @@ -2793,6 +2795,11 @@ json_openchannel_signed(struct command *cmd, wallet_inflight_save(cmd->ld->wallet, inflight); watch_opening_inflight(cmd->ld, inflight); + /* Channel's funding psbt also updated now */ + tal_free(channel->funding_psbt); + channel->funding_psbt = clone_psbt(channel, inflight->funding_psbt); + wallet_channel_save(cmd->ld->wallet, channel); + /* Only after we've updated/saved our psbt do we check * for peer connected */ if (!channel->owner) diff --git a/lightningd/opening_common.h b/lightningd/opening_common.h index 8b75b56a022f..d17a3f9bf9e3 100644 --- a/lightningd/opening_common.h +++ b/lightningd/opening_common.h @@ -103,6 +103,9 @@ struct funding_channel { /* Place to stash the per-peer-state while we wait * for them to get back to us with signatures */ struct peer_fd *peer_fd; + + /* Were we the one to publish the commitment/splicing tx? */ + const struct wally_psbt *funding_psbt; }; struct uncommitted_channel *new_uncommitted_channel(struct peer *peer); diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index 7c12a0ffd551..7fd826b40575 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -98,7 +98,8 @@ wallet_commit_channel(struct lightningd *ld, u32 feerate, const u8 *our_upfront_shutdown_script, const u8 *remote_upfront_shutdown_script, - const struct channel_type *type) + const struct channel_type *type, + const struct wally_psbt *funding_psbt) { struct channel *channel; struct amount_msat our_msat; @@ -237,7 +238,8 @@ wallet_commit_channel(struct lightningd *ld, NULL, 0, &zero_channel_stats, - tal_arr(NULL, struct channel_state_change *, 0)); + tal_arr(NULL, struct channel_state_change *, 0), + funding_psbt); /* Now we finally put it in the database. */ wallet_channel_insert(ld->wallet, channel); @@ -445,7 +447,8 @@ static void opening_funder_finished(struct subd *openingd, const u8 *resp, feerate, fc->our_upfront_shutdown_script, remote_upfront_shutdown_script, - type); + type, + fc->funding_psbt); if (!channel) { was_pending(command_fail(fc->cmd, LIGHTNINGD, "Key generation failure")); @@ -548,7 +551,8 @@ static void opening_fundee_finished(struct subd *openingd, feerate, local_upfront_shutdown_script, remote_upfront_shutdown_script, - type); + type, + NULL); if (!channel) { uncommitted_channel_disconnect(uc, LOG_BROKEN, "Commit channel failed"); @@ -1092,6 +1096,8 @@ static struct command_result *json_fundchannel_complete(struct command *cmd, if (command_check_only(cmd)) return command_check_done(cmd); + fc->funding_psbt = tal_steal(fc, funding_psbt); + /* Set the cmd to this new cmd */ peer->uncommitted_channel->fc->cmd = cmd; msg = towire_openingd_funder_complete(NULL, @@ -1639,7 +1645,8 @@ static struct channel *stub_chan(struct command *cmd, NULL, 0, &zero_channel_stats, - tal_arr(NULL, struct channel_state_change *, 0)); + tal_arr(NULL, struct channel_state_change *, 0), + NULL); /* We don't want to gossip about this, ever. */ channel->channel_gossip = tal_free(channel->channel_gossip); diff --git a/wallet/test/run-db.c b/wallet/test/run-db.c index 106cd38026ae..ba675f95b003 100644 --- a/wallet/test/run-db.c +++ b/wallet/test/run-db.c @@ -254,7 +254,8 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED, struct peer_update *peer_update STEALS UNNEEDED, u64 last_stable_connection UNNEEDED, const struct channel_stats *stats UNNEEDED, - struct channel_state_change **state_changes STEALS UNNEEDED) + struct channel_state_change **state_changes STEALS UNNEEDED, + const struct wally_psbt *funding_psbt STEALS UNNEEDED) { fprintf(stderr, "new_channel called!\n"); abort(); } /* Generated stub for new_channel_coin_mvt_general */ struct channel_coin_mvt *new_channel_coin_mvt_general(const tal_t *ctx UNNEEDED, diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index c1cd3b066f25..4d98fa2be391 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -2114,7 +2114,8 @@ static bool test_channel_inflight_crud(struct lightningd *ld, const tal_t *ctx) NULL, 0, stats, - tal_arr(NULL, struct channel_state_change *, 0)); + tal_arr(NULL, struct channel_state_change *, 0), + NULL); db_begin_transaction(w->db); CHECK(!wallet_err); wallet_channel_insert(w, chan); diff --git a/wallet/wallet.c b/wallet/wallet.c index ddc933d70ced..59a1f63cc8ff 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -1844,6 +1844,7 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm struct peer_update *remote_update; struct channel_stats stats; struct channel_state_change **state_changes; + struct wally_psbt *funding_psbt; peer_dbid = db_col_u64(stmt, "peer_id"); peer = find_peer_by_dbid(w->ld, peer_dbid); @@ -2048,6 +2049,11 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm &stats.out_msatoshi_fulfilled, AMOUNT_MSAT(0)); + if (!db_col_is_null(stmt, "funding_psbt")) + funding_psbt = db_col_psbt(tmpctx, stmt, "funding_psbt"); + else + funding_psbt = NULL; + /* Stolen by new_channel */ state_changes = wallet_state_change_get(NULL, w, db_col_u64(stmt, "id")); chan = new_channel(peer, db_col_u64(stmt, "id"), @@ -2115,7 +2121,8 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm remote_update, db_col_u64(stmt, "last_stable_connection"), &stats, - state_changes); + state_changes, + funding_psbt); if (!wallet_channel_load_inflights(w, chan)) { tal_free(chan); @@ -2169,6 +2176,10 @@ static struct closed_channel *wallet_stmt2closed_channel(const tal_t *ctx, cc->their_shachain = tal_dup(cc, struct shachain, &wshachain.chain); else cc->their_shachain = NULL; + if (!db_col_is_null(stmt, "funding_psbt")) + cc->funding_psbt = db_col_psbt(cc, stmt, "funding_psbt"); + else + cc->funding_psbt = NULL; return cc; } @@ -2204,6 +2215,7 @@ void wallet_load_closed_channels(struct wallet *w, ", lease_commit_sig" ", last_stable_connection" ", shachain_remote_id" + ", funding_psbt" " FROM channels" " LEFT JOIN peers p ON p.id = peer_id" " WHERE state = ?;")); @@ -2249,6 +2261,7 @@ void wallet_load_one_closed_channel(struct wallet *w, ", lease_commit_sig" ", last_stable_connection" ", shachain_remote_id" + ", funding_psbt" " FROM channels" " LEFT JOIN peers p ON p.id = peer_id" " WHERE channels.id = ?;")); @@ -2368,6 +2381,7 @@ static bool wallet_channels_load_active(struct wallet *w) ", out_msatoshi_offered" ", out_msatoshi_fulfilled" ", close_attempt_height" + ", funding_psbt" " FROM channels" " WHERE state != ?;")); //? 0 db_bind_int(stmt, CLOSED); @@ -2619,7 +2633,8 @@ void wallet_channel_save(struct wallet *w, struct channel *chan) " remote_htlc_maximum_msat=?," " last_stable_connection=?," " require_confirm_inputs_remote=?," - " close_attempt_height=?" + " close_attempt_height=?," + " funding_psbt=?" " WHERE id=?")); db_bind_u64(stmt, chan->their_shachain.id); if (chan->scid) @@ -2717,6 +2732,10 @@ void wallet_channel_save(struct wallet *w, struct channel *chan) db_bind_int(stmt, chan->req_confirmed_ins[REMOTE]); db_bind_int(stmt, chan->close_attempt_height); + if (chan->funding_psbt) + db_bind_psbt(stmt, chan->funding_psbt); + else + db_bind_null(stmt); db_bind_u64(stmt, chan->dbid); db_exec_prepared_v2(take(stmt)); From 8b6c372116ec9522fb89ca92c77d119cd620d660 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:47:38 +0930 Subject: [PATCH 04/15] sendpsbt: update channel psbts if this is a channel PSBT. Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 2 +- doc/schemas/sendpsbt.json | 2 +- lightningd/channel_control.c | 5 ++ wallet/walletrpc.c | 106 ++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 69c33acf4aa8..1b4fd50d0772 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -30987,7 +30987,7 @@ "rpc": "sendpsbt", "title": "Command to finalize, extract and send a partially signed bitcoin transaction (PSBT).", "description": [ - "The **sendpsbt** is a low-level RPC command which sends a fully-signed PSBT." + "The **sendpsbt** is a low-level RPC command which sends a fully-signed PSBT. If the PSBT is the same one promised by a channel (via fundchannel_complete) it will also be associated with that channel and re-transmitted if necessary on restart." ], "request": { "required": [ diff --git a/doc/schemas/sendpsbt.json b/doc/schemas/sendpsbt.json index fa1c07d77351..5409a3b9ad48 100644 --- a/doc/schemas/sendpsbt.json +++ b/doc/schemas/sendpsbt.json @@ -4,7 +4,7 @@ "rpc": "sendpsbt", "title": "Command to finalize, extract and send a partially signed bitcoin transaction (PSBT).", "description": [ - "The **sendpsbt** is a low-level RPC command which sends a fully-signed PSBT." + "The **sendpsbt** is a low-level RPC command which sends a fully-signed PSBT. If the PSBT is the same one promised by a channel (via fundchannel_complete) it will also be associated with that channel and re-transmitted if necessary on restart." ], "request": { "required": [ diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index a72f959837ae..6ffe98f4d8f4 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -2405,6 +2405,11 @@ static struct command_result *single_splice_signed(struct command *cmd, SPLICE_INPUT_ERROR, "Splice failed to convert to v2"); + /* Update "funding" psbt now */ + tal_free(channel->funding_psbt); + channel->funding_psbt = clone_psbt(channel, psbt); + wallet_channel_save(cmd->ld->wallet, channel); + msg = towire_channeld_splice_signed(tmpctx, psbt, sign_first); subd_send_msg(channel->owner, take(msg)); if (success) diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index 677db8bab66f..996f53d91860 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -1035,6 +1036,85 @@ static const struct json_command dev_finalizepsbt_command = { }; AUTODATA(json_command, &dev_finalizepsbt_command); +/* Yuck. */ +static const u8 *psbt_input_txid(const struct wally_psbt *psbt, size_t index) +{ + if (psbt->version == WALLY_PSBT_VERSION_0) + return psbt->tx->inputs[index].txhash; + return psbt->inputs[index].txhash; +} + +static u32 psbt_input_index(const struct wally_psbt *psbt, size_t index) +{ + if (psbt->version == WALLY_PSBT_VERSION_0) + return psbt->tx->inputs[index].index; + return psbt->inputs[index].index; +} + +static u32 psbt_input_sequence(const struct wally_psbt *psbt, size_t index) +{ + if (psbt->version == WALLY_PSBT_VERSION_0) + return psbt->tx->inputs[index].sequence; + return psbt->inputs[index].sequence; +} + +static u64 psbt_output_amount(const struct wally_psbt *psbt, size_t index) +{ + if (psbt->version == WALLY_PSBT_VERSION_0) + return psbt->tx->outputs[index].satoshi; + return psbt->outputs[index].amount; +} + +static size_t psbt_output_scriptlen(const struct wally_psbt *psbt, size_t index) +{ + if (psbt->version == WALLY_PSBT_VERSION_0) + return psbt->tx->outputs[index].script_len; + return psbt->outputs[index].script_len; +} + +static const u8 *psbt_output_script(const struct wally_psbt *psbt, size_t index) +{ + if (psbt->version == WALLY_PSBT_VERSION_0) + return psbt->tx->outputs[index].script; + return psbt->outputs[index].script; +} + +/* We consider two PSBTs *equivalent* if they have the same inputs and outputs */ +static bool psbt_equivalent(const struct wally_psbt *a, + const struct wally_psbt *b) +{ + if (a->num_inputs != b->num_inputs) + return false; + if (a->num_outputs != b->num_outputs) + return false; + + for (size_t i = 0; i < a->num_inputs; i++) { + if (!memeq(psbt_input_txid(a, i), WALLY_TXHASH_LEN, + psbt_input_txid(b, i), WALLY_TXHASH_LEN)) + return false; + + if (psbt_input_index(a, i) != psbt_input_index(b, i)) + return false; + + if (psbt_input_sequence(a, i) != psbt_input_sequence(b, i)) + return false; + } + + for (size_t i = 0; i < a->num_outputs; i++) { + size_t a_scriptlen, b_scriptlen; + + if (psbt_output_amount(a, i) != psbt_output_amount(b, i)) + return false; + a_scriptlen = psbt_output_scriptlen(a, i); + b_scriptlen = psbt_output_scriptlen(b, i); + if (!memeq(psbt_output_script(a, i), a_scriptlen, + psbt_output_script(b, i), b_scriptlen)) + return false; + } + + return true; +} + static struct command_result *json_sendpsbt(struct command *cmd, const char *buffer, const jsmntok_t *obj, @@ -1045,6 +1125,8 @@ static struct command_result *json_sendpsbt(struct command *cmd, struct wally_psbt *psbt; struct lightningd *ld = cmd->ld; u32 *reserve_blocks; + struct peer *p; + struct peer_node_id_map_iter it; if (!param_check(cmd, buffer, params, p_req("psbt", param_psbt, &psbt), @@ -1079,6 +1161,30 @@ static struct command_result *json_sendpsbt(struct command *cmd, if (command_check_only(cmd)) return command_check_done(cmd); + /* If this corresponds to one or more channels' PSBT, upgrade + * those to signed versions! */ + for (p = peer_node_id_map_first(ld->peers, &it); + p; + p = peer_node_id_map_next(ld->peers, &it)) { + struct channel *c; + + list_for_each(&p->channels, c, list) { + if (!c->funding_psbt) + continue; + if (psbt_is_finalized(c->funding_psbt)) + continue; + if (!psbt_equivalent(psbt, c->funding_psbt)) + continue; + + /* Found one! */ + tal_free(c->funding_psbt); + c->funding_psbt = clone_psbt(c, sending->psbt); + wallet_channel_save(ld->wallet, c); + log_info(c->log, + "Funding PSBT sent, and stored for rexmit"); + } + } + for (size_t i = 0; i < tal_count(sending->utxos); i++) { if (!wallet_reserve_utxo(ld->wallet, sending->utxos[i], get_block_height(ld->topology), From 12bdf680bbc88eebc004d99c2c6fe15f6759b9f3 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:47:38 +0930 Subject: [PATCH 05/15] lightningd: re-xmit funding txs on startup. Signed-off-by: Rusty Russell Changelog-Fixed: Protocol: we now re-transmit unseen funding transactions on startup, for more robustness. --- lightningd/bitcoind.h | 2 +- lightningd/lightningd.c | 3 +- lightningd/peer_control.c | 41 +++++++++++++++++++++ lightningd/peer_control.h | 3 ++ lightningd/test/run-find_my_abspath.c | 3 ++ lightningd/test/run-invoice-select-inchan.c | 10 +++++ tests/test_opening.py | 1 - wallet/test/run-wallet.c | 10 +++++ 8 files changed, 70 insertions(+), 3 deletions(-) diff --git a/lightningd/bitcoind.h b/lightningd/bitcoind.h index cf92e907513d..870ffd118e0a 100644 --- a/lightningd/bitcoind.h +++ b/lightningd/bitcoind.h @@ -78,7 +78,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 1c3fb7e4d77a..e854d7efd7ee 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -1449,9 +1449,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 35b88089991f..6f13ed3f840f 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -501,6 +501,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 2a28183fdace..d52c6e936986 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 80702af24345..86668a342773 100644 --- a/lightningd/test/run-find_my_abspath.c +++ b/lightningd/test/run-find_my_abspath.c @@ -228,6 +228,9 @@ bool pubkey_from_node_id(struct pubkey *key UNNEEDED, const struct node_id *id U /* 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 fc7c642496ab..1f1f282a9ae8 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -26,6 +26,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 bolt11_decode */ struct bolt11 *bolt11_decode(const tal_t *ctx UNNEEDED, const char *str UNNEEDED, const struct feature_set *our_features UNNEEDED, diff --git a/tests/test_opening.py b/tests/test_opening.py index b2aecdbd1f75..90fedeba8838 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 4d98fa2be391..0c71b8c99e54 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -66,6 +66,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 blinding_hash_e_and_ss */ void blinding_hash_e_and_ss(const struct pubkey *e UNNEEDED, const struct secret *ss UNNEEDED, From ab7a606356b1dac3f1004393149d04d4e2e5be1d Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:47:39 +0930 Subject: [PATCH 06/15] pytest: test failure if we crash after fundchannel_complete but before sendpsbt. Signed-off-by: Rusty Russell --- tests/plugins/stop_sendpsbt.py | 20 ++++++++++++++++++++ tests/test_opening.py | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100755 tests/plugins/stop_sendpsbt.py diff --git a/tests/plugins/stop_sendpsbt.py b/tests/plugins/stop_sendpsbt.py new file mode 100755 index 000000000000..bd7ac093b995 --- /dev/null +++ b/tests/plugins/stop_sendpsbt.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +""" +This plugin is used to shutdown a node before processing the sendpsbt command +""" +from pyln.client import Plugin +import os, signal + +plugin = Plugin() + + +@plugin.hook("rpc_command") +def on_rpc_command(plugin, rpc_command, **kwargs): + request = rpc_command + if request["method"] == "sendpsbt": + os.kill(os.getppid(), signal.SIGKILL) + + return {"result": "continue"} + + +plugin.run() diff --git a/tests/test_opening.py b/tests/test_opening.py index 90fedeba8838..aa6e9f622f1d 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2847,3 +2847,23 @@ def censoring_sendrawtx(r): l1.start() bitcoind.generate_block(1, wait_for_mempool=txid) + + +@pytest.mark.xfail(strict=True) +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +def test_sendpsbt_crash(bitcoind, node_factory): + """Stop sendpsbt, check it eventually opens""" + plugin_path = Path(__file__).parent / "plugins" / "stop_sendpsbt.py" + l1, l2 = node_factory.get_nodes(2, opts=[{"plugin": plugin_path, 'may_fail': True}, {}]) + + l1.fundwallet(3_000_000) + l1.connect(l2) + + # signpsbt kills l1. + with pytest.raises(RpcError, match=r'Connection to RPC server lost.'): + l1.rpc.fundchannel(l2.info['id'], "2000000sat") + + del l1.daemon.opts['plugin'] + l1.start() + bitcoind.generate_block(1, wait_for_mempool=1) From f9a2d16a702fb49340beeaa379f7ca949fe1e870 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:47:39 +0930 Subject: [PATCH 07/15] lightningd: expose funding PSBT (if we have it) in JSON API. Changelog-Added: JSON-RPC: `psbt` field in `funding` in listpeerchannels, and `funding_psbt` in listclosedchannels. Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 16 ++++++++++++++++ doc/schemas/listclosedchannels.json | 8 ++++++++ doc/schemas/listpeerchannels.json | 8 ++++++++ lightningd/closed_channel.c | 2 ++ lightningd/peer_control.c | 2 ++ lightningd/test/run-invoice-select-inchan.c | 5 +++++ wallet/test/run-wallet.c | 5 +++++ 7 files changed, 46 insertions(+) diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 1b4fd50d0772..7ae513896613 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -17285,6 +17285,7 @@ "total_htlcs_sent", "funding_txid", "funding_outnum", + "funding_psbt", "leased", "final_to_us_msat", "min_to_us_msat", @@ -17430,6 +17431,13 @@ "The 0-based output number of the funding transaction which opens the channel." ] }, + "funding_psbt": { + "type": "string", + "added": "v25.12", + "description": [ + "The PSBT (may be non-final or unsigned) we should use to open the channel, if any" + ] + }, "leased": { "type": "boolean", "description": [ @@ -22987,6 +22995,7 @@ "type": "object", "additionalProperties": false, "required": [ + "psbt", "local_funds_msat", "remote_funds_msat" ], @@ -23020,6 +23029,13 @@ "description": [ "Amount we were paid by peer at open." ] + }, + "psbt": { + "type": "string", + "added": "v25.12", + "description": [ + "The PSBT (may be non-final or unsigned) we should use to open the channel, if any. This is initially from `fundchannel_complete`, but will be updated with if `sendpsbt` is called with an updated PSBT." + ] } } }, diff --git a/doc/schemas/listclosedchannels.json b/doc/schemas/listclosedchannels.json index becf033e692d..6bab9e36989c 100644 --- a/doc/schemas/listclosedchannels.json +++ b/doc/schemas/listclosedchannels.json @@ -43,6 +43,7 @@ "total_htlcs_sent", "funding_txid", "funding_outnum", + "funding_psbt", "leased", "final_to_us_msat", "min_to_us_msat", @@ -188,6 +189,13 @@ "The 0-based output number of the funding transaction which opens the channel." ] }, + "funding_psbt": { + "type": "string", + "added": "v25.12", + "description": [ + "The PSBT (may be non-final or unsigned) we should use to open the channel, if any" + ] + }, "leased": { "type": "boolean", "description": [ diff --git a/doc/schemas/listpeerchannels.json b/doc/schemas/listpeerchannels.json index 16844c0e7665..e352527b789d 100644 --- a/doc/schemas/listpeerchannels.json +++ b/doc/schemas/listpeerchannels.json @@ -469,6 +469,7 @@ "type": "object", "additionalProperties": false, "required": [ + "psbt", "local_funds_msat", "remote_funds_msat" ], @@ -502,6 +503,13 @@ "description": [ "Amount we were paid by peer at open." ] + }, + "psbt": { + "type": "string", + "added": "v25.12", + "description": [ + "The PSBT (may be non-final or unsigned) we should use to open the channel, if any. This is initially from `fundchannel_complete`, but will be updated with if `sendpsbt` is called with an updated PSBT." + ] } } }, diff --git a/lightningd/closed_channel.c b/lightningd/closed_channel.c index dd85f4fc2542..7cd799eeb646 100644 --- a/lightningd/closed_channel.c +++ b/lightningd/closed_channel.c @@ -64,6 +64,8 @@ static void json_add_closed_channel(struct json_stream *response, } else if (!amount_msat_is_zero(channel->push)) json_add_amount_msat(response, "funding_pushed_msat", channel->push); + if (channel->funding_psbt) + json_add_psbt(response, "funding_psbt", channel->funding_psbt); json_add_amount_sat_msat(response, "total_msat", channel->funding_sats); json_add_amount_msat(response, "final_to_us_msat", channel->our_msat); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 6f13ed3f840f..1f2867204ecb 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -1178,6 +1178,8 @@ static void NON_NULL_ARGS(1, 2, 4, 5) json_add_channel(struct command *cmd, channel->push); } + if (channel->funding_psbt) + json_add_psbt(response, "psbt", channel->funding_psbt); json_object_end(response); if (!amount_sat_to_msat(&funding_msat, channel->funding_sats)) { diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 1f1f282a9ae8..95908320d264 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -583,6 +583,11 @@ void json_add_num(struct json_stream *result UNNEEDED, const char *fieldname UNN void json_add_preimage(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, const struct preimage *preimage UNNEEDED) { fprintf(stderr, "json_add_preimage called!\n"); abort(); } +/* Generated stub for json_add_psbt */ +void json_add_psbt(struct json_stream *stream UNNEEDED, + const char *fieldname UNNEEDED, + const struct wally_psbt *psbt UNNEEDED) +{ fprintf(stderr, "json_add_psbt called!\n"); abort(); } /* Generated stub for json_add_s64 */ void json_add_s64(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, int64_t value UNNEEDED) diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 0c71b8c99e54..fa64a7fa6b06 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -515,6 +515,11 @@ void json_add_num(struct json_stream *result UNNEEDED, const char *fieldname UNN void json_add_preimage(struct json_stream *result UNNEEDED, const char *fieldname UNNEEDED, const struct preimage *preimage UNNEEDED) { fprintf(stderr, "json_add_preimage called!\n"); abort(); } +/* Generated stub for json_add_psbt */ +void json_add_psbt(struct json_stream *stream UNNEEDED, + const char *fieldname UNNEEDED, + const struct wally_psbt *psbt UNNEEDED) +{ fprintf(stderr, "json_add_psbt called!\n"); abort(); } /* Generated stub for json_add_pubkey */ void json_add_pubkey(struct json_stream *response UNNEEDED, const char *fieldname UNNEEDED, From b1a7812e0641038fd1808290b76915d037fd1fb5 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:47:40 +0930 Subject: [PATCH 08/15] spender: look for unsigned PSBT on awaiting channels on startup, and re-send. This covers the other corner case, where we crash before actually signing and sending the PSBT. We can spot this because the channel is in AWAITING_LOCKIN and we have a PSBT, but it's not signed yet. Signed-off-by: Rusty Russell --- plugins/spender/main.c | 2 +- plugins/spender/openchannel.c | 87 ++++++++++++++++++++++++++++++++++- plugins/spender/openchannel.h | 3 +- tests/test_opening.py | 3 +- 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/plugins/spender/main.c b/plugins/spender/main.c index 0f4151876ceb..a94e7859c520 100644 --- a/plugins/spender/main.c +++ b/plugins/spender/main.c @@ -11,7 +11,7 @@ static const char *spender_init(struct command *init_cmd, const char *b, const jsmntok_t *t) { - openchannel_init(init_cmd->plugin, b, t); + openchannel_init(init_cmd, b, t); /* whatever_init(p, b, t); */ return NULL; } diff --git a/plugins/spender/openchannel.c b/plugins/spender/openchannel.c index 90c5cdb28a46..59a53990947a 100644 --- a/plugins/spender/openchannel.c +++ b/plugins/spender/openchannel.c @@ -1033,10 +1033,95 @@ openchannel_init_dest(struct multifundchannel_destination *dest) return send_outreq(req); } -void openchannel_init(struct plugin *p, const char *b, const jsmntok_t *t) +static struct command_result *psbt_error(struct command *aux_cmd, + const char *methodname, + const char *buf, + const jsmntok_t *result, + struct channel_id *cid) +{ + plugin_log(aux_cmd->plugin, LOG_UNUSUAL, + "Failed %s for waiting channel %s: %.*s", + methodname, + fmt_channel_id(tmpctx, cid), + json_tok_full_len(result), + json_tok_full(buf, result)); + return aux_command_done(aux_cmd); +} + +static struct command_result *sendpsbt_done(struct command *aux_cmd, + const char *methodname, + const char *buf, + const jsmntok_t *result, + struct channel_id *cid) +{ + plugin_log(aux_cmd->plugin, LOG_INFORM, + "Signed and sent psbt for waiting channel %s", + fmt_channel_id(tmpctx, cid)); + return aux_command_done(aux_cmd); +} + +static struct command_result *signpsbt_done(struct command *aux_cmd, + const char *methodname, + const char *buf, + const jsmntok_t *result, + struct channel_id *cid) +{ + const jsmntok_t *psbttok = json_get_member(buf, result, "signed_psbt"); + struct wally_psbt *psbt = json_to_psbt(tmpctx, buf, psbttok); + struct out_req *req; + + req = jsonrpc_request_start(aux_cmd, "sendpsbt", + sendpsbt_done, psbt_error, + cid); + json_add_psbt(req->js, "psbt", psbt); + return send_outreq(req); +} + +/* If there are any channels with unsigned PSBTs in AWAITING_LOCKIN, + * sign them now (assume we crashed) */ +static void list_awaiting_channels(struct command *init_cmd) +{ + const char *buf; + size_t i; + const jsmntok_t *resp, *t, *channels; + + resp = jsonrpc_request_sync(tmpctx, init_cmd, + "listpeerchannels", + NULL, &buf); + channels = json_get_member(buf, resp, "channels"); + json_for_each_arr(i, t, channels) { + struct out_req *req; + const char *state; + struct channel_id cid; + struct command *aux_cmd; + struct wally_psbt *psbt; + + if (json_scan(tmpctx, buf, t, "{state:%,channel_id:%,funding:{psbt:%}}", + JSON_SCAN_TAL(tmpctx, json_strdup, &state), + JSON_SCAN(json_tok_channel_id, &cid), + JSON_SCAN_TAL(tmpctx, json_to_psbt, &psbt)) != NULL) + continue; + + if (!streq(state, "CHANNELD_AWAITING_LOCKIN") + && !streq(state, "DUALOPEND_AWAITING_LOCKIN")) + continue; + + /* Don't do this sync, as it can reasonably fail! */ + aux_cmd = aux_command(init_cmd); + req = jsonrpc_request_start(aux_cmd, "signpsbt", + signpsbt_done, psbt_error, + tal_dup(aux_cmd, struct channel_id, &cid)); + json_add_psbt(req->js, "psbt", psbt); + send_outreq(req); + } +} + +void openchannel_init(struct command *init_cmd, const char *b, const jsmntok_t *t) { /* Initialize our list! */ list_head_init(&mfc_commands); + + list_awaiting_channels(init_cmd); } const struct plugin_notification openchannel_notifs[] = { diff --git a/plugins/spender/openchannel.h b/plugins/spender/openchannel.h index bd2957b14095..3dfea2ca59ed 100644 --- a/plugins/spender/openchannel.h +++ b/plugins/spender/openchannel.h @@ -4,6 +4,7 @@ #include struct wally_psbt; +struct command; extern const struct plugin_notification openchannel_notifs[]; extern const size_t num_openchannel_notifs; @@ -15,7 +16,7 @@ void register_mfc(struct multifundchannel_command *mfc); struct command_result * openchannel_init_dest(struct multifundchannel_destination *dest); -void openchannel_init(struct plugin *p, const char *b, +void openchannel_init(struct command *init_cmd, const char *b, const jsmntok_t *t); struct command_result * diff --git a/tests/test_opening.py b/tests/test_opening.py index aa6e9f622f1d..2b3313b9b028 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2849,7 +2849,6 @@ def censoring_sendrawtx(r): bitcoind.generate_block(1, wait_for_mempool=txid) -@pytest.mark.xfail(strict=True) @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') def test_sendpsbt_crash(bitcoind, node_factory): @@ -2867,3 +2866,5 @@ def test_sendpsbt_crash(bitcoind, node_factory): del l1.daemon.opts['plugin'] l1.start() bitcoind.generate_block(1, wait_for_mempool=1) + + assert l1.daemon.is_in_log('Signed and sent psbt for waiting channel') From ef65316f36afbe5597163447a50d4e1025747a5c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:47:40 +0930 Subject: [PATCH 09/15] lightningd: save funding PSBT to database if we're to withhold it. Normally we don't care, but if we're withholding it, keep it around so we can sign & broadcast later. Signed-off-by: Rusty Russell --- lightningd/channel.c | 5 ++++- lightningd/channel.h | 6 +++++- lightningd/closed_channel.h | 1 + lightningd/opening_common.h | 3 +++ lightningd/opening_control.c | 18 +++++++++++++----- wallet/db.c | 1 + wallet/test/run-db.c | 3 ++- wallet/test/run-wallet.c | 3 ++- wallet/wallet.c | 11 +++++++++-- 9 files changed, 40 insertions(+), 11 deletions(-) diff --git a/lightningd/channel.c b/lightningd/channel.c index c628dd8f2ab4..b1c5adf59110 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -428,6 +428,7 @@ struct channel *new_unsaved_channel(struct peer *peer, channel->channel_gossip = NULL; channel->forgets = tal_arr(channel, struct command *, 0); channel->funding_psbt = NULL; + channel->withheld = false; list_add_tail(&peer->channels, &channel->list); channel->rr_number = peer->ld->rr_counter++; tal_add_destructor(channel, destroy_channel); @@ -550,7 +551,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid, u64 last_stable_connection, const struct channel_stats *stats, struct channel_state_change **state_changes STEALS, - const struct wally_psbt *funding_psbt STEALS) + const struct wally_psbt *funding_psbt STEALS, + bool withheld) { struct channel *channel = tal(peer->ld, struct channel); struct amount_msat htlc_min, htlc_max; @@ -751,6 +753,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid, &channel->cid, "We can't be together anymore."); channel->funding_psbt = tal_steal(channel, funding_psbt); + channel->withheld = withheld; return channel; } diff --git a/lightningd/channel.h b/lightningd/channel.h index 1fdcf289eade..150c7d748a57 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -362,6 +362,9 @@ struct channel { /* Unsigned PSBT if we initiated the open channel */ const struct wally_psbt *funding_psbt; + + /* Are we not broadcasting the open tx? */ + bool withheld; }; /* Is channel owned (and should be talking to peer) */ @@ -448,7 +451,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid, u64 last_stable_connection, const struct channel_stats *stats, struct channel_state_change **state_changes STEALS, - const struct wally_psbt *funding_psbt STEALS); + const struct wally_psbt *funding_psbt STEALS, + bool withheld); /* new_inflight - Create a new channel_inflight for a channel */ struct channel_inflight *new_inflight(struct channel *channel, diff --git a/lightningd/closed_channel.h b/lightningd/closed_channel.h index cf7c91be7f79..68822bc507b2 100644 --- a/lightningd/closed_channel.h +++ b/lightningd/closed_channel.h @@ -31,6 +31,7 @@ struct closed_channel { /* NULL for older closed channels */ const struct shachain *their_shachain; const struct wally_psbt *funding_psbt; + bool withheld; }; static inline const struct channel_id *keyof_closed_channel(const struct closed_channel *cc) diff --git a/lightningd/opening_common.h b/lightningd/opening_common.h index d17a3f9bf9e3..855797ef6cef 100644 --- a/lightningd/opening_common.h +++ b/lightningd/opening_common.h @@ -106,6 +106,9 @@ struct funding_channel { /* Were we the one to publish the commitment/splicing tx? */ const struct wally_psbt *funding_psbt; + + /* Were we told to withhold the commitment tx? */ + bool withheld; }; struct uncommitted_channel *new_uncommitted_channel(struct peer *peer); diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index 7fd826b40575..e3d897d246a9 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -99,7 +99,8 @@ wallet_commit_channel(struct lightningd *ld, const u8 *our_upfront_shutdown_script, const u8 *remote_upfront_shutdown_script, const struct channel_type *type, - const struct wally_psbt *funding_psbt) + const struct wally_psbt *funding_psbt, + bool withheld) { struct channel *channel; struct amount_msat our_msat; @@ -239,7 +240,8 @@ wallet_commit_channel(struct lightningd *ld, 0, &zero_channel_stats, tal_arr(NULL, struct channel_state_change *, 0), - funding_psbt); + funding_psbt, + withheld); /* Now we finally put it in the database. */ wallet_channel_insert(ld->wallet, channel); @@ -448,7 +450,8 @@ static void opening_funder_finished(struct subd *openingd, const u8 *resp, fc->our_upfront_shutdown_script, remote_upfront_shutdown_script, type, - fc->funding_psbt); + fc->funding_psbt, + fc->withheld); if (!channel) { was_pending(command_fail(fc->cmd, LIGHTNINGD, "Key generation failure")); @@ -552,7 +555,8 @@ static void opening_fundee_finished(struct subd *openingd, local_upfront_shutdown_script, remote_upfront_shutdown_script, type, - NULL); + NULL, + false); if (!channel) { uncommitted_channel_disconnect(uc, LOG_BROKEN, "Commit channel failed"); @@ -1098,6 +1102,9 @@ static struct command_result *json_fundchannel_complete(struct command *cmd, fc->funding_psbt = tal_steal(fc, funding_psbt); + /* FIXME: Set by option */ + fc->withheld = false; + /* Set the cmd to this new cmd */ peer->uncommitted_channel->fc->cmd = cmd; msg = towire_openingd_funder_complete(NULL, @@ -1646,7 +1653,8 @@ static struct channel *stub_chan(struct command *cmd, 0, &zero_channel_stats, tal_arr(NULL, struct channel_state_change *, 0), - NULL); + NULL, + false); /* We don't want to gossip about this, ever. */ channel->channel_gossip = tal_free(channel->channel_gossip); diff --git a/wallet/db.c b/wallet/db.c index 6057952dd9c1..1acd8b6c49c1 100644 --- a/wallet/db.c +++ b/wallet/db.c @@ -1093,6 +1093,7 @@ static struct migration dbmigrations[] = { /* We do a lookup before each append, to avoid duplicates */ {SQL("CREATE INDEX chain_moves_utxo_idx ON chain_moves (utxo)"), NULL}, {NULL, migrate_from_account_db}, + {SQL("ALTER TABLE channels ADD withheld INTEGER DEFAULT 0;"), NULL}, }; /** diff --git a/wallet/test/run-db.c b/wallet/test/run-db.c index ba675f95b003..e462c1f2c0ac 100644 --- a/wallet/test/run-db.c +++ b/wallet/test/run-db.c @@ -255,7 +255,8 @@ struct channel *new_channel(struct peer *peer UNNEEDED, u64 dbid UNNEEDED, u64 last_stable_connection UNNEEDED, const struct channel_stats *stats UNNEEDED, struct channel_state_change **state_changes STEALS UNNEEDED, - const struct wally_psbt *funding_psbt STEALS UNNEEDED) + const struct wally_psbt *funding_psbt STEALS UNNEEDED, + bool withheld UNNEEDED) { fprintf(stderr, "new_channel called!\n"); abort(); } /* Generated stub for new_channel_coin_mvt_general */ struct channel_coin_mvt *new_channel_coin_mvt_general(const tal_t *ctx UNNEEDED, diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index fa64a7fa6b06..6f3eec098e39 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -2130,7 +2130,8 @@ static bool test_channel_inflight_crud(struct lightningd *ld, const tal_t *ctx) 0, stats, tal_arr(NULL, struct channel_state_change *, 0), - NULL); + NULL, + false); db_begin_transaction(w->db); CHECK(!wallet_err); wallet_channel_insert(w, chan); diff --git a/wallet/wallet.c b/wallet/wallet.c index 59a1f63cc8ff..90dab307a72d 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -2122,7 +2122,8 @@ static struct channel *wallet_stmt2channel(struct wallet *w, struct db_stmt *stm db_col_u64(stmt, "last_stable_connection"), &stats, state_changes, - funding_psbt); + funding_psbt, + db_col_int(stmt, "withheld")); if (!wallet_channel_load_inflights(w, chan)) { tal_free(chan); @@ -2180,6 +2181,7 @@ static struct closed_channel *wallet_stmt2closed_channel(const tal_t *ctx, cc->funding_psbt = db_col_psbt(cc, stmt, "funding_psbt"); else cc->funding_psbt = NULL; + cc->withheld = db_col_int(stmt, "withheld"); return cc; } @@ -2216,6 +2218,7 @@ void wallet_load_closed_channels(struct wallet *w, ", last_stable_connection" ", shachain_remote_id" ", funding_psbt" + ", withheld" " FROM channels" " LEFT JOIN peers p ON p.id = peer_id" " WHERE state = ?;")); @@ -2262,6 +2265,7 @@ void wallet_load_one_closed_channel(struct wallet *w, ", last_stable_connection" ", shachain_remote_id" ", funding_psbt" + ", withheld" " FROM channels" " LEFT JOIN peers p ON p.id = peer_id" " WHERE channels.id = ?;")); @@ -2382,6 +2386,7 @@ static bool wallet_channels_load_active(struct wallet *w) ", out_msatoshi_fulfilled" ", close_attempt_height" ", funding_psbt" + ", withheld" " FROM channels" " WHERE state != ?;")); //? 0 db_bind_int(stmt, CLOSED); @@ -2634,7 +2639,8 @@ void wallet_channel_save(struct wallet *w, struct channel *chan) " last_stable_connection=?," " require_confirm_inputs_remote=?," " close_attempt_height=?," - " funding_psbt=?" + " funding_psbt=?," + " withheld=?" " WHERE id=?")); db_bind_u64(stmt, chan->their_shachain.id); if (chan->scid) @@ -2736,6 +2742,7 @@ void wallet_channel_save(struct wallet *w, struct channel *chan) db_bind_psbt(stmt, chan->funding_psbt); else db_bind_null(stmt); + db_bind_int(stmt, chan->withheld); db_bind_u64(stmt, chan->dbid); db_exec_prepared_v2(take(stmt)); From dd1c653b00d1b59d98b894812dd5a1dafecda2d2 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:52:43 +0930 Subject: [PATCH 10/15] lightningd: add withheld flag to listpeerchannels and listclosedchannels. Signed-off-by: Rusty Russell Changelog-Added: JSON-RPC: `listpeerchannels` `funding` object `withheld` flag, and `listclosedchannels` `funding_withheld` flags, indicating fundchannel_complete was called with the `withheld` parameter true. --- contrib/msggen/msggen/schema.json | 15 +++++++++++++++ doc/schemas/listclosedchannels.json | 8 ++++++++ doc/schemas/listpeerchannels.json | 7 +++++++ lightningd/closed_channel.c | 1 + lightningd/peer_control.c | 1 + 5 files changed, 32 insertions(+) diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 7ae513896613..5c79596d00cf 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -17286,6 +17286,7 @@ "funding_txid", "funding_outnum", "funding_psbt", + "funding_withheld", "leased", "final_to_us_msat", "min_to_us_msat", @@ -17438,6 +17439,13 @@ "The PSBT (may be non-final or unsigned) we should use to open the channel, if any" ] }, + "funding_withheld": { + "type": "boolean", + "added": "v25.12", + "description": [ + "True if we have not broadcast the funding transaction (see fundchannel_complete)." + ] + }, "leased": { "type": "boolean", "description": [ @@ -23036,6 +23044,13 @@ "description": [ "The PSBT (may be non-final or unsigned) we should use to open the channel, if any. This is initially from `fundchannel_complete`, but will be updated with if `sendpsbt` is called with an updated PSBT." ] + }, + "withheld": { + "type": "boolean", + "added": "v25.12", + "description": [ + "True if `fundchannel_complete` told us it will not broadcast the funding transaction (so we know not to bother with any other onchain transactions in the case of this channel). This is set to false if `sendpsbt` is send on the above PSBT." + ] } } }, diff --git a/doc/schemas/listclosedchannels.json b/doc/schemas/listclosedchannels.json index 6bab9e36989c..637addb79b37 100644 --- a/doc/schemas/listclosedchannels.json +++ b/doc/schemas/listclosedchannels.json @@ -44,6 +44,7 @@ "funding_txid", "funding_outnum", "funding_psbt", + "funding_withheld", "leased", "final_to_us_msat", "min_to_us_msat", @@ -196,6 +197,13 @@ "The PSBT (may be non-final or unsigned) we should use to open the channel, if any" ] }, + "funding_withheld": { + "type": "boolean", + "added": "v25.12", + "description": [ + "True if we have not broadcast the funding transaction (see fundchannel_complete)." + ] + }, "leased": { "type": "boolean", "description": [ diff --git a/doc/schemas/listpeerchannels.json b/doc/schemas/listpeerchannels.json index e352527b789d..a3b57a90cbda 100644 --- a/doc/schemas/listpeerchannels.json +++ b/doc/schemas/listpeerchannels.json @@ -510,6 +510,13 @@ "description": [ "The PSBT (may be non-final or unsigned) we should use to open the channel, if any. This is initially from `fundchannel_complete`, but will be updated with if `sendpsbt` is called with an updated PSBT." ] + }, + "withheld": { + "type": "boolean", + "added": "v25.12", + "description": [ + "True if `fundchannel_complete` told us it will not broadcast the funding transaction (so we know not to bother with any other onchain transactions in the case of this channel). This is set to false if `sendpsbt` is send on the above PSBT." + ] } } }, diff --git a/lightningd/closed_channel.c b/lightningd/closed_channel.c index 7cd799eeb646..1d88cebc5fad 100644 --- a/lightningd/closed_channel.c +++ b/lightningd/closed_channel.c @@ -66,6 +66,7 @@ static void json_add_closed_channel(struct json_stream *response, channel->push); if (channel->funding_psbt) json_add_psbt(response, "funding_psbt", channel->funding_psbt); + json_add_bool(response, "funding_withheld", channel->withheld); json_add_amount_sat_msat(response, "total_msat", channel->funding_sats); json_add_amount_msat(response, "final_to_us_msat", channel->our_msat); diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index 1f2867204ecb..e2691be81e72 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -1180,6 +1180,7 @@ static void NON_NULL_ARGS(1, 2, 4, 5) json_add_channel(struct command *cmd, if (channel->funding_psbt) json_add_psbt(response, "psbt", channel->funding_psbt); + json_add_bool(response, "withheld", channel->withheld); json_object_end(response); if (!amount_sat_to_msat(&funding_msat, channel->funding_sats)) { From fbafa21b615b17f799412e048c1a1cab432b3068 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:52:58 +0930 Subject: [PATCH 11/15] lightningd: immediately close without broadcast whenever we close a withheld channel. There's no funding tx to spend. Signed-off-by: Rusty Russell --- lightningd/channel.c | 16 +++++++++++++--- lightningd/closing_control.c | 13 +++++++++---- lightningd/peer_control.c | 11 +++++++++++ lightningd/test/run-invoice-select-inchan.c | 3 +++ plugins/spender/openchannel.c | 7 ++++++- 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/lightningd/channel.c b/lightningd/channel.c index b1c5adf59110..c7b1963affb8 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -1104,12 +1104,24 @@ static void channel_fail_perm(struct channel *channel, channel_set_owner(channel, NULL); - if (channel_state_wants_onchain_fail(channel->state)) + if (channel->withheld) { + channel_set_state(channel, + channel->state, + CLOSED, + reason, + why); + } else if (channel_state_wants_onchain_fail(channel->state)) { channel_set_state(channel, channel->state, AWAITING_UNILATERAL, reason, why); + } + + if (channel_state_open_uncommitted(channel->state)) { + delete_channel(channel, false); + return; + } /* Drop non-cooperatively (unilateral) to chain. If we detect * the close from the blockchain, then we can observe @@ -1117,8 +1129,6 @@ static void channel_fail_perm(struct channel *channel, * it doesn't stand a chance anyway. */ drop_to_chain(ld, channel, false, spent_by); - if (channel_state_open_uncommitted(channel->state)) - delete_channel(channel, false); } void channel_fail_permanent(struct channel *channel, diff --git a/lightningd/closing_control.c b/lightningd/closing_control.c index 36fe90f696bb..8b761f27240d 100644 --- a/lightningd/closing_control.c +++ b/lightningd/closing_control.c @@ -54,13 +54,18 @@ static void resolve_one_close_command(struct close_command *cc, bool cooperative, const struct bitcoin_tx **close_txs) { - assert(tal_count(close_txs)); struct json_stream *result = json_stream_success(cc->cmd); - const struct bitcoin_tx *close_tx = close_txs[tal_count(close_txs) - 1]; + const struct bitcoin_tx *close_tx; - if (command_deprecated_out_ok(cc->cmd, "tx", "v24.11", "v25.12")) + /* Withheld funding channels can have no close_txs! */ + if (tal_count(close_txs) != 0) + close_tx = close_txs[tal_count(close_txs) - 1]; + else + close_tx = NULL; + + if (close_tx && command_deprecated_out_ok(cc->cmd, "tx", "v24.11", "v25.12")) json_add_tx(result, "tx", close_tx); - if (!invalid_last_tx(close_tx)) { + if (close_tx && !invalid_last_tx(close_tx)) { struct bitcoin_txid txid; bitcoin_txid(close_tx, &txid); if (command_deprecated_out_ok(cc->cmd, "txid", "v24.11", "v25.12")) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index e2691be81e72..a2fb68789c4f 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -371,6 +371,17 @@ void drop_to_chain(struct lightningd *ld, struct channel *channel, struct channel_inflight *inflight; const char *cmd_id; + /* If we withheld the funding tx, we simply close */ + if (channel->withheld) { + log_info(channel->log, + "Withheld channel: not sending a close transaction"); + resolve_close_command(ld, channel, cooperative, + tal_arr(tmpctx, const struct bitcoin_tx *, 0)); + free_htlcs(ld, channel); + delete_channel(channel, false); + return; + } + /* If we're not already (e.g. close before channel fully open), * make sure we're watching for the funding spend */ if (!channel->funding_spend_watch) { diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index 95908320d264..b03e85240de4 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -315,6 +315,9 @@ void force_peer_disconnect(struct lightningd *ld UNNEEDED, const struct peer *peer UNNEEDED, const char *why UNNEEDED) { fprintf(stderr, "force_peer_disconnect called!\n"); abort(); } +/* Generated stub for free_htlcs */ +void free_htlcs(struct lightningd *ld UNNEEDED, const struct channel *channel UNNEEDED) +{ fprintf(stderr, "free_htlcs called!\n"); abort(); } /* Generated stub for fromwire_bigsize */ bigsize_t fromwire_bigsize(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) { fprintf(stderr, "fromwire_bigsize called!\n"); abort(); } diff --git a/plugins/spender/openchannel.c b/plugins/spender/openchannel.c index 59a53990947a..15a6c2ddb2e2 100644 --- a/plugins/spender/openchannel.c +++ b/plugins/spender/openchannel.c @@ -1095,10 +1095,12 @@ static void list_awaiting_channels(struct command *init_cmd) struct channel_id cid; struct command *aux_cmd; struct wally_psbt *psbt; + bool withheld; - if (json_scan(tmpctx, buf, t, "{state:%,channel_id:%,funding:{psbt:%}}", + if (json_scan(tmpctx, buf, t, "{state:%,channel_id:%,funding:{withheld:%,psbt:%}}", JSON_SCAN_TAL(tmpctx, json_strdup, &state), JSON_SCAN(json_tok_channel_id, &cid), + JSON_SCAN(json_to_bool, &withheld), JSON_SCAN_TAL(tmpctx, json_to_psbt, &psbt)) != NULL) continue; @@ -1106,6 +1108,9 @@ static void list_awaiting_channels(struct command *init_cmd) && !streq(state, "DUALOPEND_AWAITING_LOCKIN")) continue; + if (withheld) + continue; + /* Don't do this sync, as it can reasonably fail! */ aux_cmd = aux_command(init_cmd); req = jsonrpc_request_start(aux_cmd, "signpsbt", From 13226c1f98001d84e2c441bbae49e31411260753 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:52:59 +0930 Subject: [PATCH 12/15] lightningd: don't rebroadcast withheld channels' funding_psbt on restart. Signed-off-by: Rusty Russell --- lightningd/peer_control.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index a2fb68789c4f..d4f8d79ba1af 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -536,7 +536,7 @@ void resend_opening_transactions(struct lightningd *ld) struct wally_tx *wtx; if (channel_state_uncommitted(channel->state)) continue; - if (!channel->funding_psbt) + if (!channel->funding_psbt || channel->withheld) continue; if (channel->depth != 0) continue; From 8492b08e85a1fd918a732c9c4d8342f61f17d83d Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:52:59 +0930 Subject: [PATCH 13/15] lightningd: add withhold option to fundchannel_complete. This is just a polite way of telling us that if we close, don't bother broadcasting since we didn't broadcast the funding tx. Changelog-Added: JSON-RPC: `fundchannel_complete` new parameter `withhold` (default false). Signed-off-by: Rusty Russell --- contrib/msggen/msggen/schema.json | 7 +++++++ doc/schemas/fundchannel_complete.json | 7 +++++++ lightningd/opening_control.c | 6 +++--- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 5c79596d00cf..71ec0c3ec63a 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -13029,6 +13029,13 @@ "description": [ "Transaction to use for funding (does not need to be signed but must be otherwise complete)." ] + }, + "withhold": { + "type": "boolean", + "added": "v25.12", + "description": [ + "Mark this channel 'withheld' so we know we haven't broadcast the funding transaction. If the channel is closed before we call `sendpsbt` on this psbt, it will simply be closed immediately." + ] } } }, diff --git a/doc/schemas/fundchannel_complete.json b/doc/schemas/fundchannel_complete.json index d064026b1d6c..02921ea189bf 100644 --- a/doc/schemas/fundchannel_complete.json +++ b/doc/schemas/fundchannel_complete.json @@ -26,6 +26,13 @@ "description": [ "Transaction to use for funding (does not need to be signed but must be otherwise complete)." ] + }, + "withhold": { + "type": "boolean", + "added": "v25.12", + "description": [ + "Mark this channel 'withheld' so we know we haven't broadcast the funding transaction. If the channel is closed before we call `sendpsbt` on this psbt, it will simply be closed immediately." + ] } } }, diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index e3d897d246a9..aca07139fba2 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -1025,10 +1025,12 @@ static struct command_result *json_fundchannel_complete(struct command *cmd, struct wally_psbt *funding_psbt; u32 *funding_txout_num = NULL; struct funding_channel *fc; + bool *withhold; if (!param_check(cmd, buffer, params, p_req("id", param_node_id, &id), p_req("psbt", param_psbt, &funding_psbt), + p_opt_def("withhold", param_bool, &withhold, false), NULL)) return command_param_failed(); @@ -1101,9 +1103,7 @@ static struct command_result *json_fundchannel_complete(struct command *cmd, return command_check_done(cmd); fc->funding_psbt = tal_steal(fc, funding_psbt); - - /* FIXME: Set by option */ - fc->withheld = false; + fc->withheld = *withhold; /* Set the cmd to this new cmd */ peer->uncommitted_channel->fc->cmd = cmd; From e427f0c2b59c6360f5c190da3efe513bc33ada2b Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:52:59 +0930 Subject: [PATCH 14/15] lightningd: remove withheld flag when we see sendpsbt. Signed-off-by: Rusty Russell --- wallet/walletrpc.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index 996f53d91860..8d77b25eb17d 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -1169,6 +1169,8 @@ static struct command_result *json_sendpsbt(struct command *cmd, struct channel *c; list_for_each(&p->channels, c, list) { + bool was_withheld; + if (!c->funding_psbt) continue; if (psbt_is_finalized(c->funding_psbt)) @@ -1179,9 +1181,12 @@ static struct command_result *json_sendpsbt(struct command *cmd, /* Found one! */ tal_free(c->funding_psbt); c->funding_psbt = clone_psbt(c, sending->psbt); + was_withheld = c->withheld; + c->withheld = false; wallet_channel_save(ld->wallet, c); log_info(c->log, - "Funding PSBT sent, and stored for rexmit"); + "Funding PSBT sent, and stored for rexmit%s", + was_withheld ? " (was withheld)" : ""); } } From b08672059305aed80568b9d620bac6fabd5abe4e Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 13 Sep 2025 00:52:59 +0930 Subject: [PATCH 15/15] pytest: test withhold parameter to fundchannel_complete. Signed-off-by: Rusty Russell --- contrib/pyln-client/pyln/client/lightning.py | 3 +- tests/test_opening.py | 67 ++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 1706a4727e3b..3ec01f64f76d 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -844,13 +844,14 @@ def fundchannel_cancel(self, node_id): } return self.call("fundchannel_cancel", payload) - def fundchannel_complete(self, node_id, psbt): + def fundchannel_complete(self, node_id, psbt, withhold=True): """ Complete channel establishment with {id}, using {psbt}. """ payload = { "id": node_id, "psbt": psbt, + "withhold": withhold, } return self.call("fundchannel_complete", payload) diff --git a/tests/test_opening.py b/tests/test_opening.py index 2b3313b9b028..1359b21bdc91 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2868,3 +2868,70 @@ def test_sendpsbt_crash(bitcoind, node_factory): bitcoind.generate_block(1, wait_for_mempool=1) assert l1.daemon.is_in_log('Signed and sent psbt for waiting channel') + + +@pytest.mark.parametrize("stay_withheld", [True, False]) +@pytest.mark.parametrize("mutual_close", [True, False]) +def test_zeroconf_withhold(node_factory, bitcoind, stay_withheld, mutual_close): + plugin_path = Path(__file__).parent / "plugins" / "zeroconf-selective.py" + + l1, l2 = node_factory.get_nodes(2, opts=[{'may_reconnect': True}, + { + 'plugin': str(plugin_path), + 'zeroconf_allow': '0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518', + 'may_reconnect': True + }]) + # Try to open a mindepth=0 channel + l1.fundwallet(10**7) + + l1.connect(l2) + amount = 1000000 + funding_addr = l1.rpc.fundchannel_start(l2.info['id'], f"{amount}sat", mindepth=0)['funding_address'] + + # Create the funding transaction + psbt = l1.rpc.fundpsbt(amount, "1000perkw", 1000, excess_as_change=True)['psbt'] + psbt = l1.rpc.addpsbtoutput(1000000, psbt, destination=funding_addr)['psbt'] + + # Be sure fundchannel_complete is successful + assert l1.rpc.fundchannel_complete(l2.info['id'], psbt, withhold=True)['commitments_secured'] + + # It's withheld. + assert only_one(l1.rpc.listpeerchannels()['channels'])['funding']['withheld'] is True + + # We can use the channel. + l1.rpc.xpay(l2.rpc.invoice(100, "test_zeroconf_withhold", "test_zeroconf_withhold")['bolt11']) + + # But mempool is empty! No funding tx! + assert bitcoind.rpc.getrawmempool() == [] + + # Restarting doesn't make it transmit! + l1.restart() + assert bitcoind.rpc.getrawmempool() == [] + + if mutual_close: + l1.connect(l2) + + if not stay_withheld: + # sendpsbt marks it as no longer withheld. + l1.rpc.sendpsbt(l1.rpc.signpsbt(psbt)['signed_psbt']) + assert only_one(l1.rpc.listpeerchannels()['channels'])['funding']['withheld'] is False + assert l1.daemon.is_in_log(r'Funding PSBT sent, and stored for rexmit \(was withheld\)') + wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == 1) + + ret = l1.rpc.close(l2.info['id'], unilateraltimeout=10) + if stay_withheld: + assert ret['txs'] == [] + assert ret['txids'] == [] + assert bitcoind.rpc.getrawmempool() == [] + else: + assert len(ret['txs']) == 1 + assert len(ret['txids']) == 1 + wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == 2) + + # If withheld, it's moved to closed immediately. + if stay_withheld: + assert l1.rpc.listpeerchannels()['channels'] == [] + assert only_one(l1.rpc.listclosedchannels()['closedchannels'])['funding_withheld'] is True + else: + wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['state'] == 'CLOSINGD_COMPLETE') +