ttests: test payreq status after getting paid via LN - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit a125cd5392d8aa873b15663e59b5d3c269977afc DIR parent f662859e5c7745eccaa7af195c9915035e38eb94 HTML Author: SomberNight <somber.night@protonmail.com> Date: Mon, 8 Mar 2021 22:18:06 +0100 ttests: test payreq status after getting paid via LN The test failures corresponding to single-part (non-MPP) payments expose a bug. see https://github.com/spesmilo/electrum/blob/196b4c00a35b288f28885c0ceb8afdd5ffbd9a0f/electrum/lnpeer.py#L1538-L1539 `lnworker.add_received_htlc` is not called for single-part payments... Diffstat: M electrum/lnpeer.py | 2 +- M electrum/lnworker.py | 5 ++++- M electrum/tests/test_lnpeer.py | 50 +++++++++++++++++++++---------- 3 files changed, 39 insertions(+), 18 deletions(-) --- DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py t@@ -1473,7 +1473,7 @@ class Peer(Logger): chan: Channel, htlc: UpdateAddHtlc, processed_onion: ProcessedOnionPacket, - is_trampoline: bool = False) -> Tuple[Optional[bytes], Optional[bytes]]: + is_trampoline: bool = False) -> Tuple[Optional[bytes], Optional[OnionPacket]]: """As a final recipient of an HTLC, decide if we should fulfill it. Return (preimage, trampoline_onion_packet) with at most a single element not None DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -577,9 +577,9 @@ class LNWallet(LNWorker): lnwatcher: Optional['LNWalletWatcher'] def __init__(self, wallet: 'Abstract_Wallet', xprv): - Logger.__init__(self) self.wallet = wallet self.db = wallet.db + Logger.__init__(self) LNWorker.__init__(self, xprv, LNWALLET_FEATURES) self.config = wallet.config self.lnwatcher = None t@@ -621,6 +621,9 @@ class LNWallet(LNWorker): def get_channel_by_id(self, channel_id: bytes) -> Optional[Channel]: return self._channels.get(channel_id, None) + def diagnostic_name(self): + return self.wallet.diagnostic_name() + @ignore_exceptions @log_exceptions async def sync_with_local_watchtower(self): DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py t@@ -30,10 +30,11 @@ from electrum.channel_db import ChannelDB from electrum.lnworker import LNWallet, NoPathFound from electrum.lnmsg import encode_msg, decode_msg from electrum.logging import console_stderr_handler, Logger -from electrum.lnworker import PaymentInfo, RECEIVED, PR_UNPAID +from electrum.lnworker import PaymentInfo, RECEIVED from electrum.lnonion import OnionFailureCode from electrum.lnutil import ChannelBlackList, derive_payment_secret_from_payment_preimage from electrum.lnutil import LOCAL, REMOTE +from electrum.invoices import PR_PAID, PR_UNPAID from .test_lnchannel import create_test_channels from .test_bitcoin import needs_test_with_all_chacha20_implementations t@@ -112,7 +113,8 @@ class MockWallet: class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): - def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue): + def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue, name): + self.name = name Logger.__init__(self) NetworkRetryManager.__init__(self, max_retry_delay_normal=1, init_retry_delay_normal=1) self.node_keypair = local_keypair t@@ -173,6 +175,9 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): def save_channel(self, chan): print("Ignoring channel save") + def diagnostic_name(self): + return self.name + get_payments = LNWallet.get_payments get_payment_info = LNWallet.get_payment_info save_payment_info = LNWallet.save_payment_info t@@ -298,8 +303,8 @@ class TestPeer(ElectrumTestCase): bob_channel.node_id = k1.pubkey t1, t2 = transport_pair(k1, k2, alice_channel.name, bob_channel.name) q1, q2 = asyncio.Queue(), asyncio.Queue() - w1 = MockLNWallet(local_keypair=k1, chans=[alice_channel], tx_queue=q1) - w2 = MockLNWallet(local_keypair=k2, chans=[bob_channel], tx_queue=q2) + w1 = MockLNWallet(local_keypair=k1, chans=[alice_channel], tx_queue=q1, name=bob_channel.name) + w2 = MockLNWallet(local_keypair=k2, chans=[bob_channel], tx_queue=q2, name=alice_channel.name) p1 = Peer(w1, k2.pubkey, t1) p2 = Peer(w2, k1.pubkey, t2) w1._peers[p1.pubkey] = p1 t@@ -324,10 +329,10 @@ class TestPeer(ElectrumTestCase): trans_bd, trans_db = transport_pair(key_b, key_d, chan_bd.name, chan_db.name) trans_cd, trans_dc = transport_pair(key_c, key_d, chan_cd.name, chan_dc.name) txq_a, txq_b, txq_c, txq_d = [asyncio.Queue() for i in range(4)] - w_a = MockLNWallet(local_keypair=key_a, chans=[chan_ab, chan_ac], tx_queue=txq_a) - w_b = MockLNWallet(local_keypair=key_b, chans=[chan_ba, chan_bd], tx_queue=txq_b) - w_c = MockLNWallet(local_keypair=key_c, chans=[chan_ca, chan_cd], tx_queue=txq_c) - w_d = MockLNWallet(local_keypair=key_d, chans=[chan_db, chan_dc], tx_queue=txq_d) + w_a = MockLNWallet(local_keypair=key_a, chans=[chan_ab, chan_ac], tx_queue=txq_a, name="alice") + w_b = MockLNWallet(local_keypair=key_b, chans=[chan_ba, chan_bd], tx_queue=txq_b, name="bob") + w_c = MockLNWallet(local_keypair=key_c, chans=[chan_ca, chan_cd], tx_queue=txq_c, name="carol") + w_d = MockLNWallet(local_keypair=key_d, chans=[chan_db, chan_dc], tx_queue=txq_d, name="dave") peer_ab = Peer(w_a, key_b.pubkey, trans_ab) peer_ac = Peer(w_a, key_c.pubkey, trans_ac) peer_ba = Peer(w_b, key_a.pubkey, trans_ba) t@@ -489,11 +494,14 @@ class TestPeer(ElectrumTestCase): @needs_test_with_all_chacha20_implementations def test_payment(self): + """Alice pays Bob a single HTLC via direct channel.""" alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) - async def pay(pay_req): + async def pay(lnaddr, pay_req): + self.assertEqual(PR_UNPAID, w2.get_payment_status(lnaddr.paymenthash)) result, log = await w1.pay_invoice(pay_req) self.assertTrue(result) + self.assertEqual(PR_PAID, w2.get_payment_status(lnaddr.paymenthash)) raise PaymentDone() async def f(): async with TaskGroup() as group: t@@ -503,7 +511,9 @@ class TestPeer(ElectrumTestCase): await group.spawn(p2.htlc_switch()) await asyncio.sleep(0.01) lnaddr, pay_req = await self.prepare_invoice(w2) - await group.spawn(pay(pay_req)) + invoice_features = lnaddr.get_features() + self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT)) + await group.spawn(pay(lnaddr, pay_req)) with self.assertRaises(PaymentDone): run(f()) t@@ -614,9 +624,11 @@ class TestPeer(ElectrumTestCase): def test_payment_multihop(self): graph = self.prepare_chans_and_peers_in_square() peers = graph.all_peers() - async def pay(pay_req): + async def pay(lnaddr, pay_req): + self.assertEqual(PR_UNPAID, graph.w_d.get_payment_status(lnaddr.paymenthash)) result, log = await graph.w_a.pay_invoice(pay_req) self.assertTrue(result) + self.assertEqual(PR_PAID, graph.w_d.get_payment_status(lnaddr.paymenthash)) raise PaymentDone() async def f(): async with TaskGroup() as group: t@@ -625,7 +637,7 @@ class TestPeer(ElectrumTestCase): await group.spawn(peer.htlc_switch()) await asyncio.sleep(0.2) lnaddr, pay_req = await self.prepare_invoice(graph.w_d, include_routing_hints=True) - await group.spawn(pay(pay_req)) + await group.spawn(pay(lnaddr, pay_req)) with self.assertRaises(PaymentDone): run(f()) t@@ -679,9 +691,11 @@ class TestPeer(ElectrumTestCase): graph.w_b.network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) graph.w_c.network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) peers = graph.all_peers() - async def pay(pay_req): + async def pay(lnaddr, pay_req): + self.assertEqual(PR_UNPAID, graph.w_d.get_payment_status(lnaddr.paymenthash)) result, log = await graph.w_a.pay_invoice(pay_req) self.assertFalse(result) + self.assertEqual(PR_UNPAID, graph.w_d.get_payment_status(lnaddr.paymenthash)) self.assertEqual(OnionFailureCode.TEMPORARY_NODE_FAILURE, log[0].failure_msg.code) raise PaymentDone() async def f(): t@@ -691,7 +705,7 @@ class TestPeer(ElectrumTestCase): await group.spawn(peer.htlc_switch()) await asyncio.sleep(0.2) lnaddr, pay_req = await self.prepare_invoice(graph.w_d, include_routing_hints=True) - await group.spawn(pay(pay_req)) + await group.spawn(pay(lnaddr, pay_req)) with self.assertRaises(PaymentDone): run(f()) t@@ -702,12 +716,14 @@ class TestPeer(ElectrumTestCase): graph = self.prepare_chans_and_peers_in_square() graph.w_c.network.config.set_key('test_fail_htlcs_with_temp_node_failure', True) peers = graph.all_peers() - async def pay(pay_req): + async def pay(lnaddr, pay_req): self.assertEqual(500000000000, graph.chan_ab.balance(LOCAL)) self.assertEqual(500000000000, graph.chan_db.balance(LOCAL)) + self.assertEqual(PR_UNPAID, graph.w_d.get_payment_status(lnaddr.paymenthash)) result, log = await graph.w_a.pay_invoice(pay_req, attempts=2) self.assertEqual(2, len(log)) self.assertTrue(result) + self.assertEqual(PR_PAID, graph.w_d.get_payment_status(lnaddr.paymenthash)) self.assertEqual([graph.chan_ac.short_channel_id, graph.chan_cd.short_channel_id], [edge.short_channel_id for edge in log[0].route]) self.assertEqual([graph.chan_ab.short_channel_id, graph.chan_bd.short_channel_id], t@@ -726,7 +742,7 @@ class TestPeer(ElectrumTestCase): lnaddr, pay_req = await self.prepare_invoice(graph.w_d, include_routing_hints=True) invoice_features = lnaddr.get_features() self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT)) - await group.spawn(pay(pay_req)) + await group.spawn(pay(lnaddr, pay_req)) with self.assertRaises(PaymentDone): run(f()) t@@ -737,8 +753,10 @@ class TestPeer(ElectrumTestCase): peers = graph.all_peers() async def pay(): lnaddr, pay_req = await self.prepare_invoice(graph.w_d, include_routing_hints=True, amount_msat=amount_to_pay) + self.assertEqual(PR_UNPAID, graph.w_d.get_payment_status(lnaddr.paymenthash)) result, log = await graph.w_a.pay_invoice(pay_req, attempts=attempts) if result: + self.assertEqual(PR_PAID, graph.w_d.get_payment_status(lnaddr.paymenthash)) raise PaymentDone() else: raise NoPathFound()