tln: add lightning_listen config option - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 962f70c7da0425098ac580fee999e55e85b3b7e2 DIR parent 52377dbfa04072e2296426a6db2e921157bd6da0 HTML Author: Janus <ysangkok@gmail.com> Date: Tue, 16 Oct 2018 17:45:28 +0200 ln: add lightning_listen config option Diffstat: M electrum/lnbase.py | 7 +++---- M electrum/lntransport.py | 125 ++++++++++++++++++++++--------- M electrum/lnworker.py | 19 ++++++++++++++++++- A electrum/tests/test_lntransport.py | 58 ++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 39 deletions(-) --- DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py t@@ -220,7 +220,6 @@ class Peer(PrintError): self.transport.send_bytes(gen_msg(message_name, **kwargs)) async def initialize(self): - await self.transport.handshake() self.send_message("init", gflen=0, lflen=1, localfeatures=self.localfeatures) self.initialized.set_result(True) t@@ -342,7 +341,7 @@ class Peer(PrintError): try: await asyncio.wait_for(self.initialize(), 10) except (OSError, asyncio.TimeoutError, HandshakeFailed) as e: - self.print_error('disconnecting due to: {}'.format(repr(e))) + self.print_error('initialize failed, disconnecting: {}'.format(repr(e))) return self.channel_db.add_recent_peer(self.peer_addr) # loop t@@ -530,7 +529,7 @@ class Peer(PrintError): their_revocation_store = RevocationStore() remote_balance_sat = funding_sat * 1000 - push_msat chan = { - "node_id": self.pubkey, + "node_id": self.peer_addr.pubkey, "channel_id": channel_id, "short_channel_id": None, "funding_outpoint": Outpoint(funding_txid, funding_idx), t@@ -726,7 +725,7 @@ class Peer(PrintError): remote_bitcoin_sig = announcement_signatures_msg["bitcoin_signature"] if not ecc.verify_signature(chan.config[REMOTE].multisig_key.pubkey, remote_bitcoin_sig, h): raise Exception("bitcoin_sig invalid in announcement_signatures") - if not ecc.verify_signature(self.pubkey, remote_node_sig, h): + if not ecc.verify_signature(self.peer_addr.pubkey, remote_node_sig, h): raise Exception("node_sig invalid in announcement_signatures") node_sigs = [local_node_sig, remote_node_sig] DIR diff --git a/electrum/lntransport.py b/electrum/lntransport.py t@@ -23,7 +23,6 @@ class HandshakeState(object): self.h = sha256(self.h + data) return self.h - def get_nonce_bytes(n): """BOLT 8 requires the nonce to be 12 bytes, 4 bytes leading zeroes and 8 bytes little endian encoded 64 bit integer. t@@ -60,29 +59,22 @@ def get_bolt8_hkdf(salt, ikm): return T1, T2 def act1_initiator_message(hs, epriv, epub): - hs.update(epub) ss = get_ecdh(epriv, hs.responder_pub) ck2, temp_k1 = get_bolt8_hkdf(hs.ck, ss) hs.ck = ck2 - c = aead_encrypt(temp_k1, 0, hs.h, b"") + c = aead_encrypt(temp_k1, 0, hs.update(epub), b"") #for next step if we do it hs.update(c) msg = hs.handshake_version + epub + c assert len(msg) == 50 - return msg + return msg, temp_k1 def create_ephemeral_key() -> (bytes, bytes): privkey = ecc.ECPrivkey.generate_random_key() return privkey.get_secret_bytes(), privkey.get_public_key_bytes() -class LNTransport: - def __init__(self, privkey, remote_pubkey, reader, writer): - self.privkey = privkey - self.remote_pubkey = remote_pubkey - self.reader = reader - self.writer = writer - +class LNTransportBase: def send_bytes(self, msg): l = len(msg).to_bytes(2, 'big') lc = aead_encrypt(self.sk, self.sn(), b'', l) t@@ -116,12 +108,97 @@ class LNTransport: raise LightningPeerConnectionClosed() read_buffer += s + def rn(self): + o = self._rn, self.rk + self._rn += 1 + if self._rn == 1000: + self.r_ck, self.rk = get_bolt8_hkdf(self.r_ck, self.rk) + self._rn = 0 + return o + + def sn(self): + o = self._sn + self._sn += 1 + if self._sn == 1000: + self.s_ck, self.sk = get_bolt8_hkdf(self.s_ck, self.sk) + self._sn = 0 + return o + + def init_counters(self, ck): + # init counters + self._sn = 0 + self._rn = 0 + self.r_ck = ck + self.s_ck = ck + +class LNResponderTransport(LNTransportBase): + def __init__(self, privkey, reader, writer): + self.privkey = privkey + self.reader = reader + self.writer = writer + + async def handshake(self, **kwargs): + hs = HandshakeState(privkey_to_pubkey(self.privkey)) + + act1 = b'' + while len(act1) < 50: + act1 += await self.reader.read(50 - len(act1)) + if len(act1) != 50: + raise HandshakeFailed('responder: short act 1 read, length is ' + str(len(act1))) + if bytes([act1[0]]) != HandshakeState.handshake_version: + raise HandshakeFailed('responder: bad handshake version in act 1') + c = act1[-16:] + re = act1[1:34] + h = hs.update(re) + ss = get_ecdh(self.privkey, re) + ck, temp_k1 = get_bolt8_hkdf(sha256(HandshakeState.protocol_name), ss) + _p = aead_decrypt(temp_k1, 0, h, c) + hs.update(c) + + # act 2 + if 'epriv' not in kwargs: + epriv, epub = create_ephemeral_key() + else: + epriv = kwargs['epriv'] + epub = ecc.ECPrivkey(epriv).get_public_key_bytes() + hs.ck = ck + hs.responder_pub = re + + msg, temp_k2 = act1_initiator_message(hs, epriv, epub) + self.writer.write(msg) + + # act 3 + act3 = b'' + while len(act3) < 66: + act3 += await self.reader.read(66 - len(act3)) + if len(act3) != 66: + raise HandshakeFailed('responder: short act 3 read, length is ' + str(len(act3))) + if bytes([act3[0]]) != HandshakeState.handshake_version: + raise HandshakeFailed('responder: bad handshake version in act 3') + c = act3[1:50] + t = act3[-16:] + rs = aead_decrypt(temp_k2, 1, hs.h, c) + ss = get_ecdh(epriv, rs) + ck, temp_k3 = get_bolt8_hkdf(hs.ck, ss) + _p = aead_decrypt(temp_k3, 0, hs.update(c), t) + self.rk, self.sk = get_bolt8_hkdf(ck, b'') + self.init_counters(ck) + return rs + +class LNTransport(LNTransportBase): + def __init__(self, privkey, remote_pubkey, reader, writer): + assert type(privkey) is bytes and len(privkey) == 32 + self.privkey = privkey + self.remote_pubkey = remote_pubkey + self.reader = reader + self.writer = writer + async def handshake(self): hs = HandshakeState(self.remote_pubkey) # Get a new ephemeral key epriv, epub = create_ephemeral_key() - msg = act1_initiator_message(hs, epriv, epub) + msg, _temp_k1 = act1_initiator_message(hs, epriv, epub) # act 1 self.writer.write(msg) rspns = await self.reader.read(2**10) t@@ -145,27 +222,7 @@ class LNTransport: ck, temp_k3 = get_bolt8_hkdf(hs.ck, ss) hs.ck = ck t = aead_encrypt(temp_k3, 0, hs.h, b'') - self.sk, self.rk = get_bolt8_hkdf(hs.ck, b'') msg = hs.handshake_version + c + t self.writer.write(msg) - # init counters - self._sn = 0 - self._rn = 0 - self.r_ck = ck - self.s_ck = ck - - def rn(self): - o = self._rn, self.rk - self._rn += 1 - if self._rn == 1000: - self.r_ck, self.rk = get_bolt8_hkdf(self.r_ck, self.rk) - self._rn = 0 - return o - - def sn(self): - o = self._sn - self._sn += 1 - if self._sn == 1000: - self.s_ck, self.sk = get_bolt8_hkdf(self.s_ck, self.sk) - self._sn = 0 - return o + self.sk, self.rk = get_bolt8_hkdf(hs.ck, b'') + self.init_counters(ck) DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -16,7 +16,7 @@ from . import bitcoin from .keystore import BIP32_KeyStore from .bitcoin import sha256, COIN from .util import bh2u, bfh, PrintError, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions -from .lntransport import LNTransport +from .lntransport import LNTransport, LNResponderTransport from .lnbase import Peer from .lnaddr import lnencode, LnAddr, lndecode from .ecc import der_sig_from_sig_string t@@ -119,6 +119,7 @@ class LNWorker(PrintError): async def _init_peer(): reader, writer = await asyncio.open_connection(peer_addr.host, peer_addr.port) transport = LNTransport(self.node_keypair.privkey, node_id, reader, writer) + await transport.handshake() peer.transport = transport await self.network.main_taskgroup.spawn(peer.main_loop()) asyncio.ensure_future(_init_peer()) t@@ -493,6 +494,22 @@ class LNWorker(PrintError): async def main_loop(self): await self.on_network_update('network_updated') # shortcut (don't block) if funding tx locked and verified await self.network.lnwatcher.on_network_update('network_updated') # ping watcher to check our channels + listen_addr = self.config.get('lightning_listen') + if listen_addr: + adr, colon, port = listen_addr.rpartition(':') + if adr[0] == '[': + # ipv6 + adr = adr[1:-1] + async def cb(reader, writer): + t = LNResponderTransport(self.node_keypair.privkey, reader, writer) + node_id = await t.handshake() + peer = Peer(self, LNPeerAddr("bogus", 1337, node_id), request_initial_sync=self.config.get("request_initial_sync", True)) + peer.transport = t + self.peers[node_id] = peer + await self.network.main_taskgroup.spawn(peer.main_loop()) + self.network.trigger_callback('ln_status') + + await asyncio.start_server(cb, adr, int(port)) while True: await asyncio.sleep(1) now = time.time() DIR diff --git a/electrum/tests/test_lntransport.py b/electrum/tests/test_lntransport.py t@@ -0,0 +1,58 @@ +from electrum.ecc import ECPrivkey +import asyncio +from electrum.lntransport import LNResponderTransport, LNTransport +from unittest import TestCase + +class TestLNTransport(TestCase): + def test_responder(self): + # local static + ls_priv=bytes.fromhex('2121212121212121212121212121212121212121212121212121212121212121') + # ephemeral + e_priv=bytes.fromhex('2222222222222222222222222222222222222222222222222222222222222222') + + class Writer: + def __init__(self): + self.state = 0 + def write(self, data): + assert self.state == 0 + self.state += 1 + assert len(data) == 50 + class Reader: + def __init__(self): + self.state = 0 + async def read(self, num_bytes): + assert self.state in (0, 1) + self.state += 1 + if self.state-1 == 0: + assert num_bytes == 50 + return bytes.fromhex('00036360e856310ce5d294e8be33fc807077dc56ac80d95d9cd4ddbd21325eff73f70df6086551151f58b8afe6c195782c6a') + elif self.state-1 == 1: + assert num_bytes == 66 + return bytes.fromhex('00b9e3a702e93e3a9948c2ed6e5fd7590a6e1c3a0344cfc9d5b57357049aa22355361aa02e55a8fc28fef5bd6d71ad0c38228dc68b1c466263b47fdf31e560e139ba') + transport = LNResponderTransport(ls_priv, Reader(), Writer()) + asyncio.get_event_loop().run_until_complete(transport.handshake(epriv=e_priv)) + def test_loop(self): + l = asyncio.get_event_loop() + responder_shaked = asyncio.Event() + server_shaked = asyncio.Event() + responder_key = ECPrivkey.generate_random_key() + initiator_key = ECPrivkey.generate_random_key() + async def cb(reader, writer): + t = LNResponderTransport(responder_key.get_secret_bytes(), reader, writer) + self.assertEqual(await t.handshake(), initiator_key.get_public_key_bytes()) + t.send_bytes(b'hello from server') + self.assertEqual(await t.read_messages().__anext__(), b'hello from client') + responder_shaked.set() + server_future = asyncio.ensure_future(asyncio.start_server(cb, '127.0.0.1', 42898)) + l.run_until_complete(server_future) + async def connect(): + reader, writer = await asyncio.open_connection('127.0.0.1', 42898) + t = LNTransport(initiator_key.get_secret_bytes(), responder_key.get_public_key_bytes(), reader, writer) + await t.handshake() + t.send_bytes(b'hello from client') + self.assertEqual(await t.read_messages().__anext__(), b'hello from server') + server_shaked.set() + + asyncio.ensure_future(connect()) + l.run_until_complete(responder_shaked.wait()) + l.run_until_complete(server_shaked.wait())