diff --git a/electrum/lnbase.py b/electrum/lnbase.py
index 6f36ecd70..65909fd92 100644
--- a/electrum/lnbase.py
+++ b/electrum/lnbase.py
@@ -31,7 +31,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, decode_onion_error
+from .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error
 from .lnaddr import lndecode
 from .lnhtlc import UpdateAddHtlc, HTLCStateMachine, RevokeAndAck, SettleHtlc
 
diff --git a/electrum/lnonion.py b/electrum/lnonion.py
new file mode 100644
index 000000000..5e8c3fc66
--- /dev/null
+++ b/electrum/lnonion.py
@@ -0,0 +1,299 @@
+# -*- 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 hashlib
+import hmac
+from collections import namedtuple
+from typing import Sequence
+
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
+from cryptography.hazmat.backends import default_backend
+
+from . import ecc
+from .crypto import sha256
+from .util import bh2u, profiler, xor_bytes, bfh
+from .lnutil import get_ecdh
+
+
+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 % 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 = 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)
diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py
index dd2f6ff1a..23e30ad8f 100644
--- a/electrum/lnrouter.py
+++ b/electrum/lnrouter.py
@@ -23,29 +23,16 @@
 # 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
 import os
 import json
 import threading
 from collections import namedtuple, defaultdict
 from typing import Sequence, Union, Tuple, Optional
 
-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 . import constants
-from .crypto import sha256
-from .util import PrintError, bh2u, profiler, xor_bytes, get_headers_dir, bfh
-from .lnutil import get_ecdh
+from .util import PrintError, bh2u, profiler, get_headers_dir, bfh
 from .storage import JsonDB
 from .lnchanannverifier import LNChanAnnVerifier, verify_sig_for_channel_update
 
@@ -424,273 +411,3 @@ class LNPathFinder(PrintError):
             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 % 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 = 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"
-
diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py
index 53bd6a33b..1da5d0f58 100644
--- a/electrum/tests/test_lnrouter.py
+++ b/electrum/tests/test_lnrouter.py
@@ -4,7 +4,8 @@ import shutil
 
 from electrum.util import bh2u, bfh
 from electrum.lnbase import Peer
-from electrum.lnrouter import OnionHopsDataSingle, new_onion_packet, OnionPerHop
+from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet, OnionPerHop,
+                              process_onion_packet, _decode_onion_error)
 from electrum import bitcoin, lnrouter
 from electrum.simple_config import SimpleConfig
 from electrum import lnchanannverifier
@@ -164,7 +165,7 @@ class Test_LNRouter(TestCaseForTestnet):
         self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661954176cd9869da33d713aa219fcef1e5c806fef11e696bcc66844de8271c27974a0fd57c2dbcb2c6dd4e8ef35d96db28d5a0e49b6ab3d6de31af65950723b8cddc108390bebf8d149002e31bdc283056477ba27c8054c248ad7306de31663a7c99ec659da15d0f6fbc7e1687485b39e9be0ec3b70164cb3618a9b546317e7c2d62ae9f0f840704535729262d30c6132d1b390f073edec8fa057176c6268b6ad06a82ff0d16d4c662194873e8b4ecf46eb2c9d4d58d2ee2021adb19840605ac5afd8bd942dd71e8244c83e28b2ed5a3b09e9e7df5c8c747e5765ba366a4f7407a6c6b0a32f74bc5e428f7fa4c3cf70e13ed91563177d94190d5149aa4b9c96d00e40d2ac35ab9c4a621ce0f6f5df7d64a9c8d435db19de192d9db522c7f7b4e201fc1b61a9bd3efd062ae24455d463818b01e2756c7d0691bc3ac4c017be34c9a8b2913bb1b937e31e0ae40f650a7cd820bcb4996825b1cbad1ff7ccc2b513b1104524c34f6573e1b59201c005a632ee5dccd3711a32e3ba1ff00fcffbe636e4b3a84bbe491b836a57ccec138b8cc2ec733846904d872f305d538d51db8e56232ec6e07877075328874cb7b09c7e799100a9ff085dead253886b174fc408a0ea7b48bce2c5d8992285011960af088f7e006ef60089d46ac9aa15acfac6c87c3cf6904764dd785419292fbafa9cca09c8ade24a6cd63f12d1cfc83fa35cf2f1cf503c39cbf78293f06c68a3cece7177169cd872bb49bf69d933a27a887dd9daefa9239fca9f0c3e309ec61d9df947211da98cf11a6e0fb77252629cdf9f2226dd69ca73fa51be4df224592f8d471b69a1aebbdaa2f3a798b3581253d97feb0a12e6606043ca0fc5efc0f49b8061d6796eff31cd8638499e2f25ffb96eec32837438ed7ebebbe587886648f63e35d80f41869f4c308f2e6970bd65fead5e8544e3239a6acc9d996b08d1546455bcafbe88ed3ed547714841946fe2e77180e4d7bf1452414e4b1745a7897184a2c4cbc3ac46f83342a55a48e29dc8f17cf595dd28f51e297ba89fd25ed0dbd1c0081a810beaab09758a36fbfd16fbdc3daa9fe05c8a73195f244ef2743a5df761f01ee6e693eb6c7f1a7834fab3671391e5ddebf611e119a2ae4456e2cee7a6d4f27a2246cdb1f8ef35f0b3d7044b3799d8d0ed0a6470557fd807c065d6d83acba07e96e10770ada8c0b4d4921522944188d5f30086a6ee0a4795331273f32beaaa43363fc58208a257e5c5c434c7325b583642219d81c7d67b908d5263b42ac1991edc69a777da60f38eff138c844af9e549374e8b29b166211bfded24587a29394e33828b784da7e7b62ab7e49ea2693fcdd17fa96186a5ef11ef1a8adffa50f93a3119e95e6c09014f3e3b0709183fa08a826ced6deb4608b7d986ebbcf99ad58e25451d4d9d38d0059734d8501467b97182cd11e0c07c91ca50f61cc31255a3147ade654976a5989097281892aafd8df595c63bd14f1e03f5955a9398d2dd6368bbcae833ae1cc2df31eb0980b4817dfd130020ffb275743fcc01df40e3ecda1c5988e8e1bde965353b0b1bf34ea05f095000c45b6249618d275905a24d3eb58c600aeab4fb552fbf1ccdb2a5c80ace220310f89829d7e53f78c126037b6d8d500220c7a118d9621b4d6bd5379edd7e24bcf540e87aba6b88862db16fa4ee00b009fda80577be67ab94910fd8a7807dfe4ebe66b8fdcd040aa2dc17ec22639298be56b2a2c9d8940647b75f2f6d81746df16e1cb2f05e23397a8c63baea0803441ff4b7d517ff172980a056726235e2f6af85e8aa9b91ba85f14532272d6170df3166b91169dc09d4f4a251610f57ff0885a93364cfaf650bdf436c89795efed5ca934bc7ffc0a4'),
                          packet.to_bytes())
         for i, privkey in enumerate(payment_path_privkeys):
-            processed_packet = lnrouter.process_onion_packet(packet, associated_data, privkey)
+            processed_packet = process_onion_packet(packet, associated_data, privkey)
             self.assertEqual(hops_data[i].per_hop.to_bytes(), processed_packet.hop_data.per_hop.to_bytes())
             packet = processed_packet.next_packet
 
@@ -179,7 +180,7 @@ class Test_LNRouter(TestCaseForTestnet):
         ]
         session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
         error_packet_for_node_0 = bfh('9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d')
-        decoded_error, index_of_sender = lnrouter._decode_onion_error(error_packet_for_node_0, payment_path_pubkeys, session_key)
+        decoded_error, index_of_sender = _decode_onion_error(error_packet_for_node_0, payment_path_pubkeys, session_key)
         self.assertEqual(bfh('4c2fc8bc08510334b6833ad9c3e79cd1b52ae59dfe5c2a4b23ead50f09f7ee0b0002200200fe0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
                              decoded_error)
         self.assertEqual(4, index_of_sender)