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:
f321x
2026-02-19 15:42:04 +01:00
parent 4134dc7b25
commit 4254c9a051
3 changed files with 104 additions and 40 deletions
+34 -35
View File
@@ -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:
+1
View File
@@ -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)
+69 -5
View File
@@ -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)