Skip to content

Commit

Permalink
onion_message: create onion_message.py
Browse files Browse the repository at this point in the history
commands: add send_onion_message, get_blinded_path_via
lnworker: add OnionMessageManager to lnworker
lnrouter: add node filter to LNPathFinder
  • Loading branch information
accumulator committed Jul 6, 2024
1 parent 9398d09 commit c38ba15
Show file tree
Hide file tree
Showing 8 changed files with 958 additions and 48 deletions.
68 changes: 63 additions & 5 deletions electrum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import io
import sys
import datetime
import copy
Expand All @@ -41,9 +41,11 @@
import os

from .import util, ecc
from .util import (bfh, format_satoshis, json_decode, json_normalize,
is_hash256_str, is_hex_str, to_bytes, parse_max_spend, to_decimal,
UserFacingException)
from .lnmsg import OnionWireSerializer
from .logging import Logger
from .onion_message import create_blinded_path, send_onion_message_to
from .util import (bfh, format_satoshis, json_decode, json_normalize, is_hash256_str, is_hex_str, to_bytes,
parse_max_spend, to_decimal, UserFacingException)
from . import bitcoin
from .bitcoin import is_address, hash_160, COIN
from .bip32 import BIP32Node
Expand Down Expand Up @@ -165,11 +167,12 @@ async def func_wrapper(*args, **kwargs):
return decorator


class Commands:
class Commands(Logger):

def __init__(self, *, config: 'SimpleConfig',
network: 'Network' = None,
daemon: 'Daemon' = None, callback=None):
Logger.__init__(self)
self.config = config
self.daemon = daemon
self.network = network
Expand Down Expand Up @@ -1392,6 +1395,60 @@ async def convert_currency(self, from_amount=1, from_ccy = '', to_ccy = ''):
"source": self.daemon.fx.exchange.name(),
}

@command('wnl')
async def send_onion_message(self, node_id_or_blinded_path_hex: str, message: str, wallet: Abstract_Wallet = None):
"""
Send an onion message with onionmsg_tlv.message payload to node_id.
"""
assert wallet
assert node_id_or_blinded_path_hex
assert message

node_id_or_blinded_path = bfh(node_id_or_blinded_path_hex)
assert len(node_id_or_blinded_path) >= 33

destination_payload = {
'message': {'text': message.encode('utf-8')}
}

try:
send_onion_message_to(wallet, node_id_or_blinded_path, destination_payload)
return {'success': True}
except Exception as e:
msg = str(e)

return {
'success': False,
'msg': msg
}

@command('wnl')
async def get_blinded_path_via(self, node_id: str, wallet: Abstract_Wallet = None):
"""
Create a blinded path with node_id as introduction point. Introduction point must be direct peer of me.
"""
assert wallet
assert node_id

pubkey = bfh(node_id)
assert len(pubkey) == 33, 'invalid node_id'

peer = wallet.lnworker.peers[pubkey]
assert peer, 'node_id not a peer'

path = [pubkey, wallet.lnworker.node_keypair.pubkey]
session_key = os.urandom(32)
blinded_path = create_blinded_path(session_key, final_recipient_data={}, path=path)

with io.BytesIO() as blinded_path_fd:
OnionWireSerializer._write_complex_field(fd=blinded_path_fd,
field_type='blinded_path',
count=1,
value=blinded_path)
encoded_blinded_path = blinded_path_fd.getvalue()

return encoded_blinded_path.hex()


def eval_bool(x: str) -> bool:
if x == 'false': return False
Expand Down Expand Up @@ -1419,6 +1476,7 @@ def eval_bool(x: str) -> bool:
'redeem_script': 'redeem script (hexadecimal)',
'lightning_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value",
'onchain_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value",
'node_id': "Node pubkey in hex format"
}

command_options = {
Expand Down
61 changes: 25 additions & 36 deletions electrum/lnonion.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from enum import IntEnum

from . import ecc
from .crypto import sha256, hmac_oneshot, chacha20_encrypt, chacha20_poly1305_encrypt
from .crypto import sha256, hmac_oneshot, chacha20_encrypt, chacha20_poly1305_encrypt, chacha20_poly1305_decrypt
from .ecc import ECPubkey
from .util import profiler, xor_bytes, bfh
from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH,
Expand Down Expand Up @@ -175,15 +175,15 @@ def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes],


def get_shared_secrets_along_route2(payment_path_pubkeys_plus: Sequence[Union[bytes, Tuple[bytes, bytes]]],
session_key: bytes) -> tuple[Sequence[bytes], Sequence[bytes]]:
session_key: bytes) -> Tuple[Sequence[bytes], Sequence[bytes]]:
num_hops = len(payment_path_pubkeys_plus)
hop_shared_secrets = num_hops * [b'']
hop_blinded_node_ids = num_hops * [b'']
ephemeral_key = session_key
payment_path_pubkeys = deepcopy(payment_path_pubkeys_plus)
# compute shared key for each hop
for i in range(0, num_hops):
if isinstance(payment_path_pubkeys[i], tuple):
if isinstance(payment_path_pubkeys[i], tuple): # pubkey + ephemeral privkey override
ephemeral_key = payment_path_pubkeys[i][1]
payment_path_pubkeys[i] = payment_path_pubkeys[i][0]
hop_shared_secrets[i] = get_ecdh(ephemeral_key, payment_path_pubkeys[i])
Expand Down Expand Up @@ -250,38 +250,27 @@ def new_onion_packet(
hmac=next_hmac)


def new_onion_packet2(
payment_path_pubkeys: Sequence[Union[bytes, tuple]],
session_key: bytes,
hops_data: Sequence[OnionHopsDataSingle],
*,
associated_data: bytes = b'',
trampoline: bool = False,
) -> OnionPacket:
num_hops = len(payment_path_pubkeys)
assert num_hops == len(hops_data)
hop_shared_secrets, blinded_node_ids = get_shared_secrets_along_route2(payment_path_pubkeys, session_key)
# compute routing info and MAC for each hop
for i in range(num_hops):
if hops_data[i].tlv_stream_name == 'onionmsg_tlv': # route blinding?
rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])
with io.BytesIO() as encrypted_data_tlv_fd:
OnionWireSerializer.write_tlv_stream(
fd=encrypted_data_tlv_fd,
tlv_stream_name='encrypted_data_tlv',
**hops_data[i].blind_fields)
encrypted_data_tlv_bytes = encrypted_data_tlv_fd.getvalue()
encrypted_recipient_data = chacha20_poly1305_encrypt(key=rho_key, nonce=bytes(12), data=encrypted_data_tlv_bytes)
payload = hops_data[i].payload
payload['encrypted_recipient_data'] = {'encrypted_recipient_data': encrypted_recipient_data}

return new_onion_packet(
payment_path_pubkeys=blinded_node_ids,
session_key=session_key,
hops_data=hops_data,
associated_data=associated_data,
trampoline=trampoline,
)
def encrypt_encrypted_data_tlv(*, shared_secret, **kwargs):
rho_key = get_bolt04_onion_key(b'rho', shared_secret)
with io.BytesIO() as encrypted_data_tlv_fd:
OnionWireSerializer.write_tlv_stream(
fd=encrypted_data_tlv_fd,
tlv_stream_name='encrypted_data_tlv',
**kwargs)
encrypted_data_tlv_bytes = encrypted_data_tlv_fd.getvalue()
encrypted_recipient_data = chacha20_poly1305_encrypt(key=rho_key, nonce=bytes(12),
data=encrypted_data_tlv_bytes)
return encrypted_recipient_data


def decrypt_encrypted_data_tlv(*, shared_secret: bytes, encrypted_recipient_data: bytes) -> dict:
rho_key = get_bolt04_onion_key(b'rho', shared_secret)
recipient_data_bytes = chacha20_poly1305_decrypt(key=rho_key, nonce=bytes(12), data=encrypted_recipient_data)

with io.BytesIO(recipient_data_bytes) as fd:
recipient_data = OnionWireSerializer.read_tlv_stream(fd=fd, tlv_stream_name='encrypted_data_tlv')

return recipient_data


def calc_hops_data_for_payment(
Expand Down Expand Up @@ -374,7 +363,7 @@ def process_onion_packet(
onion_packet: OnionPacket,
our_onion_private_key: bytes,
*,
associated_data: bytes = bytes(),
associated_data: bytes = b'',
is_trampoline=False,
tlv_stream_name='payload') -> ProcessedOnionPacket:
if not ecc.ECPubkey.is_pubkey_bytes(onion_packet.public_key):
Expand Down
10 changes: 7 additions & 3 deletions electrum/lnpeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from aiorpcx import ignore_after

from .crypto import sha256, sha256d
from . import bitcoin, util
from . import bitcoin, util, onion_message
from . import ecc
from .ecc import ecdsa_sig64_from_r_and_s, ecdsa_der_sig_from_ecdsa_sig64, ECPubkey
from . import constants
Expand All @@ -28,8 +28,8 @@
from .transaction import PartialTxOutput, match_script_against_template, Sighash
from .logging import Logger
from .lnrouter import RouteEdge
from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment,
process_onion_packet, OnionPacket, construct_onion_error, obfuscate_onion_error, OnionRoutingFailure,
from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment, process_onion_packet,
OnionPacket, construct_onion_error, obfuscate_onion_error, OnionRoutingFailure,
ProcessedOnionPacket, UnsupportedOnionPacketVersion, InvalidOnionMac, InvalidOnionPubkey,
OnionFailureCodeMetaFlag)
from .lnchannel import Channel, RevokeAndAck, RemoteCtnTooFarInFuture, ChannelState, PeerState, ChanCloseOption, CF_ANNOUNCE_CHANNEL
Expand Down Expand Up @@ -2795,3 +2795,7 @@ def process_onion_packet(
if self.network.config.TEST_FAIL_HTLCS_WITH_TEMP_NODE_FAILURE:
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
return processed_onion

def on_onion_message(self, payload):
if getattr(self.lnworker, 'onion_message_manager'): # only on LNWallet
self.lnworker.onion_message_manager.on_onion_message(payload)
19 changes: 16 additions & 3 deletions electrum/lnrouter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import queue
from collections import defaultdict
from typing import Sequence, Tuple, Optional, Dict, TYPE_CHECKING, Set
from typing import Sequence, Tuple, Optional, Dict, TYPE_CHECKING, Set, Callable
import time
import threading
from threading import RLock
Expand Down Expand Up @@ -531,10 +531,17 @@ def get_shortest_path_hops(
invoice_amount_msat: int,
my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
node_filter: Optional[Callable[[NodeInfo], bool]] = None
) -> Dict[bytes, PathEdge]:
# note: we don't lock self.channel_db, so while the path finding runs,
# the underlying graph could potentially change... (not good but maybe ~OK?)

# if destination is filtered, there is no route
if node_filter:
node_info = self.channel_db.get_node_info_for_node_id(nodeB)
if not node_filter(node_info):
return {}

# run Dijkstra
# The search is run in the REVERSE direction, from nodeB to nodeA,
# to properly calculate compound routing fees.
Expand Down Expand Up @@ -577,6 +584,10 @@ def get_shortest_path_hops(
if channel_info is None:
continue
edge_startnode = channel_info.node2_id if channel_info.node1_id == edge_endnode else channel_info.node1_id
if node_filter:
node_info = self.channel_db.get_node_info_for_node_id(edge_startnode)
if not node_filter(node_info):
continue
is_mine = edge_channel_id in my_sending_channels
if is_mine:
if edge_startnode == nodeA: # payment outgoing, on our channel
Expand Down Expand Up @@ -617,6 +628,7 @@ def find_path_for_payment(
invoice_amount_msat: int,
my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
node_filter: Optional[Callable[[NodeInfo], bool]] = None
) -> Optional[LNPaymentPath]:
"""Return a path from nodeA to nodeB."""
assert type(nodeA) is bytes
Expand All @@ -630,7 +642,8 @@ def find_path_for_payment(
nodeB=nodeB,
invoice_amount_msat=invoice_amount_msat,
my_sending_channels=my_sending_channels,
private_route_edges=private_route_edges)
private_route_edges=private_route_edges,
node_filter=node_filter)

if nodeA not in previous_hops:
return None # no path found
Expand Down Expand Up @@ -690,7 +703,7 @@ def find_route(
nodeA: bytes,
nodeB: bytes,
invoice_amount_msat: int,
path = None,
path: Optional[Sequence[PathEdge]] = None,
my_sending_channels: Dict[ShortChannelID, 'Channel'] = None,
private_route_edges: Dict[ShortChannelID, RouteEdge] = None,
) -> Optional[LNPaymentRoute]:
Expand Down
5 changes: 5 additions & 0 deletions electrum/lnworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

from . import constants, util
from . import keystore
from .onion_message import OnionMessageManager
from .util import profiler, chunks, OldTaskGroup
from .invoices import Invoice, PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, LN_EXPIRY_NEVER
from .invoices import BaseInvoice
Expand Down Expand Up @@ -868,6 +869,7 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv):
self.payment_bundles = [] # lists of hashes. todo:persist
self.swap_manager = HttpSwapManager(wallet=self.wallet, lnworker=self)

self.onion_message_manager = OnionMessageManager(self)

def has_deterministic_node_id(self) -> bool:
return bool(self.db.get('lightning_xprv'))
Expand Down Expand Up @@ -956,6 +958,7 @@ def start_network(self, network: 'Network'):
self.lnwatcher = LNWalletWatcher(self, network)
self.swap_manager.start_network(network=network, lnwatcher=self.lnwatcher)
self.lnrater = LNRater(self, network)
self.onion_message_manager.start_network(network=network)

for chan in self.channels.values():
if chan.need_to_subscribe():
Expand Down Expand Up @@ -986,6 +989,8 @@ async def stop(self):
self.lnwatcher = None
if self.swap_manager: # may not be present in tests
await self.swap_manager.stop()
if self.onion_message_manager:
await self.onion_message_manager.stop()

async def wait_for_received_pending_htlcs_to_get_removed(self):
assert self.stopping_soon is True
Expand Down
Loading

0 comments on commit c38ba15

Please sign in to comment.