From df5f38dbc17dcbd00fe5207c68ccf2918d89aa2b Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 18 Nov 2025 14:41:04 +1030 Subject: [PATCH] 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 0f4151876..a94e7859c 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 7d2191620..fe1d4d4d3 100644 --- a/plugins/spender/openchannel.c +++ b/plugins/spender/openchannel.c @@ -1034,10 +1034,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 bd2957b14..3dfea2ca5 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 2258a149b..19e1a2724 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2849,7 +2849,6 @@ def test_opening_crash(bitcoind, node_factory): bitcoind.generate_block(1, wait_for_mempool=txid) -@pytest.mark.xfail(strict=True) @pytest.mark.openchannel('v1') def test_sendpsbt_crash(bitcoind, node_factory): """Stop sendpsbt, check it eventually opens""" @@ -2866,3 +2865,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')