From 77d7feecbf3f5dcc2d98edefacce7dc45467afc8 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 24 Jun 2026 09:59:04 +0100 Subject: [PATCH] xpay: add a flag to indicate completion of initialization Add a "ready" flag that becomes true after the plugin has finished initializing. Usually RPC calls during initialization are done using synchronous communication but xpay uses hooks to bind itself to "pay". For some reason registering to hooks and using rcp_scan at init cannot be done. Therefore xpay's initialization is asynchronous which has the downside of race conditions like: trying to make a payment while xpay does not know yet the current node id. This is unlikely to happen in real life but it breaks our tests randomly. Fixes flake `tests/test_xpay.py:test_xpay_fake_channeld` ``` Valgrind error file: valgrind-errors.356233 ==356233== Conditional jump or move depends on uninitialised value(s) ==356233== at 0x1116ED: handle_block_added (xpay.c:3159) ==356233== by 0x11D7A8: ld_command_handle (libplugin.c:2144) ==356233== by 0x11DB8A: ld_read_json (libplugin.c:2282) ==356233== by 0x15DAC1: next_plan (io.c:60) ==356233== by 0x15DF4C: do_plan (io.c:422) ==356233== by 0x15E005: io_ready (io.c:439) ==356233== by 0x15F99B: io_loop (poll.c:470) ==356233== by 0x11DFD6: plugin_main (libplugin.c:2481) ==356233== by 0x11876E: main (xpay.c:3412) ==356233== { Memcheck:Cond fun:handle_block_added fun:ld_command_handle fun:ld_read_json fun:next_plan fun:do_plan fun:io_ready fun:io_loop fun:plugin_main fun:main } ``` Changelog-None Signed-off-by: Lagrang3 --- plugins/xpay/xpay.c | 92 +++++++++++++++++++++++++++------------------ tests/test_xpay.py | 2 + 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c index 7cf2c2061ccc..6356b2847256 100644 --- a/plugins/xpay/xpay.c +++ b/plugins/xpay/xpay.c @@ -52,6 +52,13 @@ struct xpay { /* Suppress calls to askrene-age */ bool dev_no_age; const char **user_layers; + /* We cannot initialize xpay with rpc_scan, we use instead asynchronous + * requests to fetch the node_id, the blockheight and create the xpay + * layer in askrene. Hence we need a flag to signal when we are ready + * for processing payments otherwise we get a race condition: a payment + * arrives and we don't know our own node_id yet, for example. In + * practice this will be more useful for tests. */ + bool ready; }; static struct xpay *xpay_of(struct plugin *plugin) @@ -2303,6 +2310,7 @@ static struct command_result *json_xpay_params(struct command *cmd, const jsmntok_t *params, bool as_pay) { + struct xpay *xpay = xpay_of(cmd->plugin); struct amount_msat *msat, *maxfee, *partial; const char *invstring; const char **layers; @@ -2329,6 +2337,8 @@ static struct command_result *json_xpay_params(struct command *cmd, p_opt_dev("dev_use_shadow", param_bool, &dev_use_shadow, true), NULL)) return command_param_failed(); + if (!xpay->ready) + return command_fail(cmd, PLUGIN_ERROR, "xpay is initializing"); /* Is this a one-shot vibe payment? Kids these days! */ if (!as_pay && bolt12_has_offer_prefix(invstring)) { @@ -2513,6 +2523,8 @@ static struct payment *new_payment(const tal_t *ctx, bool attempt_ongoing(struct plugin *plugin, const struct sha256 *payment_hash) { struct xpay *xpay = xpay_of(plugin); + if (!xpay->ready) + return false; const struct payment *payment; list_for_each(&xpay->payments, payment, list) { @@ -2815,6 +2827,7 @@ static struct command_result *json_sendamount(struct command *cmd, const char *buffer, const jsmntok_t *params) { + struct xpay *xpay = xpay_of(cmd->plugin); struct amount_msat *send_msat, *maxfee; struct amount_msat invoice_msat; const char *invstring; @@ -2837,6 +2850,8 @@ static struct command_result *json_sendamount(struct command *cmd, p_opt("label", param_label, &label), NULL)) return command_param_failed(); + if (!xpay->ready) + return command_fail(cmd, PLUGIN_ERROR, "xpay is initializing"); // FIXME: why does xpay returns this only after // preapproveinvoice_succeed? @@ -2901,21 +2916,15 @@ static struct command_result *json_sendamount(struct command *cmd, label, NULL, false, false, send_msat); } -static struct command_result *getchaininfo_done(struct command *aux_cmd, - const char *method, - const char *buf, - const jsmntok_t *result, - void *unused) +static struct command_result *xpay_layer_created(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + void *unused) { struct xpay *xpay = xpay_of(aux_cmd->plugin); - - /* We use headercount from the backend, in case we're still syncing */ - if (!json_to_u32(buf, json_get_member(buf, result, "headercount"), - &xpay->blockheight)) { - plugin_err(aux_cmd->plugin, "Bad getchaininfo '%.*s'", - json_tok_full_len(result), - json_tok_full(buf, result)); - } + xpay->ready = true; + plugin_log(aux_cmd->plugin, LOG_INFORM, "xpay is ready"); return aux_command_done(aux_cmd); } @@ -2927,6 +2936,7 @@ static struct command_result *getinfo_done(struct command *aux_cmd, { struct xpay *xpay = xpay_of(aux_cmd->plugin); const char *err; + struct out_req *req; err = json_scan(tmpctx, buf, result, "{id:%}", JSON_SCAN(json_to_pubkey, &xpay->local_id)); @@ -2936,7 +2946,35 @@ static struct command_result *getinfo_done(struct command *aux_cmd, json_tok_full(buf, result), err); } - return aux_command_done(aux_cmd); + + req = jsonrpc_request_start(aux_cmd, "askrene-create-layer", + xpay_layer_created, plugin_broken_cb, + "askrene-create-layer"); + json_add_string(req->js, "layer", "xpay"); + json_add_bool(req->js, "persistent", true); + return send_outreq(req); +} + +static struct command_result *getchaininfo_done(struct command *aux_cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + void *unused) +{ + struct xpay *xpay = xpay_of(aux_cmd->plugin); + struct out_req *req; + + /* We use headercount from the backend, in case we're still syncing */ + if (!json_to_u32(buf, json_get_member(buf, result, "headercount"), + &xpay->blockheight)) { + plugin_err(aux_cmd->plugin, "Bad getchaininfo '%.*s'", + json_tok_full_len(result), + json_tok_full(buf, result)); + } + + req = jsonrpc_request_start(aux_cmd, "getinfo", getinfo_done, + plugin_broken_cb, "getinfo"); + return send_outreq(req); } static struct command_result *populate_private_layer(struct command *cmd, @@ -2968,15 +3006,6 @@ static struct command_result *age_layer(struct command *cmd, struct payment *pay return send_outreq(req); } -static struct command_result *xpay_layer_created(struct command *aux_cmd, - const char *method, - const char *buf, - const jsmntok_t *result, - void *unused) -{ - return aux_command_done(aux_cmd); -} - static struct command_result *json_xkeysend(struct command *cmd, const char *buf, const jsmntok_t *params) @@ -3007,6 +3036,8 @@ static struct command_result *json_xkeysend(struct command *cmd, p_opt("extratlvs", param_extra_tlvs, &extra_fields), NULL)) return command_param_failed(); + if (!xpay->ready) + return command_fail(cmd, PLUGIN_ERROR, "xpay is initializing"); randbytes(&preimage, sizeof(preimage)); sha256(&payment_hash, &preimage, sizeof(preimage)); @@ -3100,20 +3131,6 @@ static const char *init(struct command *init_cmd, json_add_u32(req->js, "last_height", 0); send_outreq(req); - req = jsonrpc_request_start(aux_command(init_cmd), "getinfo", - getinfo_done, - plugin_broken_cb, - "getinfo"); - send_outreq(req); - - req = jsonrpc_request_start(aux_command(init_cmd), "askrene-create-layer", - xpay_layer_created, - plugin_broken_cb, - "askrene-create-layer"); - json_add_string(req->js, "layer", "xpay"); - json_add_bool(req->js, "persistent", true); - send_outreq(req); - return NULL; } @@ -3408,6 +3425,7 @@ int main(int argc, char *argv[]) xpay->slow_mode = false; xpay->dev_no_age = false; xpay->user_layers = tal_arr(xpay, const char *, 0); + xpay->ready = false; list_head_init(&xpay->payments); plugin_main(argv, init, take(xpay), PLUGIN_RESTARTABLE, true, NULL, diff --git a/tests/test_xpay.py b/tests/test_xpay.py index 4c4cec3f848a..d0185604edf6 100644 --- a/tests/test_xpay.py +++ b/tests/test_xpay.py @@ -259,6 +259,8 @@ def test_xpay_fake_channeld(node_factory, bitcoind, chainparams, slow_mode): {'allow_bad_gossip': True, 'log-level': 'info', }]) + l1.daemon.logsearch_start = 0 + l1.daemon.wait_for_log('xpay is ready') # l1 needs to know l2's shaseed for the channel so it can make revocations hsmfile = os.path.join(l2.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")