URI: 
       tsplit lnrouter from lnbase - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 8ba63380b4b3fe7d79432c9d20cb3ba8faf65d22
   DIR parent f6763b60846442433ce8ec08f45b88239f56b354
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Mon, 28 May 2018 13:51:39 +0200
       
       split lnrouter from lnbase
       
       Diffstat:
         M lib/lnbase.py                       |     481 +------------------------------
         A lib/lnrouter.py                     |     517 +++++++++++++++++++++++++++++++
         M lib/lnworker.py                     |      12 +++++++++---
         M lib/tests/test_lnbase.py            |      12 +++++++-----
       
       4 files changed, 539 insertions(+), 483 deletions(-)
       ---
   DIR diff --git a/lib/lnbase.py b/lib/lnbase.py
       t@@ -35,6 +35,7 @@ from . import constants
        from . import transaction
        from .util import PrintError, bh2u, print_error, bfh, profiler, xor_bytes
        from .transaction import opcodes, Transaction
       +from .lnrouter import new_onion_packet, OnionHopsDataSingle, OnionPerHop
        
        from collections import namedtuple, defaultdict
        
       t@@ -562,8 +563,10 @@ def is_synced(network):
            synced = server_height != 0 and network.is_up_to_date() and local_height >= server_height
            return synced
        
       +
        class Peer(PrintError):
       -    def __init__(self, host, port, pubkey, privkey, network, request_initial_sync=False):
       +
       +    def __init__(self, host, port, pubkey, privkey, network, channel_db, path_finder, request_initial_sync=False):
                self.update_add_htlc_event = asyncio.Event()
                self.channel_update_event = asyncio.Event()
                self.host = host
       t@@ -571,6 +574,8 @@ class Peer(PrintError):
                self.privkey = privkey
                self.pubkey = pubkey
                self.network = network
       +        self.channel_db = channel_db
       +        self.path_finder = path_finder
                self.read_buffer = b''
                self.ping_time = 0
                self.futures = ["channel_accepted",
       t@@ -591,10 +596,6 @@ class Peer(PrintError):
                self.commitment_signed = defaultdict(asyncio.Future)
                self.initialized = asyncio.Future()
                self.localfeatures = (0x08 if request_initial_sync else 0)
       -        # view of the network
       -        self.nodes = {} # received node announcements
       -        self.channel_db = ChannelDB()
       -        self.path_finder = LNPathFinder(self.channel_db)
                self.unfulfilled_htlcs = []
        
            def diagnostic_name(self):
       t@@ -1333,476 +1334,6 @@ class Peer(PrintError):
                self.revoke_and_ack[channel_id].set_result(payload)
        
        
       -class ChannelInfo(PrintError):
       -
       -    def __init__(self, channel_announcement_payload):
       -        self.channel_id = channel_announcement_payload['short_channel_id']
       -        self.node_id_1 = channel_announcement_payload['node_id_1']
       -        self.node_id_2 = channel_announcement_payload['node_id_2']
       -        assert type(self.node_id_1) is bytes
       -        assert type(self.node_id_2) is bytes
       -        assert list(sorted([self.node_id_1, self.node_id_2])) == [self.node_id_1, self.node_id_2]
       -
       -        self.capacity_sat = None
       -        self.policy_node1 = None
       -        self.policy_node2 = None
       -
       -    def set_capacity(self, capacity):
       -        # TODO call this after looking up UTXO for funding txn on chain
       -        self.capacity_sat = capacity
       -
       -    def on_channel_update(self, msg_payload):
       -        assert self.channel_id == msg_payload['short_channel_id']
       -        flags = int.from_bytes(msg_payload['flags'], 'big')
       -        direction = flags & 1
       -        if direction == 0:
       -            self.policy_node1 = ChannelInfoDirectedPolicy(msg_payload)
       -        else:
       -            self.policy_node2 = ChannelInfoDirectedPolicy(msg_payload)
       -        self.print_error('channel update', binascii.hexlify(self.channel_id).decode("ascii"), flags)
       -
       -    def get_policy_for_node(self, node_id):
       -        if node_id == self.node_id_1:
       -            return self.policy_node1
       -        elif node_id == self.node_id_2:
       -            return self.policy_node2
       -        else:
       -            raise Exception('node_id {} not in channel {}'.format(node_id, self.channel_id))
       -
       -
       -class ChannelInfoDirectedPolicy:
       -
       -    def __init__(self, channel_update_payload):
       -        self.cltv_expiry_delta           = channel_update_payload['cltv_expiry_delta']
       -        self.htlc_minimum_msat           = channel_update_payload['htlc_minimum_msat']
       -        self.fee_base_msat               = channel_update_payload['fee_base_msat']
       -        self.fee_proportional_millionths = channel_update_payload['fee_proportional_millionths']
       -        self.cltv_expiry_delta = int.from_bytes(self.cltv_expiry_delta, "big")
       -        self.htlc_minimum_msat = int.from_bytes(self.htlc_minimum_msat, "big")
       -        self.fee_base_msat = int.from_bytes(self.fee_base_msat, "big")
       -        self.fee_proportional_millionths = int.from_bytes(self.fee_proportional_millionths, "big")
       -
       -class ChannelDB(PrintError):
       -
       -    def __init__(self):
       -        self._id_to_channel_info = {}
       -        self._channels_for_node = defaultdict(set)  # node -> set(short_channel_id)
       -
       -    def get_channel_info(self, channel_id):
       -        return self._id_to_channel_info.get(channel_id, None)
       -
       -    def get_channels_for_node(self, node_id):
       -        """Returns the set of channels that have node_id as one of the endpoints."""
       -        return self._channels_for_node[node_id]
       -
       -    def on_channel_announcement(self, msg_payload):
       -        short_channel_id = msg_payload['short_channel_id']
       -        self.print_error('channel announcement', binascii.hexlify(short_channel_id).decode("ascii"))
       -        channel_info = ChannelInfo(msg_payload)
       -        self._id_to_channel_info[short_channel_id] = channel_info
       -        self._channels_for_node[channel_info.node_id_1].add(short_channel_id)
       -        self._channels_for_node[channel_info.node_id_2].add(short_channel_id)
       -
       -    def on_channel_update(self, msg_payload):
       -        short_channel_id = msg_payload['short_channel_id']
       -        try:
       -            channel_info = self._id_to_channel_info[short_channel_id]
       -        except KeyError:
       -            print("could not find", short_channel_id)
       -        else:
       -            channel_info.on_channel_update(msg_payload)
       -
       -    def remove_channel(self, short_channel_id):
       -        try:
       -            channel_info = self._id_to_channel_info[short_channel_id]
       -        except KeyError:
       -            self.print_error('cannot find channel {}'.format(short_channel_id))
       -            return
       -        self._id_to_channel_info.pop(short_channel_id, None)
       -        for node in (channel_info.node_id_1, channel_info.node_id_2):
       -            try:
       -                self._channels_for_node[node].remove(short_channel_id)
       -            except KeyError:
       -                pass
       -
       -
       -class RouteEdge:
       -
       -    def __init__(self, node_id: bytes, short_channel_id: bytes,
       -                 channel_policy: ChannelInfoDirectedPolicy):
       -        self.node_id = node_id
       -        self.short_channel_id = short_channel_id
       -        self.channel_policy = channel_policy
       -
       -
       -class LNPathFinder(PrintError):
       -
       -    def __init__(self, channel_db):
       -        self.channel_db = channel_db
       -
       -    def _edge_cost(self, short_channel_id: bytes, start_node: bytes, payment_amt_msat: int) -> float:
       -        """Heuristic cost of going through a channel.
       -        direction: 0 or 1. --- 0 means node_id_1 -> node_id_2
       -        """
       -        channel_info = self.channel_db.get_channel_info(short_channel_id)
       -        if channel_info is None:
       -            return float('inf')
       -
       -        channel_policy = channel_info.get_policy_for_node(start_node)
       -        if channel_policy is None: return float('inf')
       -        cltv_expiry_delta           = channel_policy.cltv_expiry_delta
       -        htlc_minimum_msat           = channel_policy.htlc_minimum_msat
       -        fee_base_msat               = channel_policy.fee_base_msat
       -        fee_proportional_millionths = channel_policy.fee_proportional_millionths
       -        if payment_amt_msat is not None:
       -            if payment_amt_msat < htlc_minimum_msat:
       -                return float('inf')  # payment amount too little
       -            if channel_info.capacity_sat is not None and \
       -                    payment_amt_msat // 1000 > channel_info.capacity_sat:
       -                return float('inf')  # payment amount too large
       -        amt = payment_amt_msat or 50000 * 1000  # guess for typical payment amount
       -        fee_msat = fee_base_msat + amt * fee_proportional_millionths / 1000000
       -        # TODO revise
       -        # paying 10 more satoshis ~ waiting one more block
       -        fee_cost = fee_msat / 1000 / 10
       -        cltv_cost = cltv_expiry_delta
       -        return cltv_cost + fee_cost + 1
       -
       -    @profiler
       -    def find_path_for_payment(self, from_node_id: bytes, to_node_id: bytes,
       -                              amount_msat: int=None) -> Sequence[Tuple[bytes, bytes]]:
       -        """Return a path between from_node_id and to_node_id.
       -
       -        Returns a list of (node_id, short_channel_id) representing a path.
       -        To get from node ret[n][0] to ret[n+1][0], use channel ret[n+1][1];
       -        i.e. an element reads as, "to get to node_id, travel through short_channel_id"
       -        """
       -        if amount_msat is not None: assert type(amount_msat) is int
       -        # TODO find multiple paths??
       -
       -        # run Dijkstra
       -        distance_from_start = defaultdict(lambda: float('inf'))
       -        distance_from_start[from_node_id] = 0
       -        prev_node = {}
       -        nodes_to_explore = queue.PriorityQueue()
       -        nodes_to_explore.put((0, from_node_id))
       -
       -        while nodes_to_explore.qsize() > 0:
       -            dist_to_cur_node, cur_node = nodes_to_explore.get()
       -            if cur_node == to_node_id:
       -                break
       -            if dist_to_cur_node != distance_from_start[cur_node]:
       -                # queue.PriorityQueue does not implement decrease_priority,
       -                # so instead of decreasing priorities, we add items again into the queue.
       -                # so there are duplicates in the queue, that we discard now:
       -                continue
       -            for edge_channel_id in self.channel_db.get_channels_for_node(cur_node):
       -                channel_info = self.channel_db.get_channel_info(edge_channel_id)
       -                node1, node2 = channel_info.node_id_1, channel_info.node_id_2
       -                neighbour = node2 if node1 == cur_node else node1
       -                alt_dist_to_neighbour = distance_from_start[cur_node] \
       -                                        + self._edge_cost(edge_channel_id, cur_node, amount_msat)
       -                if alt_dist_to_neighbour < distance_from_start[neighbour]:
       -                    distance_from_start[neighbour] = alt_dist_to_neighbour
       -                    prev_node[neighbour] = cur_node, edge_channel_id
       -                    nodes_to_explore.put((alt_dist_to_neighbour, neighbour))
       -        else:
       -            return None  # no path found
       -
       -        # backtrack from end to start
       -        cur_node = to_node_id
       -        path = []
       -        while cur_node != from_node_id:
       -            prev_node_id, edge_taken = prev_node[cur_node]
       -            path += [(cur_node, edge_taken)]
       -            cur_node = prev_node_id
       -        path.reverse()
       -        return path
       -
       -    def create_route_from_path(self, path, from_node_id: bytes) -> Sequence[RouteEdge]:
       -        assert type(from_node_id) is bytes
       -        if path is None:
       -            raise Exception('cannot create route from None path')
       -        route = []
       -        prev_node_id = from_node_id
       -        for node_id, short_channel_id in path:
       -            channel_info = self.channel_db.get_channel_info(short_channel_id)
       -            if channel_info is None:
       -                raise Exception('cannot find channel info for short_channel_id: {}'.format(bh2u(short_channel_id)))
       -            channel_policy = channel_info.get_policy_for_node(prev_node_id)
       -            if channel_policy is None:
       -                raise Exception('cannot find channel policy for short_channel_id: {}'.format(bh2u(short_channel_id)))
       -            route.append(RouteEdge(node_id, short_channel_id, channel_policy))
       -            prev_node_id = node_id
       -        return route
       -
       -
       -# bolt 04, "onion"  ----->
       -
       -NUM_MAX_HOPS_IN_PATH = 20
       -HOPS_DATA_SIZE = 1300      # also sometimes called routingInfoSize in bolt-04
       -PER_HOP_FULL_SIZE = 65     # HOPS_DATA_SIZE / 20
       -NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE
       -PER_HOP_HMAC_SIZE = 32
       -
       -
       -class UnsupportedOnionPacketVersion(Exception): pass
       -class InvalidOnionMac(Exception): pass
       -
       -
       -class OnionPerHop:
       -
       -    def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes):
       -        self.short_channel_id = short_channel_id
       -        self.amt_to_forward = amt_to_forward
       -        self.outgoing_cltv_value = outgoing_cltv_value
       -
       -    def to_bytes(self) -> bytes:
       -        ret = self.short_channel_id
       -        ret += self.amt_to_forward
       -        ret += self.outgoing_cltv_value
       -        ret += bytes(12)  # padding
       -        if len(ret) != 32:
       -            raise Exception('unexpected length {}'.format(len(ret)))
       -        return ret
       -
       -    @classmethod
       -    def from_bytes(cls, b: bytes):
       -        if len(b) != 32:
       -            raise Exception('unexpected length {}'.format(len(b)))
       -        return OnionPerHop(
       -            short_channel_id=b[:8],
       -            amt_to_forward=b[8:16],
       -            outgoing_cltv_value=b[16:20]
       -        )
       -
       -
       -class OnionHopsDataSingle:  # called HopData in lnd
       -
       -    def __init__(self, per_hop: OnionPerHop = None):
       -        self.realm = 0
       -        self.per_hop = per_hop
       -        self.hmac = None
       -
       -    def to_bytes(self) -> bytes:
       -        ret = bytes([self.realm])
       -        ret += self.per_hop.to_bytes()
       -        ret += self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE)
       -        if len(ret) != PER_HOP_FULL_SIZE:
       -            raise Exception('unexpected length {}'.format(len(ret)))
       -        return ret
       -
       -    @classmethod
       -    def from_bytes(cls, b: bytes):
       -        if len(b) != PER_HOP_FULL_SIZE:
       -            raise Exception('unexpected length {}'.format(len(b)))
       -        ret = OnionHopsDataSingle()
       -        ret.realm = b[0]
       -        if ret.realm != 0:
       -            raise Exception('only realm 0 is supported')
       -        ret.per_hop = OnionPerHop.from_bytes(b[1:33])
       -        ret.hmac = b[33:]
       -        return ret
       -
       -
       -class OnionPacket:
       -
       -    def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes):
       -        self.version = 0
       -        self.public_key = public_key
       -        self.hops_data = hops_data  # also called RoutingInfo in bolt-04
       -        self.hmac = hmac
       -
       -    def to_bytes(self) -> bytes:
       -        ret = bytes([self.version])
       -        ret += self.public_key
       -        ret += self.hops_data
       -        ret += self.hmac
       -        if len(ret) != 1366:
       -            raise Exception('unexpected length {}'.format(len(ret)))
       -        return ret
       -
       -    @classmethod
       -    def from_bytes(cls, b: bytes):
       -        if len(b) != 1366:
       -            raise Exception('unexpected length {}'.format(len(b)))
       -        version = b[0]
       -        if version != 0:
       -            raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version))
       -        return OnionPacket(
       -            public_key=b[1:34],
       -            hops_data=b[34:1334],
       -            hmac=b[1334:]
       -        )
       -
       -
       -def get_bolt04_onion_key(key_type: bytes, secret: bytes) -> bytes:
       -    if key_type not in (b'rho', b'mu', b'um', b'ammag'):
       -        raise Exception('invalid key_type {}'.format(key_type))
       -    key = hmac.new(key_type, msg=secret, digestmod=hashlib.sha256).digest()
       -    return key
       -
       -
       -def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes],
       -                                   session_key: bytes) -> Sequence[bytes]:
       -    num_hops = len(payment_path_pubkeys)
       -    hop_shared_secrets = num_hops * [b'']
       -    ephemeral_key = session_key
       -    # compute shared key for each hop
       -    for i in range(0, num_hops):
       -        hop_shared_secrets[i] = get_ecdh(ephemeral_key, payment_path_pubkeys[i])
       -        ephemeral_pubkey = ecc.ECPrivkey(ephemeral_key).get_public_key_bytes()
       -        blinding_factor = sha256(ephemeral_pubkey + hop_shared_secrets[i])
       -        blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big")
       -        ephemeral_key_int = int.from_bytes(ephemeral_key, byteorder="big")
       -        ephemeral_key_int = ephemeral_key_int * blinding_factor_int % SECP256k1.order
       -        ephemeral_key = ephemeral_key_int.to_bytes(32, byteorder="big")
       -    return hop_shared_secrets
       -
       -
       -def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
       -                     hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket:
       -    num_hops = len(payment_path_pubkeys)
       -    hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
       -
       -    filler = generate_filler(b'rho', num_hops, PER_HOP_FULL_SIZE, hop_shared_secrets)
       -    mix_header = bytes(HOPS_DATA_SIZE)
       -    next_hmac = bytes(PER_HOP_HMAC_SIZE)
       -
       -    # compute routing info and MAC for each hop
       -    for i in range(num_hops-1, -1, -1):
       -        rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])
       -        mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i])
       -        hops_data[i].hmac = next_hmac
       -        stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES)
       -        mix_header = mix_header[:-PER_HOP_FULL_SIZE]
       -        mix_header = hops_data[i].to_bytes() + mix_header
       -        mix_header = xor_bytes(mix_header, stream_bytes)
       -        if i == num_hops - 1 and len(filler) != 0:
       -            mix_header = mix_header[:-len(filler)] + filler
       -        packet = mix_header + associated_data
       -        next_hmac = hmac.new(mu_key, msg=packet, digestmod=hashlib.sha256).digest()
       -
       -    return OnionPacket(
       -        public_key=ecc.ECPrivkey(session_key).get_public_key_bytes(),
       -        hops_data=mix_header,
       -        hmac=next_hmac)
       -
       -
       -def generate_filler(key_type: bytes, num_hops: int, hop_size: int,
       -                    shared_secrets: Sequence[bytes]) -> bytes:
       -    filler_size = (NUM_MAX_HOPS_IN_PATH + 1) * hop_size
       -    filler = bytearray(filler_size)
       -
       -    for i in range(0, num_hops-1):  # -1, as last hop does not obfuscate
       -        filler = filler[hop_size:]
       -        filler += bytearray(hop_size)
       -        stream_key = get_bolt04_onion_key(key_type, shared_secrets[i])
       -        stream_bytes = generate_cipher_stream(stream_key, filler_size)
       -        filler = xor_bytes(filler, stream_bytes)
       -
       -    return filler[(NUM_MAX_HOPS_IN_PATH-num_hops+2)*hop_size:]
       -
       -
       -def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes:
       -    algo = algorithms.ChaCha20(stream_key, nonce=bytes(16))
       -    cipher = Cipher(algo, mode=None, backend=default_backend())
       -    encryptor = cipher.encryptor()
       -    return encryptor.update(bytes(num_bytes))
       -
       -
       -ProcessedOnionPacket = namedtuple("ProcessedOnionPacket", ["are_we_final", "hop_data", "next_packet"])
       -
       -
       -# TODO replay protection
       -def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
       -                         our_onion_private_key: bytes) -> ProcessedOnionPacket:
       -    shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key)
       -
       -    # check message integrity
       -    mu_key = get_bolt04_onion_key(b'mu', shared_secret)
       -    calculated_mac = hmac.new(mu_key, msg=onion_packet.hops_data+associated_data,
       -                              digestmod=hashlib.sha256).digest()
       -    if onion_packet.hmac != calculated_mac:
       -        raise InvalidOnionMac()
       -
       -    # peel an onion layer off
       -    rho_key = get_bolt04_onion_key(b'rho', shared_secret)
       -    stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES)
       -    padded_header = onion_packet.hops_data + bytes(PER_HOP_FULL_SIZE)
       -    next_hops_data = xor_bytes(padded_header, stream_bytes)
       -
       -    # calc next ephemeral key
       -    blinding_factor = sha256(onion_packet.public_key + shared_secret)
       -    blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big")
       -    next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int
       -    next_public_key = next_public_key_int.get_public_key_bytes()
       -
       -    hop_data = OnionHopsDataSingle.from_bytes(next_hops_data[:PER_HOP_FULL_SIZE])
       -    next_onion_packet = OnionPacket(
       -        public_key=next_public_key,
       -        hops_data=next_hops_data[PER_HOP_FULL_SIZE:],
       -        hmac=hop_data.hmac
       -    )
       -    if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):
       -        # we are the destination / exit node
       -        are_we_final = True
       -    else:
       -        # we are an intermediate node; forwarding
       -        are_we_final = False
       -    return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet)
       -
       -
       -class FailedToDecodeOnionError(Exception): pass
       -
       -
       -class OnionRoutingFailureMessage:
       -
       -    def __init__(self, code: int, data: bytes):
       -        self.code = code
       -        self.data = data
       -    def __repr__(self):
       -        return repr((self.code, self.data))
       -
       -
       -def _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
       -                        session_key: bytes) -> (bytes, int):
       -    """Returns the decoded error bytes, and the index of the sender of the error."""
       -    num_hops = len(payment_path_pubkeys)
       -    hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
       -    for i in range(num_hops):
       -        ammag_key = get_bolt04_onion_key(b'ammag', hop_shared_secrets[i])
       -        um_key = get_bolt04_onion_key(b'um', hop_shared_secrets[i])
       -        stream_bytes = generate_cipher_stream(ammag_key, len(error_packet))
       -        error_packet = xor_bytes(error_packet, stream_bytes)
       -        hmac_computed = hmac.new(um_key, msg=error_packet[32:], digestmod=hashlib.sha256).digest()
       -        hmac_found = error_packet[:32]
       -        if hmac_computed == hmac_found:
       -            return error_packet, i
       -    raise FailedToDecodeOnionError()
       -
       -
       -def decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
       -                       session_key: bytes) -> (OnionRoutingFailureMessage, int):
       -    """Returns the failure message, and the index of the sender of the error."""
       -    decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key)
       -    failure_msg = get_failure_msg_from_onion_error(decrypted_error)
       -    return failure_msg, sender_index
       -
       -
       -def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailureMessage:
       -    # get failure_msg bytes from error packet
       -    failure_len = int.from_bytes(decrypted_error_packet[32:34], byteorder='big')
       -    failure_msg = decrypted_error_packet[34:34+failure_len]
       -    # create failure message object
       -    failure_code = int.from_bytes(failure_msg[:2], byteorder='big')
       -    failure_data = failure_msg[2:]
       -    return OnionRoutingFailureMessage(failure_code, failure_data)
       -
       -
       -# <----- bolt 04, "onion"
       -
       -
        def count_trailing_zeros(index):
            """ BOLT-03 (where_to_put_secret) """
            try:
   DIR diff --git a/lib/lnrouter.py b/lib/lnrouter.py
       t@@ -0,0 +1,517 @@
       +# -*- coding: utf-8 -*-
       +#
       +# Electrum - lightweight Bitcoin client
       +# Copyright (C) 2018 The Electrum developers
       +#
       +# Permission is hereby granted, free of charge, to any person
       +# obtaining a copy of this software and associated documentation files
       +# (the "Software"), to deal in the Software without restriction,
       +# including without limitation the rights to use, copy, modify, merge,
       +# publish, distribute, sublicense, and/or sell copies of the Software,
       +# and to permit persons to whom the Software is furnished to do so,
       +# subject to the following conditions:
       +#
       +# The above copyright notice and this permission notice shall be
       +# included in all copies or substantial portions of the Software.
       +#
       +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
       +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
       +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
       +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
       +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
       +# 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 queue
       +import traceback
       +import sys
       +import binascii
       +import hashlib
       +import hmac
       +from collections import namedtuple, defaultdict
       +from typing import Sequence, Union, Tuple
       +
       +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
       +from cryptography.hazmat.backends import default_backend
       +
       +from . import bitcoin
       +from . import ecc
       +from . import crypto
       +from .crypto import sha256
       +from .util import PrintError, bh2u, print_error, bfh, profiler, xor_bytes
       +from . import lnbase
       +
       +
       +class ChannelInfo(PrintError):
       +
       +    def __init__(self, channel_announcement_payload):
       +        self.channel_id = channel_announcement_payload['short_channel_id']
       +        self.node_id_1 = channel_announcement_payload['node_id_1']
       +        self.node_id_2 = channel_announcement_payload['node_id_2']
       +        assert type(self.node_id_1) is bytes
       +        assert type(self.node_id_2) is bytes
       +        assert list(sorted([self.node_id_1, self.node_id_2])) == [self.node_id_1, self.node_id_2]
       +
       +        self.capacity_sat = None
       +        self.policy_node1 = None
       +        self.policy_node2 = None
       +
       +    def set_capacity(self, capacity):
       +        # TODO call this after looking up UTXO for funding txn on chain
       +        self.capacity_sat = capacity
       +
       +    def on_channel_update(self, msg_payload):
       +        assert self.channel_id == msg_payload['short_channel_id']
       +        flags = int.from_bytes(msg_payload['flags'], 'big')
       +        direction = flags & 1
       +        if direction == 0:
       +            self.policy_node1 = ChannelInfoDirectedPolicy(msg_payload)
       +        else:
       +            self.policy_node2 = ChannelInfoDirectedPolicy(msg_payload)
       +        self.print_error('channel update', binascii.hexlify(self.channel_id).decode("ascii"), flags)
       +
       +    def get_policy_for_node(self, node_id):
       +        if node_id == self.node_id_1:
       +            return self.policy_node1
       +        elif node_id == self.node_id_2:
       +            return self.policy_node2
       +        else:
       +            raise Exception('node_id {} not in channel {}'.format(node_id, self.channel_id))
       +
       +
       +class ChannelInfoDirectedPolicy:
       +
       +    def __init__(self, channel_update_payload):
       +        self.cltv_expiry_delta           = channel_update_payload['cltv_expiry_delta']
       +        self.htlc_minimum_msat           = channel_update_payload['htlc_minimum_msat']
       +        self.fee_base_msat               = channel_update_payload['fee_base_msat']
       +        self.fee_proportional_millionths = channel_update_payload['fee_proportional_millionths']
       +        self.cltv_expiry_delta = int.from_bytes(self.cltv_expiry_delta, "big")
       +        self.htlc_minimum_msat = int.from_bytes(self.htlc_minimum_msat, "big")
       +        self.fee_base_msat = int.from_bytes(self.fee_base_msat, "big")
       +        self.fee_proportional_millionths = int.from_bytes(self.fee_proportional_millionths, "big")
       +
       +
       +class ChannelDB(PrintError):
       +
       +    def __init__(self):
       +        self._id_to_channel_info = {}
       +        self._channels_for_node = defaultdict(set)  # node -> set(short_channel_id)
       +
       +    def get_channel_info(self, channel_id):
       +        return self._id_to_channel_info.get(channel_id, None)
       +
       +    def get_channels_for_node(self, node_id):
       +        """Returns the set of channels that have node_id as one of the endpoints."""
       +        return self._channels_for_node[node_id]
       +
       +    def on_channel_announcement(self, msg_payload):
       +        short_channel_id = msg_payload['short_channel_id']
       +        self.print_error('channel announcement', binascii.hexlify(short_channel_id).decode("ascii"))
       +        channel_info = ChannelInfo(msg_payload)
       +        self._id_to_channel_info[short_channel_id] = channel_info
       +        self._channels_for_node[channel_info.node_id_1].add(short_channel_id)
       +        self._channels_for_node[channel_info.node_id_2].add(short_channel_id)
       +
       +    def on_channel_update(self, msg_payload):
       +        short_channel_id = msg_payload['short_channel_id']
       +        try:
       +            channel_info = self._id_to_channel_info[short_channel_id]
       +        except KeyError:
       +            print("could not find", short_channel_id)
       +        else:
       +            channel_info.on_channel_update(msg_payload)
       +
       +    def remove_channel(self, short_channel_id):
       +        try:
       +            channel_info = self._id_to_channel_info[short_channel_id]
       +        except KeyError:
       +            self.print_error('cannot find channel {}'.format(short_channel_id))
       +            return
       +        self._id_to_channel_info.pop(short_channel_id, None)
       +        for node in (channel_info.node_id_1, channel_info.node_id_2):
       +            try:
       +                self._channels_for_node[node].remove(short_channel_id)
       +            except KeyError:
       +                pass
       +
       +
       +class RouteEdge:
       +
       +    def __init__(self, node_id: bytes, short_channel_id: bytes,
       +                 channel_policy: ChannelInfoDirectedPolicy):
       +        self.node_id = node_id
       +        self.short_channel_id = short_channel_id
       +        self.channel_policy = channel_policy
       +
       +
       +class LNPathFinder(PrintError):
       +
       +    def __init__(self, channel_db):
       +        self.channel_db = channel_db
       +
       +    def _edge_cost(self, short_channel_id: bytes, start_node: bytes, payment_amt_msat: int) -> float:
       +        """Heuristic cost of going through a channel.
       +        direction: 0 or 1. --- 0 means node_id_1 -> node_id_2
       +        """
       +        channel_info = self.channel_db.get_channel_info(short_channel_id)
       +        if channel_info is None:
       +            return float('inf')
       +
       +        channel_policy = channel_info.get_policy_for_node(start_node)
       +        if channel_policy is None: return float('inf')
       +        cltv_expiry_delta           = channel_policy.cltv_expiry_delta
       +        htlc_minimum_msat           = channel_policy.htlc_minimum_msat
       +        fee_base_msat               = channel_policy.fee_base_msat
       +        fee_proportional_millionths = channel_policy.fee_proportional_millionths
       +        if payment_amt_msat is not None:
       +            if payment_amt_msat < htlc_minimum_msat:
       +                return float('inf')  # payment amount too little
       +            if channel_info.capacity_sat is not None and \
       +                    payment_amt_msat // 1000 > channel_info.capacity_sat:
       +                return float('inf')  # payment amount too large
       +        amt = payment_amt_msat or 50000 * 1000  # guess for typical payment amount
       +        fee_msat = fee_base_msat + amt * fee_proportional_millionths / 1000000
       +        # TODO revise
       +        # paying 10 more satoshis ~ waiting one more block
       +        fee_cost = fee_msat / 1000 / 10
       +        cltv_cost = cltv_expiry_delta
       +        return cltv_cost + fee_cost + 1
       +
       +    @profiler
       +    def find_path_for_payment(self, from_node_id: bytes, to_node_id: bytes,
       +                              amount_msat: int=None) -> Sequence[Tuple[bytes, bytes]]:
       +        """Return a path between from_node_id and to_node_id.
       +
       +        Returns a list of (node_id, short_channel_id) representing a path.
       +        To get from node ret[n][0] to ret[n+1][0], use channel ret[n+1][1];
       +        i.e. an element reads as, "to get to node_id, travel through short_channel_id"
       +        """
       +        if amount_msat is not None: assert type(amount_msat) is int
       +        # TODO find multiple paths??
       +
       +        # run Dijkstra
       +        distance_from_start = defaultdict(lambda: float('inf'))
       +        distance_from_start[from_node_id] = 0
       +        prev_node = {}
       +        nodes_to_explore = queue.PriorityQueue()
       +        nodes_to_explore.put((0, from_node_id))
       +
       +        while nodes_to_explore.qsize() > 0:
       +            dist_to_cur_node, cur_node = nodes_to_explore.get()
       +            if cur_node == to_node_id:
       +                break
       +            if dist_to_cur_node != distance_from_start[cur_node]:
       +                # queue.PriorityQueue does not implement decrease_priority,
       +                # so instead of decreasing priorities, we add items again into the queue.
       +                # so there are duplicates in the queue, that we discard now:
       +                continue
       +            for edge_channel_id in self.channel_db.get_channels_for_node(cur_node):
       +                channel_info = self.channel_db.get_channel_info(edge_channel_id)
       +                node1, node2 = channel_info.node_id_1, channel_info.node_id_2
       +                neighbour = node2 if node1 == cur_node else node1
       +                alt_dist_to_neighbour = distance_from_start[cur_node] \
       +                                        + self._edge_cost(edge_channel_id, cur_node, amount_msat)
       +                if alt_dist_to_neighbour < distance_from_start[neighbour]:
       +                    distance_from_start[neighbour] = alt_dist_to_neighbour
       +                    prev_node[neighbour] = cur_node, edge_channel_id
       +                    nodes_to_explore.put((alt_dist_to_neighbour, neighbour))
       +        else:
       +            return None  # no path found
       +
       +        # backtrack from end to start
       +        cur_node = to_node_id
       +        path = []
       +        while cur_node != from_node_id:
       +            prev_node_id, edge_taken = prev_node[cur_node]
       +            path += [(cur_node, edge_taken)]
       +            cur_node = prev_node_id
       +        path.reverse()
       +        return path
       +
       +    def create_route_from_path(self, path, from_node_id: bytes) -> Sequence[RouteEdge]:
       +        assert type(from_node_id) is bytes
       +        if path is None:
       +            raise Exception('cannot create route from None path')
       +        route = []
       +        prev_node_id = from_node_id
       +        for node_id, short_channel_id in path:
       +            channel_info = self.channel_db.get_channel_info(short_channel_id)
       +            if channel_info is None:
       +                raise Exception('cannot find channel info for short_channel_id: {}'.format(bh2u(short_channel_id)))
       +            channel_policy = channel_info.get_policy_for_node(prev_node_id)
       +            if channel_policy is None:
       +                raise Exception('cannot find channel policy for short_channel_id: {}'.format(bh2u(short_channel_id)))
       +            route.append(RouteEdge(node_id, short_channel_id, channel_policy))
       +            prev_node_id = node_id
       +        return route
       +
       +
       +# bolt 04, "onion"  ----->
       +
       +NUM_MAX_HOPS_IN_PATH = 20
       +HOPS_DATA_SIZE = 1300      # also sometimes called routingInfoSize in bolt-04
       +PER_HOP_FULL_SIZE = 65     # HOPS_DATA_SIZE / 20
       +NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE
       +PER_HOP_HMAC_SIZE = 32
       +
       +
       +class UnsupportedOnionPacketVersion(Exception): pass
       +class InvalidOnionMac(Exception): pass
       +
       +
       +class OnionPerHop:
       +
       +    def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes):
       +        self.short_channel_id = short_channel_id
       +        self.amt_to_forward = amt_to_forward
       +        self.outgoing_cltv_value = outgoing_cltv_value
       +
       +    def to_bytes(self) -> bytes:
       +        ret = self.short_channel_id
       +        ret += self.amt_to_forward
       +        ret += self.outgoing_cltv_value
       +        ret += bytes(12)  # padding
       +        if len(ret) != 32:
       +            raise Exception('unexpected length {}'.format(len(ret)))
       +        return ret
       +
       +    @classmethod
       +    def from_bytes(cls, b: bytes):
       +        if len(b) != 32:
       +            raise Exception('unexpected length {}'.format(len(b)))
       +        return OnionPerHop(
       +            short_channel_id=b[:8],
       +            amt_to_forward=b[8:16],
       +            outgoing_cltv_value=b[16:20]
       +        )
       +
       +
       +class OnionHopsDataSingle:  # called HopData in lnd
       +
       +    def __init__(self, per_hop: OnionPerHop = None):
       +        self.realm = 0
       +        self.per_hop = per_hop
       +        self.hmac = None
       +
       +    def to_bytes(self) -> bytes:
       +        ret = bytes([self.realm])
       +        ret += self.per_hop.to_bytes()
       +        ret += self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE)
       +        if len(ret) != PER_HOP_FULL_SIZE:
       +            raise Exception('unexpected length {}'.format(len(ret)))
       +        return ret
       +
       +    @classmethod
       +    def from_bytes(cls, b: bytes):
       +        if len(b) != PER_HOP_FULL_SIZE:
       +            raise Exception('unexpected length {}'.format(len(b)))
       +        ret = OnionHopsDataSingle()
       +        ret.realm = b[0]
       +        if ret.realm != 0:
       +            raise Exception('only realm 0 is supported')
       +        ret.per_hop = OnionPerHop.from_bytes(b[1:33])
       +        ret.hmac = b[33:]
       +        return ret
       +
       +
       +class OnionPacket:
       +
       +    def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes):
       +        self.version = 0
       +        self.public_key = public_key
       +        self.hops_data = hops_data  # also called RoutingInfo in bolt-04
       +        self.hmac = hmac
       +
       +    def to_bytes(self) -> bytes:
       +        ret = bytes([self.version])
       +        ret += self.public_key
       +        ret += self.hops_data
       +        ret += self.hmac
       +        if len(ret) != 1366:
       +            raise Exception('unexpected length {}'.format(len(ret)))
       +        return ret
       +
       +    @classmethod
       +    def from_bytes(cls, b: bytes):
       +        if len(b) != 1366:
       +            raise Exception('unexpected length {}'.format(len(b)))
       +        version = b[0]
       +        if version != 0:
       +            raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version))
       +        return OnionPacket(
       +            public_key=b[1:34],
       +            hops_data=b[34:1334],
       +            hmac=b[1334:]
       +        )
       +
       +
       +def get_bolt04_onion_key(key_type: bytes, secret: bytes) -> bytes:
       +    if key_type not in (b'rho', b'mu', b'um', b'ammag'):
       +        raise Exception('invalid key_type {}'.format(key_type))
       +    key = hmac.new(key_type, msg=secret, digestmod=hashlib.sha256).digest()
       +    return key
       +
       +
       +def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes],
       +                                   session_key: bytes) -> Sequence[bytes]:
       +    num_hops = len(payment_path_pubkeys)
       +    hop_shared_secrets = num_hops * [b'']
       +    ephemeral_key = session_key
       +    # compute shared key for each hop
       +    for i in range(0, num_hops):
       +        hop_shared_secrets[i] = lnbase.get_ecdh(ephemeral_key, payment_path_pubkeys[i])
       +        ephemeral_pubkey = ecc.ECPrivkey(ephemeral_key).get_public_key_bytes()
       +        blinding_factor = sha256(ephemeral_pubkey + hop_shared_secrets[i])
       +        blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big")
       +        ephemeral_key_int = int.from_bytes(ephemeral_key, byteorder="big")
       +        ephemeral_key_int = ephemeral_key_int * blinding_factor_int % ecc.CURVE_ORDER
       +        ephemeral_key = ephemeral_key_int.to_bytes(32, byteorder="big")
       +    return hop_shared_secrets
       +
       +
       +def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
       +                     hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket:
       +    num_hops = len(payment_path_pubkeys)
       +    hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
       +
       +    filler = generate_filler(b'rho', num_hops, PER_HOP_FULL_SIZE, hop_shared_secrets)
       +    mix_header = bytes(HOPS_DATA_SIZE)
       +    next_hmac = bytes(PER_HOP_HMAC_SIZE)
       +
       +    # compute routing info and MAC for each hop
       +    for i in range(num_hops-1, -1, -1):
       +        rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])
       +        mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i])
       +        hops_data[i].hmac = next_hmac
       +        stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES)
       +        mix_header = mix_header[:-PER_HOP_FULL_SIZE]
       +        mix_header = hops_data[i].to_bytes() + mix_header
       +        mix_header = xor_bytes(mix_header, stream_bytes)
       +        if i == num_hops - 1 and len(filler) != 0:
       +            mix_header = mix_header[:-len(filler)] + filler
       +        packet = mix_header + associated_data
       +        next_hmac = hmac.new(mu_key, msg=packet, digestmod=hashlib.sha256).digest()
       +
       +    return OnionPacket(
       +        public_key=ecc.ECPrivkey(session_key).get_public_key_bytes(),
       +        hops_data=mix_header,
       +        hmac=next_hmac)
       +
       +
       +def generate_filler(key_type: bytes, num_hops: int, hop_size: int,
       +                    shared_secrets: Sequence[bytes]) -> bytes:
       +    filler_size = (NUM_MAX_HOPS_IN_PATH + 1) * hop_size
       +    filler = bytearray(filler_size)
       +
       +    for i in range(0, num_hops-1):  # -1, as last hop does not obfuscate
       +        filler = filler[hop_size:]
       +        filler += bytearray(hop_size)
       +        stream_key = get_bolt04_onion_key(key_type, shared_secrets[i])
       +        stream_bytes = generate_cipher_stream(stream_key, filler_size)
       +        filler = xor_bytes(filler, stream_bytes)
       +
       +    return filler[(NUM_MAX_HOPS_IN_PATH-num_hops+2)*hop_size:]
       +
       +
       +def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes:
       +    algo = algorithms.ChaCha20(stream_key, nonce=bytes(16))
       +    cipher = Cipher(algo, mode=None, backend=default_backend())
       +    encryptor = cipher.encryptor()
       +    return encryptor.update(bytes(num_bytes))
       +
       +
       +ProcessedOnionPacket = namedtuple("ProcessedOnionPacket", ["are_we_final", "hop_data", "next_packet"])
       +
       +
       +# TODO replay protection
       +def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
       +                         our_onion_private_key: bytes) -> ProcessedOnionPacket:
       +    shared_secret = lnbase.get_ecdh(our_onion_private_key, onion_packet.public_key)
       +
       +    # check message integrity
       +    mu_key = get_bolt04_onion_key(b'mu', shared_secret)
       +    calculated_mac = hmac.new(mu_key, msg=onion_packet.hops_data+associated_data,
       +                              digestmod=hashlib.sha256).digest()
       +    if onion_packet.hmac != calculated_mac:
       +        raise InvalidOnionMac()
       +
       +    # peel an onion layer off
       +    rho_key = get_bolt04_onion_key(b'rho', shared_secret)
       +    stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES)
       +    padded_header = onion_packet.hops_data + bytes(PER_HOP_FULL_SIZE)
       +    next_hops_data = xor_bytes(padded_header, stream_bytes)
       +
       +    # calc next ephemeral key
       +    blinding_factor = sha256(onion_packet.public_key + shared_secret)
       +    blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big")
       +    next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int
       +    next_public_key = next_public_key_int.get_public_key_bytes()
       +
       +    hop_data = OnionHopsDataSingle.from_bytes(next_hops_data[:PER_HOP_FULL_SIZE])
       +    next_onion_packet = OnionPacket(
       +        public_key=next_public_key,
       +        hops_data=next_hops_data[PER_HOP_FULL_SIZE:],
       +        hmac=hop_data.hmac
       +    )
       +    if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):
       +        # we are the destination / exit node
       +        are_we_final = True
       +    else:
       +        # we are an intermediate node; forwarding
       +        are_we_final = False
       +    return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet)
       +
       +
       +class FailedToDecodeOnionError(Exception): pass
       +
       +
       +class OnionRoutingFailureMessage:
       +
       +    def __init__(self, code: int, data: bytes):
       +        self.code = code
       +        self.data = data
       +
       +    def __repr__(self):
       +        return repr((self.code, self.data))
       +
       +
       +def _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
       +                        session_key: bytes) -> (bytes, int):
       +    """Returns the decoded error bytes, and the index of the sender of the error."""
       +    num_hops = len(payment_path_pubkeys)
       +    hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
       +    for i in range(num_hops):
       +        ammag_key = get_bolt04_onion_key(b'ammag', hop_shared_secrets[i])
       +        um_key = get_bolt04_onion_key(b'um', hop_shared_secrets[i])
       +        stream_bytes = generate_cipher_stream(ammag_key, len(error_packet))
       +        error_packet = xor_bytes(error_packet, stream_bytes)
       +        hmac_computed = hmac.new(um_key, msg=error_packet[32:], digestmod=hashlib.sha256).digest()
       +        hmac_found = error_packet[:32]
       +        if hmac_computed == hmac_found:
       +            return error_packet, i
       +    raise FailedToDecodeOnionError()
       +
       +
       +def decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
       +                       session_key: bytes) -> (OnionRoutingFailureMessage, int):
       +    """Returns the failure message, and the index of the sender of the error."""
       +    decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key)
       +    failure_msg = get_failure_msg_from_onion_error(decrypted_error)
       +    return failure_msg, sender_index
       +
       +
       +def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailureMessage:
       +    # get failure_msg bytes from error packet
       +    failure_len = int.from_bytes(decrypted_error_packet[32:34], byteorder='big')
       +    failure_msg = decrypted_error_packet[34:34+failure_len]
       +    # create failure message object
       +    failure_code = int.from_bytes(failure_msg[:2], byteorder='big')
       +    failure_data = failure_msg[2:]
       +    return OnionRoutingFailureMessage(failure_code, failure_data)
       +
       +
       +# <----- bolt 04, "onion"
       +
   DIR diff --git a/lib/lnworker.py b/lib/lnworker.py
       t@@ -10,7 +10,6 @@ import binascii
        import asyncio
        
        
       -from .lnbase import Peer, H256
        from .bitcoin import sha256, COIN
        from .util import bh2u, bfh
        from .constants import set_testnet, set_simnet
       t@@ -20,6 +19,7 @@ from .storage import WalletStorage
        from .wallet import Wallet
        from .lnbase import Peer, Outpoint, ChannelConfig, LocalState, RemoteState, Keypair, OnlyPubkeyKeypair, OpenChannel, ChannelConstraints, RevocationStore, aiosafe
        from .lightning_payencode.lnaddr import lnencode, LnAddr, lndecode
       +from . import lnrouter
        
        
        is_key = lambda k: k.endswith("_basepoint") or k.endswith("_key")
       t@@ -90,9 +90,14 @@ class LNWorker:
            def __init__(self, wallet, network):
                self.wallet = wallet
                self.network = network
       -        self.privkey = H256(b"0123456789")
       +        self.privkey = sha256(b"0123456789")
                self.config = network.config
                self.peers = {}
       +        # view of the network
       +        self.nodes = {}  # received node announcements
       +        self.channel_db = lnrouter.ChannelDB()
       +        self.path_finder = lnrouter.LNPathFinder(self.channel_db)
       +
                self.channels = wallet.storage.get("channels", {})
                peer_list = network.config.get('lightning_peers', node_list)
                for host, port, pubkey in peer_list:
       t@@ -102,7 +107,8 @@ class LNWorker:
                self.on_network_update('updated') # shortcut (don't block) if funding tx locked and verified
        
            def add_peer(self, host, port, pubkey):
       -        peer = Peer(host, int(port), binascii.unhexlify(pubkey), self.privkey, self.network)
       +        peer = Peer(host, int(port), binascii.unhexlify(pubkey), self.privkey,
       +                    self.network, self.channel_db, self.path_finder)
                self.network.futures.append(asyncio.run_coroutine_threadsafe(peer.main_loop(), asyncio.get_event_loop()))
                self.peers[pubkey] = peer
        
   DIR diff --git a/lib/tests/test_lnbase.py b/lib/tests/test_lnbase.py
       t@@ -13,7 +13,7 @@ from lib import bitcoin
        import ecdsa.ellipticcurve
        from ecdsa.curves import SECP256k1
        from lib.util import bfh
       -from lib import bitcoin, lnbase
       +from lib import bitcoin, lnbase, lnrouter
        
        funding_tx_id = '8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be'
        funding_output_index = 0
       t@@ -254,7 +254,9 @@ class Test_LNBase(unittest.TestCase):
                             success=True, cltv_timeout=0)
        
            def test_find_path_for_payment(self):
       -        p = Peer('', 0, 'a', bitcoin.sha256('privkeyseed'), None)
       +        channel_db = lnrouter.ChannelDB()
       +        path_finder = lnrouter.LNPathFinder(channel_db)
       +        p = Peer('', 0, 'a', bitcoin.sha256('privkeyseed'), None, channel_db, path_finder)
                p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'c', 'short_channel_id': bfh('0000000000000001')})
                p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'e', 'short_channel_id': bfh('0000000000000002')})
                p.on_channel_announcement({'node_id_1': b'a', 'node_id_2': b'b', 'short_channel_id': bfh('0000000000000003')})
       t@@ -274,7 +276,7 @@ class Test_LNBase(unittest.TestCase):
                p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(999)})
                p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(99999999)})
                p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
       -        print(p.path_finder.find_path_for_payment('a', 'e', 100000))
       +        self.assertNotEqual(None, p.path_finder.find_path_for_payment(b'a', b'e', 100000))
        
            def test_key_derivation(self):
                # BOLT3, Appendix E
       t@@ -378,7 +380,7 @@ class Test_LNBase(unittest.TestCase):
parazyd.org:70 /git/electrum/commit/8ba63380b4b3fe7d79432c9d20cb3ba8faf65d22.gph:1134: line too long