Skip to content

Commit 4ed0aed

Browse files
committed
onion_message: fix route construction to ip
Don't include first hop of the path, this is the hop from us to the first node and we don't need a payload for ourselves. Also adds unittest checking this.
1 parent e5766d9 commit 4ed0aed

3 files changed

Lines changed: 106 additions & 42 deletions

File tree

electrum/onion_message.py

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -208,51 +208,50 @@ def create_route_to_introduction_point(
208208

209209
# last edge is to introduction point and start of blinded path. remove from route
210210
assert path[-1].end_node == introduction_point, 'last hop in route must be introduction point'
211+
assert len(path) > 1, "if we are directly connected to the IP, why didn't we return the peer above?"
211212

212213
peer = lnwallet.lnpeermgr.get_peer_by_pubkey(path[0].end_node)
213214
assert peer, "first hop is not a peer"
214215

216+
# rm last hop (ip-1 -> ip) as it is added explicitly from the blinded path we got (final_hop_pre_ip)
215217
path = path[:-1]
216218

217-
if len(path) == 0:
218-
# we pass the onion directly to the introduction point
219-
path_key = blinded_path['first_path_key']
220-
else:
221-
# we construct a route to the introduction point
222-
payment_path_pubkeys = [edge.end_node for edge in path]
223-
hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route(
224-
payment_path_pubkeys,
225-
session_key)
226-
227-
for edge in path[:-1]:
228-
hop = OnionHopsDataSingle(
229-
tlv_stream_name='onionmsg_tlv',
230-
blind_fields={'next_node_id': {'node_id': edge.end_node}},
231-
)
232-
hops_data.append(hop)
233-
234-
# final hop pre-ip, add next_path_key_override
235-
final_hop_pre_ip = OnionHopsDataSingle(
219+
# we construct a route to the introduction point
220+
payment_path_pubkeys = [edge.end_node for edge in path]
221+
hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route(
222+
payment_path_pubkeys,
223+
session_key)
224+
225+
# exclude first hop (us to first node on path): we don't need to a layer for ourselves
226+
for edge in path[1:]:
227+
hop = OnionHopsDataSingle(
236228
tlv_stream_name='onionmsg_tlv',
237-
blind_fields={
238-
'next_node_id': {'node_id': introduction_point},
239-
'next_path_key_override': {'path_key': blinded_path['first_path_key']},
240-
},
229+
blind_fields={'next_node_id': {'node_id': edge.end_node}},
241230
)
242-
hops_data.append(final_hop_pre_ip)
243-
244-
# encrypt encrypted_data_tlv here
245-
for i, hop in enumerate(hops_data):
246-
encrypted_recipient_data = encrypt_onionmsg_data_tlv(
247-
shared_secret=hop_shared_secrets[i],
248-
**hop.blind_fields)
249-
payload = dict(hop.payload)
250-
payload['encrypted_recipient_data'] = {
251-
'encrypted_recipient_data': encrypted_recipient_data
252-
}
253-
hops_data[i] = dataclasses.replace(hop, payload=payload)
231+
hops_data.append(hop)
232+
233+
# final hop pre-ip, add next_path_key_override
234+
final_hop_pre_ip = OnionHopsDataSingle(
235+
tlv_stream_name='onionmsg_tlv',
236+
blind_fields={
237+
'next_node_id': {'node_id': introduction_point},
238+
'next_path_key_override': {'path_key': blinded_path['first_path_key']},
239+
},
240+
)
241+
hops_data.append(final_hop_pre_ip)
242+
243+
# encrypt encrypted_data_tlv here
244+
for i, hop in enumerate(hops_data):
245+
encrypted_recipient_data = encrypt_onionmsg_data_tlv(
246+
shared_secret=hop_shared_secrets[i],
247+
**hop.blind_fields)
248+
payload = dict(hop.payload)
249+
payload['encrypted_recipient_data'] = {
250+
'encrypted_recipient_data': encrypted_recipient_data
251+
}
252+
hops_data[i] = dataclasses.replace(hop, payload=payload)
254253

255-
path_key = ecc.ECPrivkey(session_key).get_public_key_bytes()
254+
path_key = ecc.ECPrivkey(session_key).get_public_key_bytes()
256255

257256
return peer, path_key, hops_data, blinded_node_ids
258257

@@ -628,7 +627,7 @@ async def process_send_queue(self) -> None:
628627
try:
629628
self._send_pending_message(key)
630629
except BaseException as e:
631-
self.logger.debug(f'error while sending {key=} {e!r}')
630+
self.logger.debug(f'error while sending {key=}: ', exc_info=True)
632631
req.future.set_exception(copy.copy(e))
633632
# NOTE: above, when passing the caught exception instance e directly it leads to GeneratorExit() in
634633
if isinstance(e, NoRouteFound) and e.peer_address:

tests/test_lnpeer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ class SuccessfulTest(Exception): pass
377377

378378

379379
def inject_chan_into_gossipdb(*, channel_db: ChannelDB, graph: Graph, node1name: str, node2name: str) -> None:
380+
print(f"injecting channel {node1name} -> {node2name} into channel_db")
380381
chan_ann_raw = graph.channels[(node1name, node2name)].construct_channel_announcement_without_sigs()[0]
381382
chan_ann_dict = decode_msg(chan_ann_raw)[1]
382383
channel_db.add_channel_announcements(chan_ann_dict, trusted=True)

tests/test_onion_message.py

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import dataclasses
66
import logging
77
from functools import partial
8-
from unittest.mock import Mock
9-
from types import MappingProxyType
8+
from unittest.mock import patch
109
from aiorpcx import NetAddress
1110

1211
import electrum_ecc as ecc
@@ -17,18 +16,19 @@
1716
from electrum.lnonion import (
1817
OnionHopsDataSingle, OnionPacket, process_onion_packet, get_bolt04_onion_key, encrypt_onionmsg_data_tlv,
1918
get_shared_secrets_along_route, new_onion_packet, ONION_MESSAGE_LARGE_SIZE, HOPS_DATA_SIZE, InvalidPayloadSize,
20-
encrypt_hops_recipient_data, blinding_privkey)
19+
encrypt_hops_recipient_data, blinding_privkey, decrypt_onionmsg_data_tlv)
2120
from electrum.crypto import get_ecdh, privkey_to_pubkey
2221
from electrum.lntransport import LNPeerAddr
2322
from electrum.lnutil import LnFeatures, Keypair, MIN_FINAL_CLTV_DELTA_ACCEPTED, REMOTE
2423
from electrum.onion_message import (
25-
create_blinded_path, OnionMessageManager, NoRouteFound, Timeout, get_blinded_paths_to_me,
24+
create_blinded_path, OnionMessageManager, NoRouteFound, Timeout,
25+
create_route_to_introduction_point, get_blinded_paths_to_me
2626
)
2727
from electrum.util import bfh, read_json_file, OldTaskGroup, get_asyncio_loop
2828
from electrum.logging import console_stderr_handler
2929

3030
from . import ElectrumTestCase
31-
from .test_lnpeer import TestPeer
31+
from .test_lnpeer import TestPeer, inject_chan_into_gossipdb
3232

3333

3434
TIME_STEP = 0.01 # run tests 100 x faster
@@ -531,3 +531,67 @@ async def test_get_blinded_paths_to_me_payment(self):
531531
self.assertEqual(blinded_path['num_hops'], len(blinded_path['path']).to_bytes(length=1))
532532
self.assertIn('blinded_node_id', blinded_path['path'][0])
533533
self.assertIn('encrypted_recipient_data', blinded_path['path'][0])
534+
535+
async def test_create_route_to_introduction_point(self):
536+
# A -- B -- C -- D -- E
537+
# Alice constructs route to Edward as introduction point to some blinded path
538+
line_graph = self.GRAPH_DEFINITIONS['line_graph']
539+
graph = self.prepare_chans_and_peers_in_graph(line_graph)
540+
alice, bob, carol, dave, edward = graph.workers.values()
541+
542+
session_key = os.urandom(32)
543+
introduction_point = edward.node_keypair.pubkey
544+
first_path_key = ecc.ECPrivkey.generate_random_key().get_public_key_bytes()
545+
blinded_path = {
546+
'first_path_key': first_path_key,
547+
}
548+
with self.assertRaises(NoRouteFound):
549+
create_route_to_introduction_point(alice, blinded_path, introduction_point, session_key)
550+
551+
for name, definition in line_graph.items():
552+
for channel_partner in definition.get('channels', {}):
553+
inject_chan_into_gossipdb(
554+
channel_db=alice.channel_db,
555+
graph=graph,
556+
node1name=name,
557+
node2name=channel_partner,
558+
)
559+
560+
# patch is_onion_message_node so we don't have to inject node announcements
561+
with patch('electrum.onion_message.is_onion_message_node', return_value=True):
562+
r = create_route_to_introduction_point(alice, blinded_path, introduction_point, session_key)
563+
peer, path_key, hops_data, blinded_node_ids = r
564+
# alice hands the onion over to bob
565+
self.assertEqual(peer.pubkey, bob.lnpeermgr.node_keypair.pubkey)
566+
567+
self.assertEqual(path_key, ecc.ECPrivkey(session_key).get_public_key_bytes())
568+
self.assertEqual(len(hops_data), 3)
569+
self.assertEqual(len(hops_data), len(blinded_node_ids))
570+
571+
# bob unwraps the first layer, sees the next peer, next peer should be carol
572+
self.assertEqual(hops_data[0].blind_fields['next_node_id']['node_id'], carol.lnpeermgr.node_keypair.pubkey)
573+
self.assertEqual(hops_data[1].blind_fields['next_node_id']['node_id'], dave.lnpeermgr.node_keypair.pubkey)
574+
self.assertEqual(hops_data[2].blind_fields['next_node_id']['node_id'], edward.lnpeermgr.node_keypair.pubkey)
575+
self.assertEqual(hops_data[2].blind_fields['next_path_key_override']['path_key'], first_path_key)
576+
577+
# verify that the recipient data is encrypted to the correct node
578+
hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route(
579+
(bob.node_keypair.pubkey, carol.node_keypair.pubkey, dave.node_keypair.pubkey),
580+
session_key)
581+
for hop, ss in zip(hops_data, hop_shared_secrets):
582+
encrypted_recipient_data = hop.payload['encrypted_recipient_data']['encrypted_recipient_data']
583+
decrypt_onionmsg_data_tlv(
584+
shared_secret=ss,
585+
encrypted_recipient_data=encrypted_recipient_data,
586+
)
587+
588+
# now Bob is IP, Alice is directly connected to IP
589+
introduction_point = bob.node_keypair.pubkey
590+
r = create_route_to_introduction_point(alice, blinded_path, introduction_point, session_key)
591+
peer, path_key, hops_data, blinded_node_ids = r
592+
self.assertEqual(path_key, first_path_key)
593+
self.assertEqual(len(hops_data), 0)
594+
self.assertEqual(len(blinded_node_ids), 0)
595+
alice_bob_peer = alice.lnpeermgr.get_peer_by_pubkey(bob.node_keypair.pubkey)
596+
self.assertIsNotNone(alice_bob_peer)
597+
self.assertEqual(peer, alice_bob_peer)

0 commit comments

Comments
 (0)