diff --git a/tests/test_pay.py b/tests/test_pay.py index f2626f30a..a72a1cfa3 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -2647,3 +2647,76 @@ def test_partial_payment(node_factory, bitcoind, executor): assert pay['status'] == 'complete' assert pay['number_of_parts'] == 2 assert pay['amount_sent_msat'] == Millisatoshi(1002) + + +@unittest.skipIf(not EXPERIMENTAL_FEATURES, "needs partid support") +def test_partial_payment_timeout(node_factory, bitcoind): + l1, l2 = node_factory.line_graph(2) + + inv = l2.rpc.invoice(1000, 'inv', 'inv') + paysecret = l2.rpc.decodepay(inv['bolt11'])['payment_secret'] + + route = l1.rpc.getroute(l2.info['id'], 500, 1)['route'] + l1.rpc.call('sendpay', [route, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 1]) + + with pytest.raises(RpcError, match=r'WIRE_MPP_TIMEOUT'): + l1.rpc.call('waitsendpay', [inv['payment_hash'], 70 + TIMEOUT // 4, 1]) + + # We can still pay it normally. + l1.rpc.call('sendpay', [route, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 1]) + l1.rpc.call('sendpay', [route, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 2]) + l1.rpc.call('waitsendpay', [inv['payment_hash'], TIMEOUT, 1]) + l1.rpc.call('waitsendpay', [inv['payment_hash'], TIMEOUT, 2]) + + +@unittest.skipIf(not EXPERIMENTAL_FEATURES, "needs partid support") +def test_partial_payment_restart(node_factory, bitcoind): + """Test that we recover a set when we restart""" + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True, + opts=[{}] + + [{'may_reconnect': True}] * 2) + + inv = l3.rpc.invoice(1000, 'inv', 'inv') + paysecret = l3.rpc.decodepay(inv['bolt11'])['payment_secret'] + + route = l1.rpc.getroute(l3.info['id'], 500, 1)['route'] + + l1.rpc.call('sendpay', [route, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 1]) + + wait_for(lambda: [f['status'] for f in l2.rpc.listforwards()['forwards']] == ['offered']) + + # Restart, and make sure it's reconnected to l2. + l3.restart() + print(l2.rpc.listpeers()) + wait_for(lambda: [p['connected'] for p in l2.rpc.listpeers()['peers']] == [True, True]) + + # Pay second part. + l1.rpc.call('sendpay', [route, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 2]) + + l1.rpc.call('waitsendpay', [inv['payment_hash'], TIMEOUT, 1]) + l1.rpc.call('waitsendpay', [inv['payment_hash'], TIMEOUT, 2]) + + +@unittest.skipIf(not DEVELOPER, "needs dev-fail") +@unittest.skipIf(not EXPERIMENTAL_FEATURES, "needs partid support") +def test_partial_payment_htlc_loss(node_factory, bitcoind): + """Test that we discard a set when the HTLC is lost""" + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + inv = l3.rpc.invoice(1000, 'inv', 'inv') + paysecret = l3.rpc.decodepay(inv['bolt11'])['payment_secret'] + + route = l1.rpc.getroute(l3.info['id'], 500, 1)['route'] + + l1.rpc.call('sendpay', [route, inv['payment_hash'], None, 1000, inv['bolt11'], paysecret, 1]) + wait_for(lambda: [f['status'] for f in l2.rpc.listforwards()['forwards']] == ['offered']) + + l2.rpc.dev_fail(l3.info['id']) + + # Since HTLC is missing from commit (dust), it's closed as soon as l2 sees + # it onchain. l3 shouldn't crash though. + bitcoind.generate_block(1, wait_for_mempool=1) + + with pytest.raises(RpcError, + match=r'WIRE_PERMANENT_CHANNEL_FAILURE \(reply from remote\)'): + l1.rpc.call('waitsendpay', [inv['payment_hash'], TIMEOUT, 1])