diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c index b4e2c41102ff..ac3bc4dfdfeb 100644 --- a/plugins/xpay/xpay.c +++ b/plugins/xpay/xpay.c @@ -1631,6 +1631,31 @@ static struct command_result *param_string_array(struct command *cmd, const char return NULL; } +static struct command_result *getchaininfo_success(struct command *cmd, + const char *method, + const char *buffer, + const jsmntok_t *toks, + struct payment *payment) +{ + struct xpay *xpay = xpay_of(cmd->plugin); + u32 headercount; + const char *err; + + err = json_scan(tmpctx, buffer, toks, "{headercount:%}", + JSON_SCAN(json_to_u32, &headercount)); + if (err) + plugin_err(cmd->plugin, "Bad getchaininfo response: %s", err); + + if (headercount > xpay->blockheight) { + plugin_log(cmd->plugin, LOG_DBG, + "Updating payment blockheight from %d (blocks " + "synced) to %d (headercount)", + (int)xpay->blockheight, (int)headercount); + xpay->blockheight = headercount; + } + return populate_private_layer(cmd, payment); +} + static struct command_result * preapproveinvoice_succeed(struct command *cmd, const char *method, @@ -1639,6 +1664,7 @@ preapproveinvoice_succeed(struct command *cmd, struct payment *payment) { struct xpay *xpay = xpay_of(cmd->plugin); + struct out_req *req; /* Now we can conclude `check` command */ if (command_check_only(cmd)) { @@ -1653,7 +1679,11 @@ preapproveinvoice_succeed(struct command *cmd, if (payment->disable_mpp) payment_log(payment, LOG_INFORM, "No MPP support: this is going to be hard to pay"); - return populate_private_layer(cmd, payment); + /* Fetch the header height for we might be out-of-sync. */ + req = jsonrpc_request_start(cmd, "getchaininfo", &getchaininfo_success, + forward_error, payment); + json_add_u32(req->js, "last_height", 0); + return send_outreq(req); } static struct command_result *check_offer_payable(struct command *cmd, diff --git a/tests/test_xpay.py b/tests/test_xpay.py index 71e6e8bc6967..a916c4aa9fb6 100644 --- a/tests/test_xpay.py +++ b/tests/test_xpay.py @@ -1018,3 +1018,37 @@ def test_xpay_bip353(node_factory): node_factory.join_nodes([l2, l1]) l2.rpc.xpay('fake@fake.com', 100) + + +def test_blockheight_mismatch(node_factory, bitcoind): + """Test that we can send a payment even if not caught up with the chain. + + Since CLTV computations are based on headers and not our own sync height, the + recipient should still be happy with the parameters we chose. + + Stolen from pay + """ + + send, direct, recv = node_factory.line_graph(3, wait_for_announce=True) + sync_blockheight(bitcoind, [send, recv]) + + # Pin `send` at the current height. by not returning the next + # block. This error is special-cased not to count as the + # backend failing since it is used to poll for the next block. + def mock_getblock(req): + return { + "id": req["id"], + "error": {"code": -8, "message": "Block height out of range"}, + } + + send.daemon.rpcproxy.mock_rpc("getblock", mock_getblock) + bitcoind.generate_block(100) + + sync_blockheight(bitcoind, [recv]) + + inv = recv.rpc.invoice(42, "lbl", "desc")["bolt11"] + send.rpc.xpay(inv) + + # Test a direct payment as well. + inv = direct.rpc.invoice(13, "lbl", "desc")["bolt11"] + send.rpc.xpay(inv)