Skip to content

Commit f168cb5

Browse files
rustyrussellendothermicdev
authored andcommitted
xpay: add xpay-slow-mode to force waiting for all parts before returning.
This was requested by Michael of Boltz; it's mainly useful if you plan to try failed payments on a *different* node. In that case, there's a theoretical possibility that slow parts of this payment could combine with that from a different node and overpay. We don't allow this from the same node, already. Changelog-Added: xpay: `xpay-slow-mode` makes xpay wait for all parts of a payment to complete before returning success or failure. Signed-off-by: Rusty Russell <[email protected]>
1 parent db1e26e commit f168cb5

File tree

5 files changed

+94
-5
lines changed

5 files changed

+94
-5
lines changed

contrib/msggen/msggen/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21207,6 +21207,12 @@
2120721207
"source": "default",
2120821208
"plugin": "/root/lightning/plugins/cln-xpay",
2120921209
"dynamic": true
21210+
},
21211+
"xpay-slow-mode": {
21212+
"value_bool": false,
21213+
"source": "default",
21214+
"plugin": "/root/lightning/plugins/cln-xpay",
21215+
"dynamic": true
2121021216
}
2121121217
}
2121221218
}

doc/lightningd-config.5.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,10 @@ command, so they invoices can also be paid onchain.
546546

547547
Setting this makes `xpay` intercept simply `pay` commands (default `false`).
548548

549+
* **xpay-slow-mode**=*BOOL* [plugin `xpay`, *dynamic*]
550+
551+
Setting this makes `xpay` wait until all parts have failed/succeeded before returning. Usually this is unnecessary, as xpay will return on the first success (we have the preimage, if they don't take all the parts that's their problem) or failure (the destination could succeed another part, but it would mean it was only partially paid). The default is `false`.
552+
549553
### Networking options
550554

551555
Note that for simple setups, the implicit *autolisten* option does the

doc/schemas/lightning-listconfigs.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2989,6 +2989,12 @@
29892989
"source": "default",
29902990
"plugin": "/root/lightning/plugins/cln-xpay",
29912991
"dynamic": true
2992+
},
2993+
"xpay-slow-mode": {
2994+
"value_bool": false,
2995+
"source": "default",
2996+
"plugin": "/root/lightning/plugins/cln-xpay",
2997+
"dynamic": true
29922998
}
29932999
}
29943000
}

plugins/xpay/xpay.c

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ struct xpay {
3636
u32 blockheight;
3737
/* Do we take over "pay" commands? */
3838
bool take_over_pay;
39+
/* Are we to wait for all parts to complete before returning? */
40+
bool slow_mode;
3941
};
4042

4143
static struct xpay *xpay_of(struct plugin *plugin)
@@ -353,13 +355,26 @@ static struct amount_msat total_sent(const struct payment *payment)
353355
return total;
354356
}
355357

358+
/* Should we finish command now? */
359+
static bool should_finish_command(const struct payment *payment)
360+
{
361+
const struct xpay *xpay = xpay_of(payment->plugin);
362+
363+
if (!xpay->slow_mode)
364+
return true;
365+
366+
/* In slow mode, only finish when no remaining attempts
367+
* (caller has already moved it to past_attempts). */
368+
return list_empty(&payment->current_attempts);
369+
}
370+
356371
static void payment_succeeded(struct payment *payment,
357372
const struct preimage *preimage)
358373
{
359374
struct json_stream *js;
360375

361376
/* Only succeed once */
362-
if (payment->cmd) {
377+
if (payment->cmd && should_finish_command(payment)) {
363378
js = jsonrpc_stream_success(payment->cmd);
364379
json_add_preimage(js, "payment_preimage", preimage);
365380
json_add_amount_msat(js, "amount_msat", payment->amount);
@@ -388,6 +403,18 @@ static void payment_failed(struct command *aux_cmd,
388403
...)
389404
PRINTF_FMT(4,5);
390405

406+
/* Returns NULL if no past attempts succeeded, otherwise the preimage */
407+
static const struct preimage *
408+
any_attempts_succeeded(const struct payment *payment)
409+
{
410+
struct attempt *attempt;
411+
list_for_each(&payment->past_attempts, attempt, list) {
412+
if (attempt->preimage)
413+
return attempt->preimage;
414+
}
415+
return NULL;
416+
}
417+
391418
static void payment_failed(struct command *aux_cmd,
392419
struct payment *payment,
393420
enum jsonrpc_errcode code,
@@ -402,9 +429,18 @@ static void payment_failed(struct command *aux_cmd,
402429
va_end(args);
403430

404431
/* Only fail once */
405-
if (payment->cmd) {
406-
was_pending(command_fail(payment->cmd, code, "%s", msg));
407-
payment->cmd = NULL;
432+
if (payment->cmd && should_finish_command(payment)) {
433+
const struct preimage *preimage;
434+
435+
/* Corner case: in slow_mode, an earlier one could have
436+
* theoretically succeeded. */
437+
preimage = any_attempts_succeeded(payment);
438+
if (preimage)
439+
payment_succeeded(payment, preimage);
440+
else {
441+
was_pending(command_fail(payment->cmd, code, "%s", msg));
442+
payment->cmd = NULL;
443+
}
408444
}
409445

410446
/* If no commands outstanding, we can now clean up */
@@ -2057,6 +2093,7 @@ int main(int argc, char *argv[])
20572093
setup_locale();
20582094
xpay = tal(NULL, struct xpay);
20592095
xpay->take_over_pay = false;
2096+
xpay->slow_mode = false;
20602097
plugin_main(argv, init, take(xpay),
20612098
PLUGIN_RESTARTABLE, true, NULL,
20622099
commands, ARRAY_SIZE(commands),
@@ -2066,5 +2103,8 @@ int main(int argc, char *argv[])
20662103
plugin_option_dynamic("xpay-handle-pay", "bool",
20672104
"Make xpay take over pay commands it can handle.",
20682105
bool_option, bool_jsonfmt, &xpay->take_over_pay),
2106+
plugin_option_dynamic("xpay-slow-mode", "bool",
2107+
"Wait until all parts have completed before returning success or failure",
2108+
bool_option, bool_jsonfmt, &xpay->slow_mode),
20692109
NULL);
20702110
}

tests/test_xpay.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ def test_xpay_selfpay(node_factory):
212212

213213
@pytest.mark.slow_test
214214
@unittest.skipIf(TEST_NETWORK != 'regtest', '29-way split for node 17 is too dusty on elements')
215-
def test_xpay_fake_channeld(node_factory, bitcoind, chainparams):
215+
@pytest.mark.parametrize("slow_mode", [False, True])
216+
def test_xpay_fake_channeld(node_factory, bitcoind, chainparams, slow_mode):
216217
outfile = tempfile.NamedTemporaryFile(prefix='gossip-store-')
217218
nodeids = subprocess.check_output(['devtools/gossmap-compress',
218219
'decompress',
@@ -239,6 +240,8 @@ def test_xpay_fake_channeld(node_factory, bitcoind, chainparams):
239240
shaseed = subprocess.check_output(["tools/hsmtool", "dumpcommitments", l1.info['id'], "1", "0", hsmfile]).decode('utf-8').strip().partition(": ")[2]
240241
l1.rpc.dev_peer_shachain(l2.info['id'], shaseed)
241242

243+
# Toggle whether we wait for all the parts to finish.
244+
l1.rpc.setconfig('xpay-slow-mode', slow_mode)
242245
failed_parts = []
243246
for n in range(0, 100):
244247
if n in (62, 76, 80, 97):
@@ -677,3 +680,33 @@ def test_xpay_bolt12_no_mpp(node_factory, chainparams):
677680
assert ret['successful_parts'] == 2
678681
assert ret['amount_msat'] == AMOUNT
679682
assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1
683+
684+
685+
def test_xpay_slow_mode(node_factory, bitcoind):
686+
# l1 -> l2 -> l3 -> l5
687+
# \-> l4 -/^
688+
l1, l2, l3, l4, l5 = node_factory.get_nodes(5, opts=[{'xpay-slow-mode': True},
689+
{}, {}, {}, {}])
690+
node_factory.join_nodes([l2, l3, l5])
691+
node_factory.join_nodes([l2, l4, l5])
692+
693+
# Make sure l1 can see all paths.
694+
node_factory.join_nodes([l1, l2])
695+
bitcoind.generate_block(5)
696+
wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 10)
697+
698+
# First try an MPP which fails
699+
inv = l5.rpc.invoice(500000000, 'test_xpay_slow_mode_fail', 'test_xpay_slow_mode_fail', preimage='01' * 32)['bolt11']
700+
l5.rpc.delinvoice('test_xpay_slow_mode_fail', status='unpaid')
701+
702+
with pytest.raises(RpcError, match=r"Destination said it doesn't know invoice: incorrect_or_unknown_payment_details"):
703+
l1.rpc.xpay(inv)
704+
705+
# Now a successful one
706+
inv = l5.rpc.invoice(500000000, 'test_xpay_slow_mode', 'test_xpay_slow_mode', preimage='00' * 32)['bolt11']
707+
708+
assert l1.rpc.xpay(inv) == {'payment_preimage': '00' * 32,
709+
'amount_msat': 500000000,
710+
'amount_sent_msat': 500010002,
711+
'failed_parts': 0,
712+
'successful_parts': 2}

0 commit comments

Comments
 (0)