electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 9310e9023efa3015c677790a9d092df915e82755
parent 750d8cfab5bc9b263d9f94b3b59079c8a2cf941b
Author: SomberNight <somber.night@protonmail.com>
Date:   Tue,  2 Mar 2021 18:35:07 +0100

test_lnpeer: add test: multihop payment that routes around failure

This would have caught the overpayment bug fixed in
https://github.com/spesmilo/electrum/commit/693583edc5010ece804e312626c7c40157613ff1

Diffstat:
Melectrum/lnutil.py | 2+-
Melectrum/lnworker.py | 1+
Melectrum/tests/test_lnpeer.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
3 files changed, 66 insertions(+), 9 deletions(-)

diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -251,7 +251,7 @@ class Outpoint(StoredObject): class HtlcLog(NamedTuple): success: bool - amount_msat: int + amount_msat: int # amount for receiver (e.g. from invoice) route: Optional['LNPaymentRoute'] = None preimage: Optional[bytes] = None error_bytes: Optional[bytes] = None diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -1103,6 +1103,7 @@ class LNWallet(LNWorker): # 3. await a queue htlc_log = await self.sent_htlcs[payment_hash].get() amount_inflight -= htlc_log.amount_msat + assert amount_inflight >= 0, f"amount_inflight={amount_inflight} < 0" log.append(htlc_log) if htlc_log.success: return diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py @@ -32,7 +32,8 @@ 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.lnonion import OnionFailureCode -from electrum.lnutil import ChannelBlackList +from electrum.lnutil import ChannelBlackList, derive_payment_secret_from_payment_preimage +from electrum.lnutil import LOCAL, REMOTE from .test_lnchannel import create_test_channels from .test_bitcoin import needs_test_with_all_chacha20_implementations @@ -125,6 +126,8 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): self.features = LnFeatures(0) self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT self.features |= LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT + self.features |= LnFeatures.VAR_ONION_OPT + self.features |= LnFeatures.PAYMENT_SECRET_OPT self.pending_payments = defaultdict(asyncio.Future) for chan in chans: chan.lnworker = self @@ -235,11 +238,11 @@ def transport_pair(k1, k2, name1, name2): class SquareGraph(NamedTuple): - # A - # / \ - # B C - # \ / - # D + # A + # high fee / \ low fee + # B C + # high fee \ / low fee + # D w_a: MockLNWallet w_b: MockLNWallet w_c: MockLNWallet @@ -340,6 +343,16 @@ class TestPeer(ElectrumTestCase): w_b.network.config.set_key('lightning_forward_payments', True) w_c.network.config.set_key('lightning_forward_payments', True) + # forwarding fees, etc + chan_ab.forwarding_fee_proportional_millionths *= 500 + chan_ab.forwarding_fee_base_msat *= 500 + chan_ba.forwarding_fee_proportional_millionths *= 500 + chan_ba.forwarding_fee_base_msat *= 500 + chan_bd.forwarding_fee_proportional_millionths *= 500 + chan_bd.forwarding_fee_base_msat *= 500 + chan_db.forwarding_fee_proportional_millionths *= 500 + chan_db.forwarding_fee_base_msat *= 500 + # mark_open won't work if state is already OPEN. # so set it to FUNDED for chan in [chan_ab, chan_ac, chan_ba, chan_bd, chan_ca, chan_cd, chan_db, chan_dc]: @@ -393,12 +406,20 @@ class TestPeer(ElectrumTestCase): routing_hints = await w2._calc_routing_hints_for_invoice(amount_msat) else: routing_hints = [] + invoice_features = w2.features.for_invoice() + if invoice_features.supports(LnFeatures.PAYMENT_SECRET_OPT): + payment_secret = derive_payment_secret_from_payment_preimage(payment_preimage) + else: + payment_secret = None lnaddr = LnAddr( paymenthash=RHASH, amount=amount_btc, tags=[('c', lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE), - ('d', 'coffee') - ] + routing_hints) + ('d', 'coffee'), + ('9', invoice_features), + ] + routing_hints, + payment_secret=payment_secret, + ) return lnencode(lnaddr, w2.node_keypair.privkey) def test_reestablish(self): @@ -661,6 +682,41 @@ class TestPeer(ElectrumTestCase): run(f()) @needs_test_with_all_chacha20_implementations + def test_payment_multihop_route_around_failure(self): + # Alice will pay Dave. Alice first tries A->C->D route, due to lower fees, but Carol + # will fail the htlc and get blacklisted. Alice will then try A->B->D and succeed. + 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): + self.assertEqual(500000000000, graph.chan_ab.balance(LOCAL)) + self.assertEqual(500000000000, graph.chan_db.balance(LOCAL)) + result, log = await graph.w_a.pay_invoice(pay_req, attempts=2) + self.assertEqual(2, len(log)) + self.assertTrue(result) + 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], + [edge.short_channel_id for edge in log[1].route]) + self.assertEqual(OnionFailureCode.TEMPORARY_NODE_FAILURE, log[0].failure_msg.code) + self.assertEqual(499899450000, graph.chan_ab.balance(LOCAL)) + await asyncio.sleep(0.2) # wait for COMMITMENT_SIGNED / REVACK msgs to update balance + self.assertEqual(500100000000, graph.chan_db.balance(LOCAL)) + raise PaymentDone() + async def f(): + async with TaskGroup() as group: + for peer in peers: + await group.spawn(peer._message_loop()) + await group.spawn(peer.htlc_switch()) + await asyncio.sleep(0.2) + pay_req = await self.prepare_invoice(graph.w_d, include_routing_hints=True) + invoice_features = LnFeatures(lndecode(pay_req).get_tag('9') or 0) + self.assertFalse(invoice_features.supports(LnFeatures.BASIC_MPP_OPT)) + await group.spawn(pay(pay_req)) + with self.assertRaises(PaymentDone): + run(f()) + + @needs_test_with_all_chacha20_implementations def test_close(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel)