Skip to content

Commit 43e59a6

Browse files
rustyrussellendothermicdev
authored andcommitted
pytest: test xpay gracefully handles failure after success.
We can create this scenario by having one path force close. We take the opportunity to log this, even in non-slow-mode, since it's interesting (not our bug, but someone just lost money!). Signed-off-by: Rusty Russell <[email protected]>
1 parent f168cb5 commit 43e59a6

File tree

2 files changed

+97
-9
lines changed

2 files changed

+97
-9
lines changed

plugins/xpay/xpay.c

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,21 @@ static const char *describe_scidd(struct attempt *attempt, size_t index)
519519
return fmt_short_channel_id_dir(tmpctx, &scidd);
520520
}
521521

522+
/* How much did previous successes deliver? */
523+
static struct amount_msat total_delivered(const struct payment *payment)
524+
{
525+
struct amount_msat sum = AMOUNT_MSAT(0);
526+
struct attempt *attempt;
527+
528+
list_for_each(&payment->past_attempts, attempt, list) {
529+
if (!attempt->preimage)
530+
continue;
531+
if (!amount_msat_accumulate(&sum, attempt->delivers))
532+
abort();
533+
}
534+
return sum;
535+
}
536+
522537
static void update_knowledge_from_error(struct command *aux_cmd,
523538
const char *buf,
524539
const jsmntok_t *error,
@@ -531,7 +546,7 @@ static void update_knowledge_from_error(struct command *aux_cmd,
531546
int index;
532547
enum onion_wire failcode;
533548
bool from_final;
534-
const char *failcode_name, *errmsg;
549+
const char *failcode_name, *errmsg, *description;
535550
enum jsonrpc_errcode ecode;
536551

537552
tok = json_get_member(buf, error, "code");
@@ -574,6 +589,7 @@ static void update_knowledge_from_error(struct command *aux_cmd,
574589
/* Garbled? Blame random hop. */
575590
if (!replymsg) {
576591
index = pseudorand(tal_count(attempt->hops));
592+
description = "Garbled error message";
577593
add_result_summary(attempt, LOG_UNUSUAL,
578594
"We got a garbled error message, and chose to (randomly) to disable %s for this payment",
579595
describe_scidd(attempt, index));
@@ -609,13 +625,14 @@ static void update_knowledge_from_error(struct command *aux_cmd,
609625
} else
610626
errmsg = failcode_name;
611627

612-
attempt_debug(attempt,
613-
"Error %s for path %s, from %s",
614-
errmsg,
615-
fmt_path(tmpctx, attempt),
616-
from_final ? "destination"
617-
: index == 0 ? "local node"
618-
: fmt_pubkey(tmpctx, &attempt->hops[index-1].next_node));
628+
description = tal_fmt(tmpctx,
629+
"Error %s for path %s, from %s",
630+
errmsg,
631+
fmt_path(tmpctx, attempt),
632+
from_final ? "destination"
633+
: index == 0 ? "local node"
634+
: fmt_pubkey(tmpctx, &attempt->hops[index-1].next_node));
635+
attempt_debug(attempt, "%s", description);
619636

620637
/* Final node sent an error */
621638
if (from_final) {
@@ -735,7 +752,7 @@ static void update_knowledge_from_error(struct command *aux_cmd,
735752
attempt->hops[index].scidd);
736753
json_add_bool(req->js, "enabled", false);
737754
send_payment_req(aux_cmd, attempt->payment, req);
738-
return;
755+
goto check_previous_success;
739756

740757
channel_capacity:
741758
req = payment_ignored_req(aux_cmd, attempt, "askrene-inform-channel");
@@ -747,6 +764,21 @@ static void update_knowledge_from_error(struct command *aux_cmd,
747764
json_add_amount_msat(req->js, "amount_msat", attempt->hops[index].amount_out);
748765
json_add_string(req->js, "inform", "constrained");
749766
send_payment_req(aux_cmd, attempt->payment, req);
767+
768+
check_previous_success:
769+
/* If they give us the preimage but we didn't succeed in giving them
770+
* all the money, that's a win for us. But either the destination is
771+
* buggy, or someone along the way lost money! */
772+
if (any_attempts_succeeded(attempt->payment)) {
773+
payment_log(attempt->payment, LOG_UNUSUAL,
774+
"Destination accepted partial payment,"
775+
" failed a part (%s), but accepted only %s of %s."
776+
" Winning?!",
777+
description,
778+
fmt_amount_msat(tmpctx,
779+
total_delivered(attempt->payment)),
780+
fmt_amount_msat(tmpctx, attempt->payment->amount));
781+
}
750782
}
751783

752784
static struct command_result *unreserve_path(struct command *aux_cmd,

tests/test_xpay.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import os
1010
import pytest
11+
import re
1112
import subprocess
1213
import sys
1314
from hashlib import sha256
@@ -710,3 +711,58 @@ def test_xpay_slow_mode(node_factory, bitcoind):
710711
'amount_sent_msat': 500010002,
711712
'failed_parts': 0,
712713
'successful_parts': 2}
714+
715+
716+
@pytest.mark.parametrize("slow_mode", [False, True])
717+
def test_fail_after_success(node_factory, bitcoind, executor, slow_mode):
718+
# l1 -> l2 -> l3 -> l5
719+
# \-> l4 -/^
720+
l1, l2, l3, l4, l5 = node_factory.get_nodes(5, opts=[{'xpay-slow-mode': slow_mode},
721+
{},
722+
{},
723+
{'disconnect': ['-WIRE_UPDATE_FULFILL_HTLC']},
724+
{}])
725+
node_factory.join_nodes([l2, l3, l5])
726+
node_factory.join_nodes([l2, l4, l5])
727+
728+
# Make sure l1 can see all paths.
729+
node_factory.join_nodes([l1, l2])
730+
bitcoind.generate_block(5)
731+
wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 10)
732+
733+
inv = l5.rpc.invoice(500000000, 'test_xpay_slow_mode', 'test_xpay_slow_mode', preimage='00' * 32)['bolt11']
734+
fut = executor.submit(l1.rpc.xpay, invstring=inv, retry_for=0)
735+
736+
# Part via l3 is fine. Part via l4 is stuck, so we kill l4 and mine
737+
# blocks to make l2 force close.
738+
l4.daemon.wait_for_log('dev_disconnect: -WIRE_UPDATE_FULFILL_HTLC')
739+
l4.stop()
740+
741+
# Normally, we return as soon as first part succeeds.
742+
if slow_mode is False:
743+
assert fut.result(TIMEOUT) == {'payment_preimage': '00' * 32,
744+
'amount_msat': 500000000,
745+
'amount_sent_msat': 500010002,
746+
'failed_parts': 0,
747+
'successful_parts': 2}
748+
749+
# Time it out, l2 will collect it.
750+
bitcoind.generate_block(13)
751+
l2.daemon.wait_for_log('Peer permanent failure in CHANNELD_NORMAL: Offered HTLC 0 SENT_ADD_ACK_REVOCATION cltv 124 hit deadline')
752+
bitcoind.generate_block(3, wait_for_mempool=1)
753+
754+
l1.daemon.wait_for_log(r"UNUSUAL.*Destination accepted partial payment, failed a part \(Error permanent_channel_failure for path ->022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59->0382ce59ebf18be7d84677c2e35f23294b9992ceca95491fcf8a56c6cb2d9de199->032cf15d1ad9c4a08d26eab1918f732d8ef8fdc6abb9640bf3db174372c491304e, from 022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59\)")
755+
# Could be either way around, check both
756+
line = l1.daemon.is_in_log(r"UNUSUAL.*Destination accepted partial payment, failed a part")
757+
assert re.search(r'but accepted only 32000msat of 500000000msat\. Winning\?!', line) or re.search(r'but accepted only 499968000msat of 500000000msat\. Winning\?!', line)
758+
759+
if slow_mode is True:
760+
# Now it succeeds, but notes that it only sent one part!
761+
res = fut.result(TIMEOUT)
762+
# Some variation due to floating point.
763+
assert res['amount_sent_msat'] < 500000000
764+
assert res == {'payment_preimage': '00' * 32,
765+
'amount_msat': 500000000,
766+
'amount_sent_msat': res['amount_sent_msat'],
767+
'failed_parts': 1,
768+
'successful_parts': 1}

0 commit comments

Comments
 (0)