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.
This commit is contained in:
+34
-35
@@ -208,51 +208,50 @@ def create_route_to_introduction_point(
|
||||
|
||||
# last edge is to introduction point and start of blinded path. remove from route
|
||||
assert path[-1].end_node == introduction_point, 'last hop in route must be introduction point'
|
||||
assert len(path) > 1, "if we are directly connected to the IP, why didn't we return the peer above?"
|
||||
|
||||
peer = lnwallet.lnpeermgr.get_peer_by_pubkey(path[0].end_node)
|
||||
assert peer, "first hop is not a peer"
|
||||
|
||||
# rm last hop (ip-1 -> ip) as it is added explicitly from the blinded path we got (final_hop_pre_ip)
|
||||
path = path[:-1]
|
||||
|
||||
if len(path) == 0:
|
||||
# we pass the onion directly to the introduction point
|
||||
path_key = blinded_path['first_path_key']
|
||||
else:
|
||||
# we construct a route to the introduction point
|
||||
payment_path_pubkeys = [edge.end_node for edge in path]
|
||||
hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route(
|
||||
payment_path_pubkeys,
|
||||
session_key)
|
||||
# we construct a route to the introduction point
|
||||
payment_path_pubkeys = [edge.end_node for edge in path]
|
||||
hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route(
|
||||
payment_path_pubkeys,
|
||||
session_key)
|
||||
|
||||
for edge in path[:-1]:
|
||||
hop = OnionHopsDataSingle(
|
||||
tlv_stream_name='onionmsg_tlv',
|
||||
blind_fields={'next_node_id': {'node_id': edge.end_node}},
|
||||
)
|
||||
hops_data.append(hop)
|
||||
|
||||
# final hop pre-ip, add next_path_key_override
|
||||
final_hop_pre_ip = OnionHopsDataSingle(
|
||||
# exclude first hop (us to first node on path): we don't need to a layer for ourselves
|
||||
for edge in path[1:]:
|
||||
hop = OnionHopsDataSingle(
|
||||
tlv_stream_name='onionmsg_tlv',
|
||||
blind_fields={
|
||||
'next_node_id': {'node_id': introduction_point},
|
||||
'next_path_key_override': {'path_key': blinded_path['first_path_key']},
|
||||
},
|
||||
blind_fields={'next_node_id': {'node_id': edge.end_node}},
|
||||
)
|
||||
hops_data.append(final_hop_pre_ip)
|
||||
hops_data.append(hop)
|
||||
|
||||
# encrypt encrypted_data_tlv here
|
||||
for i, hop in enumerate(hops_data):
|
||||
encrypted_recipient_data = encrypt_onionmsg_data_tlv(
|
||||
shared_secret=hop_shared_secrets[i],
|
||||
**hop.blind_fields)
|
||||
payload = dict(hop.payload)
|
||||
payload['encrypted_recipient_data'] = {
|
||||
'encrypted_recipient_data': encrypted_recipient_data
|
||||
}
|
||||
hops_data[i] = dataclasses.replace(hop, payload=payload)
|
||||
# final hop pre-ip, add next_path_key_override
|
||||
final_hop_pre_ip = OnionHopsDataSingle(
|
||||
tlv_stream_name='onionmsg_tlv',
|
||||
blind_fields={
|
||||
'next_node_id': {'node_id': introduction_point},
|
||||
'next_path_key_override': {'path_key': blinded_path['first_path_key']},
|
||||
},
|
||||
)
|
||||
hops_data.append(final_hop_pre_ip)
|
||||
|
||||
path_key = ecc.ECPrivkey(session_key).get_public_key_bytes()
|
||||
# encrypt encrypted_data_tlv here
|
||||
for i, hop in enumerate(hops_data):
|
||||
encrypted_recipient_data = encrypt_onionmsg_data_tlv(
|
||||
shared_secret=hop_shared_secrets[i],
|
||||
**hop.blind_fields)
|
||||
payload = dict(hop.payload)
|
||||
payload['encrypted_recipient_data'] = {
|
||||
'encrypted_recipient_data': encrypted_recipient_data
|
||||
}
|
||||
hops_data[i] = dataclasses.replace(hop, payload=payload)
|
||||
|
||||
path_key = ecc.ECPrivkey(session_key).get_public_key_bytes()
|
||||
|
||||
return peer, path_key, hops_data, blinded_node_ids
|
||||
|
||||
@@ -628,7 +627,7 @@ class OnionMessageManager(Logger):
|
||||
try:
|
||||
self._send_pending_message(key)
|
||||
except BaseException as e:
|
||||
self.logger.debug(f'error while sending {key=} {e!r}')
|
||||
self.logger.debug(f'error while sending {key=}: ', exc_info=True)
|
||||
req.future.set_exception(copy.copy(e))
|
||||
# NOTE: above, when passing the caught exception instance e directly it leads to GeneratorExit() in
|
||||
if isinstance(e, NoRouteFound) and e.peer_address:
|
||||
|
||||
@@ -377,6 +377,7 @@ class SuccessfulTest(Exception): pass
|
||||
|
||||
|
||||
def inject_chan_into_gossipdb(*, channel_db: ChannelDB, graph: Graph, node1name: str, node2name: str) -> None:
|
||||
print(f"injecting channel {node1name} -> {node2name} into channel_db")
|
||||
chan_ann_raw = graph.channels[(node1name, node2name)].construct_channel_announcement_without_sigs()[0]
|
||||
chan_ann_dict = decode_msg(chan_ann_raw)[1]
|
||||
channel_db.add_channel_announcements(chan_ann_dict, trusted=True)
|
||||
|
||||
@@ -5,8 +5,7 @@ import time
|
||||
import dataclasses
|
||||
import logging
|
||||
from functools import partial
|
||||
from unittest.mock import Mock
|
||||
from types import MappingProxyType
|
||||
from unittest.mock import patch
|
||||
from aiorpcx import NetAddress
|
||||
|
||||
import electrum_ecc as ecc
|
||||
@@ -17,18 +16,19 @@ from electrum.lnmsg import decode_msg, OnionWireSerializer
|
||||
from electrum.lnonion import (
|
||||
OnionHopsDataSingle, OnionPacket, process_onion_packet, get_bolt04_onion_key, encrypt_onionmsg_data_tlv,
|
||||
get_shared_secrets_along_route, new_onion_packet, ONION_MESSAGE_LARGE_SIZE, HOPS_DATA_SIZE, InvalidPayloadSize,
|
||||
encrypt_hops_recipient_data, blinding_privkey)
|
||||
encrypt_hops_recipient_data, blinding_privkey, decrypt_onionmsg_data_tlv)
|
||||
from electrum.crypto import get_ecdh, privkey_to_pubkey
|
||||
from electrum.lntransport import LNPeerAddr
|
||||
from electrum.lnutil import LnFeatures, Keypair, MIN_FINAL_CLTV_DELTA_ACCEPTED, REMOTE
|
||||
from electrum.onion_message import (
|
||||
create_blinded_path, OnionMessageManager, NoRouteFound, Timeout, get_blinded_paths_to_me,
|
||||
create_blinded_path, OnionMessageManager, NoRouteFound, Timeout,
|
||||
create_route_to_introduction_point, get_blinded_paths_to_me
|
||||
)
|
||||
from electrum.util import bfh, read_json_file, OldTaskGroup, get_asyncio_loop
|
||||
from electrum.logging import console_stderr_handler
|
||||
|
||||
from . import ElectrumTestCase
|
||||
from .test_lnpeer import TestPeer
|
||||
from .test_lnpeer import TestPeer, inject_chan_into_gossipdb
|
||||
|
||||
|
||||
TIME_STEP = 0.01 # run tests 100 x faster
|
||||
@@ -531,3 +531,67 @@ class TestOnionMessageUtils(TestPeer):
|
||||
self.assertEqual(blinded_path['num_hops'], len(blinded_path['path']).to_bytes(length=1, byteorder='big'))
|
||||
self.assertIn('blinded_node_id', blinded_path['path'][0])
|
||||
self.assertIn('encrypted_recipient_data', blinded_path['path'][0])
|
||||
|
||||
async def test_create_route_to_introduction_point(self):
|
||||
# A -- B -- C -- D -- E
|
||||
# Alice constructs route to Edward as introduction point to some blinded path
|
||||
line_graph = self.GRAPH_DEFINITIONS['line_graph']
|
||||
graph = self.prepare_chans_and_peers_in_graph(line_graph)
|
||||
alice, bob, carol, dave, edward = graph.workers.values()
|
||||
|
||||
session_key = os.urandom(32)
|
||||
introduction_point = edward.node_keypair.pubkey
|
||||
first_path_key = ecc.ECPrivkey.generate_random_key().get_public_key_bytes()
|
||||
blinded_path = {
|
||||
'first_path_key': first_path_key,
|
||||
}
|
||||
with self.assertRaises(NoRouteFound):
|
||||
create_route_to_introduction_point(alice, blinded_path, introduction_point, session_key)
|
||||
|
||||
for name, definition in line_graph.items():
|
||||
for channel_partner in definition.get('channels', {}):
|
||||
inject_chan_into_gossipdb(
|
||||
channel_db=alice.channel_db,
|
||||
graph=graph,
|
||||
node1name=name,
|
||||
node2name=channel_partner,
|
||||
)
|
||||
|
||||
# patch is_onion_message_node so we don't have to inject node announcements
|
||||
with patch('electrum.onion_message.is_onion_message_node', return_value=True):
|
||||
r = create_route_to_introduction_point(alice, blinded_path, introduction_point, session_key)
|
||||
peer, path_key, hops_data, blinded_node_ids = r
|
||||
# alice hands the onion over to bob
|
||||
self.assertEqual(peer.pubkey, bob.lnpeermgr.node_keypair.pubkey)
|
||||
|
||||
self.assertEqual(path_key, ecc.ECPrivkey(session_key).get_public_key_bytes())
|
||||
self.assertEqual(len(hops_data), 3)
|
||||
self.assertEqual(len(hops_data), len(blinded_node_ids))
|
||||
|
||||
# bob unwraps the first layer, sees the next peer, next peer should be carol
|
||||
self.assertEqual(hops_data[0].blind_fields['next_node_id']['node_id'], carol.lnpeermgr.node_keypair.pubkey)
|
||||
self.assertEqual(hops_data[1].blind_fields['next_node_id']['node_id'], dave.lnpeermgr.node_keypair.pubkey)
|
||||
self.assertEqual(hops_data[2].blind_fields['next_node_id']['node_id'], edward.lnpeermgr.node_keypair.pubkey)
|
||||
self.assertEqual(hops_data[2].blind_fields['next_path_key_override']['path_key'], first_path_key)
|
||||
|
||||
# verify that the recipient data is encrypted to the correct node
|
||||
hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route(
|
||||
(bob.node_keypair.pubkey, carol.node_keypair.pubkey, dave.node_keypair.pubkey),
|
||||
session_key)
|
||||
for hop, ss in zip(hops_data, hop_shared_secrets):
|
||||
encrypted_recipient_data = hop.payload['encrypted_recipient_data']['encrypted_recipient_data']
|
||||
decrypt_onionmsg_data_tlv(
|
||||
shared_secret=ss,
|
||||
encrypted_recipient_data=encrypted_recipient_data,
|
||||
)
|
||||
|
||||
# now Bob is IP, Alice is directly connected to IP
|
||||
introduction_point = bob.node_keypair.pubkey
|
||||
r = create_route_to_introduction_point(alice, blinded_path, introduction_point, session_key)
|
||||
peer, path_key, hops_data, blinded_node_ids = r
|
||||
self.assertEqual(path_key, first_path_key)
|
||||
self.assertEqual(len(hops_data), 0)
|
||||
self.assertEqual(len(blinded_node_ids), 0)
|
||||
alice_bob_peer = alice.lnpeermgr.get_peer_by_pubkey(bob.node_keypair.pubkey)
|
||||
self.assertIsNotNone(alice_bob_peer)
|
||||
self.assertEqual(peer, alice_bob_peer)
|
||||
|
||||
Reference in New Issue
Block a user