Skip to content

Commit 2fe9d1a

Browse files
committed
tests: lnpeer: test_mpp_cleanup_after_expiry
1. Alice sends two HTLCs to Bob, not reaching total_msat, and eventually they MPP_TIMEOUT 2. Bob fails both HTLCs 3. Alice then retries and sends HTLCs again to Bob, for the same RHASH, this time reaching total_msat, and the payment succeeds Test that the sets are properly cleaned up after MPP_TIMEOUT and the sender gets a second chance to pay the same invoice.
1 parent d1862e9 commit 2fe9d1a

File tree

1 file changed

+100
-2
lines changed

1 file changed

+100
-2
lines changed

tests/test_lnpeer.py

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@
4040
from electrum.logging import console_stderr_handler, Logger
4141
from electrum.lnworker import PaymentInfo, RECEIVED
4242
from electrum.lnonion import OnionFailureCode, OnionRoutingFailure
43-
from electrum.lnutil import UpdateAddHtlc
44-
from electrum.lnutil import LOCAL, REMOTE
43+
from electrum.lnutil import LOCAL, REMOTE, UpdateAddHtlc, RecvMPPResolution
4544
from electrum.invoices import PR_PAID, PR_UNPAID, Invoice, LN_EXPIRY_NEVER
4645
from electrum.interface import GracefulDisconnect
4746
from electrum.simple_config import SimpleConfig
@@ -1420,6 +1419,105 @@ async def on_htlc_failed(*args):
14201419
util.unregister_callback(on_htlc_fulfilled)
14211420
util.unregister_callback(on_htlc_failed)
14221421

1422+
async def test_mpp_cleanup_after_expiry(self):
1423+
"""
1424+
1. Alice sends two HTLCs to Bob, not reaching total_msat, and eventually they MPP_TIMEOUT
1425+
2. Bob fails both HTLCs
1426+
3. Alice then retries and sends HTLCs again to Bob, for the same RHASH,
1427+
this time reaching total_msat, and the payment succeeds
1428+
1429+
Test that the sets are properly cleaned up after MPP_TIMEOUT
1430+
and the sender gets a second chance to pay the same invoice.
1431+
"""
1432+
async def run_test(test_trampoline: bool):
1433+
alice_channel, bob_channel = create_test_channels()
1434+
alice_peer, bob_peer, alice_wallet, bob_wallet, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel)
1435+
lnaddr1, pay_req1 = self.prepare_invoice(bob_wallet, amount_msat=10_000)
1436+
1437+
if test_trampoline:
1438+
await self._activate_trampoline(alice_wallet)
1439+
# declare bob as trampoline node
1440+
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {
1441+
'bob': LNPeerAddr(host="127.0.0.1", port=9735, pubkey=bob_wallet.node_keypair.pubkey),
1442+
}
1443+
1444+
async def _test():
1445+
route = (await alice_wallet.create_routes_from_invoice(amount_msat=10_000, decoded_invoice=lnaddr1))[0][0].route
1446+
assert len(bob_wallet.received_mpp_htlcs) == 0
1447+
# now alice sends two small htlcs, so the set stays incomplete
1448+
alice_peer.pay( # htlc 1
1449+
route=route,
1450+
chan=alice_channel,
1451+
amount_msat=lnaddr1.get_amount_msat() // 4,
1452+
total_msat=lnaddr1.get_amount_msat(),
1453+
payment_hash=lnaddr1.paymenthash,
1454+
min_final_cltv_delta=400,
1455+
payment_secret=lnaddr1.payment_secret,
1456+
)
1457+
alice_peer.pay( # htlc 2
1458+
route=route,
1459+
chan=alice_channel,
1460+
amount_msat=lnaddr1.get_amount_msat() // 4,
1461+
total_msat=lnaddr1.get_amount_msat(),
1462+
payment_hash=lnaddr1.paymenthash,
1463+
min_final_cltv_delta=400,
1464+
payment_secret=lnaddr1.payment_secret,
1465+
)
1466+
await asyncio.sleep(bob_wallet.MPP_EXPIRY // 2) # give bob time to receive the htlc
1467+
bob_payment_key = bob_wallet._get_payment_key(lnaddr1.paymenthash).hex()
1468+
assert bob_wallet.received_mpp_htlcs[bob_payment_key].resolution == RecvMPPResolution.WAITING
1469+
assert len(bob_wallet.received_mpp_htlcs[bob_payment_key].htlcs) == 2
1470+
# now wait until bob expires the mpp (set)
1471+
await asyncio.wait_for(alice_htlc_resolved.wait(), bob_wallet.MPP_EXPIRY * 3) # this can take some time, esp. on CI
1472+
# check that bob failed the htlc
1473+
assert nhtlc_success == 0 and nhtlc_failed == 2
1474+
# check that bob deleted the mpp set as it should be expired and resolved now
1475+
assert bob_payment_key not in bob_wallet.received_mpp_htlcs
1476+
alice_wallet._paysessions.clear()
1477+
assert alice_wallet.get_preimage(lnaddr1.paymenthash) is None # bob didn't preimage
1478+
# now try to pay again, this time the full amount
1479+
result, log = await alice_wallet.pay_invoice(pay_req1)
1480+
assert result is True
1481+
assert alice_wallet.get_preimage(lnaddr1.paymenthash) is not None # bob revealed preimage
1482+
assert len(bob_wallet.received_mpp_htlcs) == 0 # bob should also clean up a successful set
1483+
raise SuccessfulTest()
1484+
1485+
async def f():
1486+
async with OldTaskGroup() as group:
1487+
await group.spawn(alice_peer._message_loop())
1488+
await group.spawn(alice_peer.htlc_switch())
1489+
await group.spawn(bob_peer._message_loop())
1490+
await group.spawn(bob_peer.htlc_switch())
1491+
await asyncio.sleep(0.01)
1492+
await group.spawn(_test())
1493+
1494+
alice_htlc_resolved = asyncio.Event()
1495+
nhtlc_success = 0
1496+
nhtlc_failed = 0
1497+
async def on_sender_htlc_fulfilled(*args):
1498+
alice_htlc_resolved.set()
1499+
alice_htlc_resolved.clear()
1500+
nonlocal nhtlc_success
1501+
nhtlc_success += 1
1502+
async def on_sender_htlc_failed(*args):
1503+
alice_htlc_resolved.set()
1504+
alice_htlc_resolved.clear()
1505+
nonlocal nhtlc_failed
1506+
nhtlc_failed += 1
1507+
util.register_callback(on_sender_htlc_fulfilled, ["htlc_fulfilled"])
1508+
util.register_callback(on_sender_htlc_failed, ["htlc_failed"])
1509+
1510+
try:
1511+
with self.assertRaises(SuccessfulTest):
1512+
await f()
1513+
finally:
1514+
util.unregister_callback(on_sender_htlc_fulfilled)
1515+
util.unregister_callback(on_sender_htlc_failed)
1516+
1517+
for use_trampoline in [True, False]:
1518+
self.logger.debug(f"test_mpp_cleanup_after_expiry: {use_trampoline=}")
1519+
await run_test(use_trampoline)
1520+
14231521
async def test_legacy_shutdown_low(self):
14241522
await self._test_shutdown(alice_fee=100, bob_fee=150)
14251523

0 commit comments

Comments
 (0)