URI: 
       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())