From e206d264c895a2031b631365952948307ee823ff Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 25 Sep 2023 12:27:04 +0200 Subject: [PATCH] trampoline forwarding: use routing hints unit tests: - remove 'drop_dave' flag, replace it by depleted channel - add test for trampoline forwarding using routing hints - lower attempts to 2 --- electrum/lnpeer.py | 7 +++++-- electrum/tests/test_lnpeer.py | 36 +++++++++++++++++------------------ electrum/trampoline.py | 25 ++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index a568ddb9b..1c5ca9141 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -51,6 +51,7 @@ from .lnrouter import fee_for_edge_msat from .json_db import StoredDict from .invoices import PR_PAID from .simple_config import FEE_LN_ETA_TARGET +from .trampoline import decode_routing_info if TYPE_CHECKING: from .lnworker import LNGossip, LNWallet @@ -1702,12 +1703,14 @@ class Peer(Logger): next_trampoline_onion = None invoice_features = payload["invoice_features"]["invoice_features"] invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"] - # TODO use invoice_routing_info + r_tags = decode_routing_info(invoice_routing_info) + self.logger.info(f'r_tags {r_tags}') # TODO legacy mpp payment, use total_msat from trampoline onion else: self.logger.info('forward_trampoline: end-to-end') invoice_features = LnFeatures.BASIC_MPP_OPT next_trampoline_onion = trampoline_onion.next_packet + r_tags = [] except Exception as e: self.logger.exception('') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') @@ -1732,7 +1735,7 @@ class Peer(Logger): payment_secret=payment_secret, amount_to_pay=amt_to_forward, min_cltv_expiry=cltv_from_onion, - r_tags=[], + r_tags=r_tags, invoice_features=invoice_features, fwd_trampoline_onion=next_trampoline_onion, fwd_trampoline_fee=trampoline_fee, diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 3884d9d61..c2f533333 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -1407,23 +1407,24 @@ class TestPeer(ElectrumTestCase): graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) await self._run_mpp(graph, {'mpp_invoice': False}, {'mpp_invoice': True}) - async def _run_trampoline_payment(self, is_legacy, direct, drop_dave=None, test_mpp_consolidation=False): - if drop_dave is None: drop_dave = [] + async def _run_trampoline_payment( + self, *, + is_legacy=False, + direct=False, + test_mpp_consolidation=False, + include_routing_hints=True, # only relevant if is_legacy is True + attempts=2, + ): async def pay(lnaddr, pay_req): self.assertEqual(PR_UNPAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash)) - result, log = await graph.workers['alice'].pay_invoice(pay_req, attempts=10) + result, log = await graph.workers['alice'].pay_invoice(pay_req, attempts=attempts) if result: self.assertEqual(PR_PAID, graph.workers['dave'].get_payment_status(lnaddr.paymenthash)) raise PaymentDone() else: raise NoPathFound() - def do_drop_dave(t): - # this will trigger UNKNOWN_NEXT_PEER - dave_node_id = graph.workers['dave'].node_keypair.pubkey - graph.workers[t].peers.pop(dave_node_id) - async def f(): await self._activate_trampoline(graph.workers['alice']) async with OldTaskGroup() as group: @@ -1432,17 +1433,17 @@ class TestPeer(ElectrumTestCase): await group.spawn(peer.htlc_switch()) for peer in peers: await peer.initialized - lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=True) - for p in drop_dave: - do_drop_dave(p) + lnaddr, pay_req = self.prepare_invoice(graph.workers['dave'], include_routing_hints=include_routing_hints) await group.spawn(pay(lnaddr, pay_req)) graph_definition = self.GRAPH_DEFINITIONS['square_graph'] if not direct: - # deplete channel from alice to carol + # deplete channel from alice to carol and from bob to dave graph_definition['alice']['channels']['carol'] = depleted_channel + graph_definition['bob']['channels']['dave'] = depleted_channel # insert a channel from bob to carol - graph_definition['bob']['channels']['carol'] = high_fee_channel + graph_definition['bob']['channels']['carol'] = low_fee_channel + # now the only route possible is alice -> bob -> carol -> dave if test_mpp_consolidation: # deplete alice to carol so that all htlcs go through bob @@ -1475,7 +1476,9 @@ class TestPeer(ElectrumTestCase): @needs_test_with_all_chacha20_implementations async def test_payment_trampoline_legacy(self): with self.assertRaises(PaymentDone): - await self._run_trampoline_payment(is_legacy=True, direct=False) + await self._run_trampoline_payment(is_legacy=True, direct=False, include_routing_hints=True) + with self.assertRaises(NoPathFound): + await self._run_trampoline_payment(is_legacy=True, direct=False, include_routing_hints=False) @needs_test_with_all_chacha20_implementations async def test_payment_trampoline_e2e_direct(self): @@ -1486,10 +1489,7 @@ class TestPeer(ElectrumTestCase): async def test_payment_trampoline_e2e_indirect(self): # must use two trampolines with self.assertRaises(PaymentDone): - await self._run_trampoline_payment(is_legacy=False, direct=False, drop_dave=['bob']) - # both trampolines drop dave - with self.assertRaises(NoPathFound): - await self._run_trampoline_payment(is_legacy=False, direct=False, drop_dave=['bob', 'carol']) + await self._run_trampoline_payment(is_legacy=False, direct=False) @needs_test_with_all_chacha20_implementations async def test_payment_multipart_trampoline_e2e(self): diff --git a/electrum/trampoline.py b/electrum/trampoline.py index fb753df2a..9de6a5022 100644 --- a/electrum/trampoline.py +++ b/electrum/trampoline.py @@ -92,10 +92,30 @@ def encode_routing_info(r_tags): for route in r_tags: result.append(bitstring.pack('uint:8', len(route))) for step in route: - pubkey, channel, feebase, feerate, cltv = step - result.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv)) + pubkey, scid, feebase, feerate, cltv = step + result.append( + bitstring.BitArray(pubkey) \ + + bitstring.BitArray(scid)\ + + bitstring.pack('intbe:32', feebase)\ + + bitstring.pack('intbe:32', feerate)\ + + bitstring.pack('intbe:16', cltv)) return result.tobytes() +def decode_routing_info(s: bytes): + s = bitstring.BitArray(s) + r_tags = [] + n = 8*(33 + 8 + 4 + 4 + 2) + while s: + route = [] + length, s = s[0:8], s[8:] + length = length.unpack('uint:8')[0] + for i in range(length): + chunk, s = s[0:n], s[n:] + item = chunk.unpack('bytes:33, bytes:8, intbe:32, intbe:32, intbe:16') + route.append(item) + r_tags.append(route) + return r_tags + def is_legacy_relay(invoice_features, r_tags) -> Tuple[bool, Set[bytes]]: """Returns if we deal with a legacy payment and the list of trampoline pubkeys in the invoice. @@ -213,6 +233,7 @@ def create_trampoline_route( # the last trampoline onion must contain routing hints for the last trampoline # node to find the recipient invoice_routing_info = encode_routing_info(r_tags) + assert invoice_routing_info == encode_routing_info(decode_routing_info(invoice_routing_info)) # lnwire invoice_features for trampoline is u64 invoice_features = invoice_features & 0xffffffffffffffff route[-1].invoice_routing_info = invoice_routing_info