URI: 
       tMerge pull request #6050 from SomberNight/202003_lnmsg_rewrite - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 158854f94e4405243cc9cf4c94c76665de7db571
   DIR parent 371f55a0f922cf57196ae3d8d7c2fabc7a9f0f08
  HTML Author: ghost43 <somber.night@protonmail.com>
       Date:   Wed,  1 Apr 2020 19:51:23 +0000
       
       Merge pull request #6050 from SomberNight/202003_lnmsg_rewrite
       
       lnmsg rewrite, implement TLV, invoice features, varonion, payment secret
       Diffstat:
         M electrum/channel_db.py              |      54 ++++++++++++++++----------------
         D electrum/lightning.json             |     903 ------------------------------
         M electrum/lnaddr.py                  |      67 ++++++++++++++++++++++++-------
         M electrum/lnchannel.py               |      17 +++++++++--------
         M electrum/lnmsg.py                   |     604 ++++++++++++++++++++++++-------
         M electrum/lnonion.py                 |     210 ++++++++++++++++++++++---------
         M electrum/lnpeer.py                  |     161 ++++++++++++++++++++-----------
         M electrum/lnrouter.py                |      48 ++++++++++++++++++++-----------
         M electrum/lnutil.py                  |     189 ++++++++++++++++++++++++++++---
         A electrum/lnwire/README.md           |       5 +++++
         A electrum/lnwire/onion_wire.csv      |      53 ++++++++++++++++++++++++++++++
         A electrum/lnwire/peer_wire.csv       |     210 +++++++++++++++++++++++++++++++
         M electrum/lnworker.py                |      59 +++++++++++++++++++++----------
         M electrum/tests/test_bolt11.py       |      60 ++++++++++++++++++++++++--------
         A electrum/tests/test_lnmsg.py        |     385 +++++++++++++++++++++++++++++++
         M electrum/tests/test_lnpeer.py       |      28 +++++++++++++++++-----------
         M electrum/tests/test_lnrouter.py     |     216 +++++++++++++++++++++++--------
         M electrum/tests/test_lnutil.py       |      52 ++++++++++++++++++++++++++++++-
       
       18 files changed, 2001 insertions(+), 1320 deletions(-)
       ---
   DIR diff --git a/electrum/channel_db.py b/electrum/channel_db.py
       t@@ -38,7 +38,8 @@ from .sql_db import SqlDB, sql
        from . import constants
        from .util import bh2u, profiler, get_headers_dir, bfh, is_ip_address, list_enabled_bits
        from .logging import Logger
       -from .lnutil import LN_GLOBAL_FEATURES_KNOWN_SET, LNPeerAddr, format_short_channel_id, ShortChannelID
       +from .lnutil import (LNPeerAddr, format_short_channel_id, ShortChannelID,
       +                     validate_features, IncompatibleOrInsaneFeatures)
        from .lnverifier import LNChannelVerifier, verify_sig_for_channel_update
        from .lnmsg import decode_msg
        
       t@@ -47,15 +48,6 @@ if TYPE_CHECKING:
            from .lnchannel import Channel
        
        
       -class UnknownEvenFeatureBits(Exception): pass
       -
       -def validate_features(features : int):
       -    enabled_features = list_enabled_bits(features)
       -    for fbit in enabled_features:
       -        if (1 << fbit) not in LN_GLOBAL_FEATURES_KNOWN_SET and fbit % 2 == 0:
       -            raise UnknownEvenFeatureBits()
       -
       -
        FLAG_DISABLE   = 1 << 1
        FLAG_DIRECTION = 1 << 0
        
       t@@ -102,14 +94,14 @@ class Policy(NamedTuple):
            def from_msg(payload: dict) -> 'Policy':
                return Policy(
                    key                         = payload['short_channel_id'] + payload['start_node'],
       -            cltv_expiry_delta           = int.from_bytes(payload['cltv_expiry_delta'], "big"),
       -            htlc_minimum_msat           = int.from_bytes(payload['htlc_minimum_msat'], "big"),
       -            htlc_maximum_msat           = int.from_bytes(payload['htlc_maximum_msat'], "big") if 'htlc_maximum_msat' in payload else None,
       -            fee_base_msat               = int.from_bytes(payload['fee_base_msat'], "big"),
       -            fee_proportional_millionths = int.from_bytes(payload['fee_proportional_millionths'], "big"),
       +            cltv_expiry_delta           = payload['cltv_expiry_delta'],
       +            htlc_minimum_msat           = payload['htlc_minimum_msat'],
       +            htlc_maximum_msat           = payload.get('htlc_maximum_msat', None),
       +            fee_base_msat               = payload['fee_base_msat'],
       +            fee_proportional_millionths = payload['fee_proportional_millionths'],
                    message_flags               = int.from_bytes(payload['message_flags'], "big"),
                    channel_flags               = int.from_bytes(payload['channel_flags'], "big"),
       -            timestamp                   = int.from_bytes(payload['timestamp'], "big")
       +            timestamp                   = payload['timestamp'],
                )
        
            @staticmethod
       t@@ -154,7 +146,7 @@ class NodeInfo(NamedTuple):
                    alias = alias.decode('utf8')
                except:
                    alias = ''
       -        timestamp = int.from_bytes(payload['timestamp'], "big")
       +        timestamp = payload['timestamp']
                node_info = NodeInfo(node_id=node_id, features=features, timestamp=timestamp, alias=alias)
                return node_info, peer_addrs
        
       t@@ -321,11 +313,12 @@ class ChannelDB(SqlDB):
                    return ret
        
            # note: currently channel announcements are trusted by default (trusted=True);
       -    #       they are not verified. Verifying them would make the gossip sync
       +    #       they are not SPV-verified. Verifying them would make the gossip sync
            #       even slower; especially as servers will start throttling us.
            #       It would probably put significant strain on servers if all clients
            #       verified the complete gossip.
            def add_channel_announcement(self, msg_payloads, *, trusted=True):
       +        # note: signatures have already been verified.
                if type(msg_payloads) is dict:
                    msg_payloads = [msg_payloads]
                added = 0
       t@@ -338,8 +331,8 @@ class ChannelDB(SqlDB):
                        continue
                    try:
                        channel_info = ChannelInfo.from_msg(msg)
       -            except UnknownEvenFeatureBits:
       -                self.logger.info("unknown feature bits")
       +            except IncompatibleOrInsaneFeatures as e:
       +                self.logger.info(f"unknown or insane feature bits: {e!r}")
                        continue
                    if trusted:
                        added += 1
       t@@ -353,7 +346,7 @@ class ChannelDB(SqlDB):
            def add_verified_channel_info(self, msg: dict, *, capacity_sat: int = None) -> None:
                try:
                    channel_info = ChannelInfo.from_msg(msg)
       -        except UnknownEvenFeatureBits:
       +        except IncompatibleOrInsaneFeatures:
                    return
                channel_info = channel_info._replace(capacity_sat=capacity_sat)
                with self.lock:
       t@@ -392,7 +385,7 @@ class ChannelDB(SqlDB):
                now = int(time.time())
                for payload in payloads:
                    short_channel_id = ShortChannelID(payload['short_channel_id'])
       -            timestamp = int.from_bytes(payload['timestamp'], "big")
       +            timestamp = payload['timestamp']
                    if max_age and now - timestamp > max_age:
                        expired.append(payload)
                        continue
       t@@ -407,7 +400,7 @@ class ChannelDB(SqlDB):
                    known.append(payload)
                # compare updates to existing database entries
                for payload in known:
       -            timestamp = int.from_bytes(payload['timestamp'], "big")
       +            timestamp = payload['timestamp']
                    start_node = payload['start_node']
                    short_channel_id = ShortChannelID(payload['short_channel_id'])
                    key = (start_node, short_channel_id)
       t@@ -499,13 +492,14 @@ class ChannelDB(SqlDB):
                    raise Exception(f'failed verifying channel update for {short_channel_id}')
        
            def add_node_announcement(self, msg_payloads):
       +        # note: signatures have already been verified.
                if type(msg_payloads) is dict:
                    msg_payloads = [msg_payloads]
                new_nodes = {}
                for msg_payload in msg_payloads:
                    try:
                        node_info, node_addresses = NodeInfo.from_msg(msg_payload)
       -            except UnknownEvenFeatureBits:
       +            except IncompatibleOrInsaneFeatures:
                        continue
                    node_id = node_info.node_id
                    # Ignore node if it has no associated channel (DoS protection)
       t@@ -599,11 +593,17 @@ class ChannelDB(SqlDB):
                self._recent_peers = sorted_node_ids[:self.NUM_MAX_RECENT_PEERS]
                c.execute("""SELECT * FROM channel_info""")
                for short_channel_id, msg in c:
       -            ci = ChannelInfo.from_raw_msg(msg)
       +            try:
       +                ci = ChannelInfo.from_raw_msg(msg)
       +            except IncompatibleOrInsaneFeatures:
       +                continue
                    self._channels[ShortChannelID.normalize(short_channel_id)] = ci
                c.execute("""SELECT * FROM node_info""")
                for node_id, msg in c:
       -            node_info, node_addresses = NodeInfo.from_raw_msg(msg)
       +            try:
       +                node_info, node_addresses = NodeInfo.from_raw_msg(msg)
       +            except IncompatibleOrInsaneFeatures:
       +                continue
                    # don't load node_addresses because they dont have timestamps
                    self._nodes[node_id] = node_info
                c.execute("""SELECT * FROM policy""")
       t@@ -671,7 +671,7 @@ class ChannelDB(SqlDB):
                        return
                    now = int(time.time())
                    remote_update_decoded = decode_msg(remote_update_raw)[1]
       -            remote_update_decoded['timestamp'] = now.to_bytes(4, byteorder="big")
       +            remote_update_decoded['timestamp'] = now
                    remote_update_decoded['start_node'] = node_id
                    return Policy.from_msg(remote_update_decoded)
                elif node_id == chan.get_local_pubkey():  # outgoing direction (from us)
   DIR diff --git a/electrum/lightning.json b/electrum/lightning.json
       t@@ -1,903 +0,0 @@
       -{
       - "init": {
       -  "type": "16",
       -  "payload": {
       -   "gflen": {
       -    "position": "0",
       -    "length": "2"
       -   },
       -   "globalfeatures": {
       -    "position": "2",
       -    "length": "gflen"
       -   },
       -   "lflen": {
       -    "position": "2+gflen",
       -    "length": "2"
       -   },
       -   "localfeatures": {
       -    "position": "4+gflen",
       -    "length": "lflen"
       -   }
       -  }
       - },
       - "error": {
       -  "type": "17",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "len": {
       -    "position": "32",
       -    "length": "2"
       -   },
       -   "data": {
       -    "position": "34",
       -    "length": "len"
       -   }
       -  }
       - },
       - "ping": {
       -  "type": "18",
       -  "payload": {
       -   "num_pong_bytes": {
       -    "position": "0",
       -    "length": "2"
       -   },
       -   "byteslen": {
       -    "position": "2",
       -    "length": "2"
       -   },
       -   "ignored": {
       -    "position": "4",
       -    "length": "byteslen"
       -   }
       -  }
       - },
       - "pong": {
       -  "type": "19",
       -  "payload": {
       -   "byteslen": {
       -    "position": "0",
       -    "length": "2"
       -   },
       -   "ignored": {
       -    "position": "2",
       -    "length": "byteslen"
       -   }
       -  }
       - },
       - "open_channel": {
       -  "type": "32",
       -  "payload": {
       -   "chain_hash": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "temporary_channel_id": {
       -    "position": "32",
       -    "length": "32"
       -   },
       -   "funding_satoshis": {
       -    "position": "64",
       -    "length": "8"
       -   },
       -   "push_msat": {
       -    "position": "72",
       -    "length": "8"
       -   },
       -   "dust_limit_satoshis": {
       -    "position": "80",
       -    "length": "8"
       -   },
       -   "max_htlc_value_in_flight_msat": {
       -    "position": "88",
       -    "length": "8"
       -   },
       -   "channel_reserve_satoshis": {
       -    "position": "96",
       -    "length": "8"
       -   },
       -   "htlc_minimum_msat": {
       -    "position": "104",
       -    "length": "8"
       -   },
       -   "feerate_per_kw": {
       -    "position": "112",
       -    "length": "4"
       -   },
       -   "to_self_delay": {
       -    "position": "116",
       -    "length": "2"
       -   },
       -   "max_accepted_htlcs": {
       -    "position": "118",
       -    "length": "2"
       -   },
       -   "funding_pubkey": {
       -    "position": "120",
       -    "length": "33"
       -   },
       -   "revocation_basepoint": {
       -    "position": "153",
       -    "length": "33"
       -   },
       -   "payment_basepoint": {
       -    "position": "186",
       -    "length": "33"
       -   },
       -   "delayed_payment_basepoint": {
       -    "position": "219",
       -    "length": "33"
       -   },
       -   "htlc_basepoint": {
       -    "position": "252",
       -    "length": "33"
       -   },
       -   "first_per_commitment_point": {
       -    "position": "285",
       -    "length": "33"
       -   },
       -   "channel_flags": {
       -    "position": "318",
       -    "length": "1"
       -   },
       -   "shutdown_len": {
       -    "position": "319",
       -    "length": "2",
       -    "feature": "option_upfront_shutdown_script"
       -   },
       -   "shutdown_scriptpubkey": {
       -    "position": "321",
       -    "length": "shutdown_len",
       -    "feature": "option_upfront_shutdown_script"
       -   }
       -  }
       - },
       - "accept_channel": {
       -  "type": "33",
       -  "payload": {
       -   "temporary_channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "dust_limit_satoshis": {
       -    "position": "32",
       -    "length": "8"
       -   },
       -   "max_htlc_value_in_flight_msat": {
       -    "position": "40",
       -    "length": "8"
       -   },
       -   "channel_reserve_satoshis": {
       -    "position": "48",
       -    "length": "8"
       -   },
       -   "htlc_minimum_msat": {
       -    "position": "56",
       -    "length": "8"
       -   },
       -   "minimum_depth": {
       -    "position": "64",
       -    "length": "4"
       -   },
       -   "to_self_delay": {
       -    "position": "68",
       -    "length": "2"
       -   },
       -   "max_accepted_htlcs": {
       -    "position": "70",
       -    "length": "2"
       -   },
       -   "funding_pubkey": {
       -    "position": "72",
       -    "length": "33"
       -   },
       -   "revocation_basepoint": {
       -    "position": "105",
       -    "length": "33"
       -   },
       -   "payment_basepoint": {
       -    "position": "138",
       -    "length": "33"
       -   },
       -   "delayed_payment_basepoint": {
       -    "position": "171",
       -    "length": "33"
       -   },
       -   "htlc_basepoint": {
       -    "position": "204",
       -    "length": "33"
       -   },
       -   "first_per_commitment_point": {
       -    "position": "237",
       -    "length": "33"
       -   },
       -   "shutdown_len": {
       -    "position": "270",
       -    "length": "2",
       -    "feature": "option_upfront_shutdown_script"
       -   },
       -   "shutdown_scriptpubkey": {
       -    "position": "272",
       -    "length": "shutdown_len",
       -    "feature": "option_upfront_shutdown_script"
       -   }
       -  }
       - },
       - "funding_created": {
       -  "type": "34",
       -  "payload": {
       -   "temporary_channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "funding_txid": {
       -    "position": "32",
       -    "length": "32"
       -   },
       -   "funding_output_index": {
       -    "position": "64",
       -    "length": "2"
       -   },
       -   "signature": {
       -    "position": "66",
       -    "length": "64"
       -   }
       -  }
       - },
       - "funding_signed": {
       -  "type": "35",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "signature": {
       -    "position": "32",
       -    "length": "64"
       -   }
       -  }
       - },
       - "funding_locked": {
       -  "type": "36",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "next_per_commitment_point": {
       -    "position": "32",
       -    "length": "33"
       -   }
       -  }
       - },
       - "shutdown": {
       -  "type": "38",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "len": {
       -    "position": "32",
       -    "length": "2"
       -   },
       -   "scriptpubkey": {
       -    "position": "34",
       -    "length": "len"
       -   }
       -  }
       - },
       - "closing_signed": {
       -  "type": "39",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "fee_satoshis": {
       -    "position": "32",
       -    "length": "8"
       -   },
       -   "signature": {
       -    "position": "40",
       -    "length": "64"
       -   }
       -  }
       - },
       - "update_add_htlc": {
       -  "type": "128",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "id": {
       -    "position": "32",
       -    "length": "8"
       -   },
       -   "amount_msat": {
       -    "position": "40",
       -    "length": "8"
       -   },
       -   "payment_hash": {
       -    "position": "48",
       -    "length": "32"
       -   },
       -   "cltv_expiry": {
       -    "position": "80",
       -    "length": "4"
       -   },
       -   "onion_routing_packet": {
       -    "position": "84",
       -    "length": "1366"
       -   }
       -  }
       - },
       - "update_fulfill_htlc": {
       -  "type": "130",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "id": {
       -    "position": "32",
       -    "length": "8"
       -   },
       -   "payment_preimage": {
       -    "position": "40",
       -    "length": "32"
       -   }
       -  }
       - },
       - "update_fail_htlc": {
       -  "type": "131",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "id": {
       -    "position": "32",
       -    "length": "8"
       -   },
       -   "len": {
       -    "position": "40",
       -    "length": "2"
       -   },
       -   "reason": {
       -    "position": "42",
       -    "length": "len"
       -   }
       -  }
       - },
       - "update_fail_malformed_htlc": {
       -  "type": "135",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "id": {
       -    "position": "32",
       -    "length": "8"
       -   },
       -   "sha256_of_onion": {
       -    "position": "40",
       -    "length": "32"
       -   },
       -   "failure_code": {
       -    "position": "72",
       -    "length": "2"
       -   }
       -  }
       - },
       - "commitment_signed": {
       -  "type": "132",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "signature": {
       -    "position": "32",
       -    "length": "64"
       -   },
       -   "num_htlcs": {
       -    "position": "96",
       -    "length": "2"
       -   },
       -   "htlc_signature": {
       -    "position": "98",
       -    "length": "num_htlcs*64"
       -   }
       -  }
       - },
       - "revoke_and_ack": {
       -  "type": "133",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "per_commitment_secret": {
       -    "position": "32",
       -    "length": "32"
       -   },
       -   "next_per_commitment_point": {
       -    "position": "64",
       -    "length": "33"
       -   }
       -  }
       - },
       - "update_fee": {
       -  "type": "134",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "feerate_per_kw": {
       -    "position": "32",
       -    "length": "4"
       -   }
       -  }
       - },
       - "channel_reestablish": {
       -  "type": "136",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "next_local_commitment_number": {
       -    "position": "32",
       -    "length": "8"
       -   },
       -   "next_remote_revocation_number": {
       -    "position": "40",
       -    "length": "8"
       -   },
       -   "your_last_per_commitment_secret": {
       -    "position": "48",
       -    "length": "32",
       -    "feature": "option_data_loss_protect"
       -   },
       -   "my_current_per_commitment_point": {
       -    "position": "80",
       -    "length": "33",
       -    "feature": "option_data_loss_protect"
       -   }
       -  }
       - },
       - "invalid_realm": {
       -  "type": "PERM|1",
       -  "payload": {}
       - },
       - "temporary_node_failure": {
       -  "type": "NODE|2",
       -  "payload": {}
       - },
       - "permanent_node_failure": {
       -  "type": "PERM|NODE|2",
       -  "payload": {}
       - },
       - "required_node_feature_missing": {
       -  "type": "PERM|NODE|3",
       -  "payload": {}
       - },
       - "invalid_onion_version": {
       -  "type": "BADONION|PERM|4",
       -  "payload": {
       -   "sha256_of_onion": {
       -    "position": "0",
       -    "length": "32"
       -   }
       -  }
       - },
       - "invalid_onion_hmac": {
       -  "type": "BADONION|PERM|5",
       -  "payload": {
       -   "sha256_of_onion": {
       -    "position": "0",
       -    "length": "32"
       -   }
       -  }
       - },
       - "invalid_onion_key": {
       -  "type": "BADONION|PERM|6",
       -  "payload": {
       -   "sha256_of_onion": {
       -    "position": "0",
       -    "length": "32"
       -   }
       -  }
       - },
       - "temporary_channel_failure": {
       -  "type": "UPDATE|7",
       -  "payload": {
       -   "len": {
       -    "position": "0",
       -    "length": "2"
       -   },
       -   "channel_update": {
       -    "position": "2",
       -    "length": "len"
       -   }
       -  }
       - },
       - "permanent_channel_failure": {
       -  "type": "PERM|8",
       -  "payload": {}
       - },
       - "required_channel_feature_missing": {
       -  "type": "PERM|9",
       -  "payload": {}
       - },
       - "unknown_next_peer": {
       -  "type": "PERM|10",
       -  "payload": {}
       - },
       - "amount_below_minimum": {
       -  "type": "UPDATE|11",
       -  "payload": {
       -   "htlc_msat": {
       -    "position": "0",
       -    "length": "8"
       -   },
       -   "len": {
       -    "position": "8",
       -    "length": "2"
       -   },
       -   "channel_update": {
       -    "position": "10",
       -    "length": "len"
       -   }
       -  }
       - },
       - "fee_insufficient": {
       -  "type": "UPDATE|12",
       -  "payload": {
       -   "htlc_msat": {
       -    "position": "0",
       -    "length": "8"
       -   },
       -   "len": {
       -    "position": "8",
       -    "length": "2"
       -   },
       -   "channel_update": {
       -    "position": "10",
       -    "length": "len"
       -   }
       -  }
       - },
       - "incorrect_cltv_expiry": {
       -  "type": "UPDATE|13",
       -  "payload": {
       -   "cltv_expiry": {
       -    "position": "0",
       -    "length": "4"
       -   },
       -   "len": {
       -    "position": "4",
       -    "length": "2"
       -   },
       -   "channel_update": {
       -    "position": "6",
       -    "length": "len"
       -   }
       -  }
       - },
       - "expiry_too_soon": {
       -  "type": "UPDATE|14",
       -  "payload": {
       -   "len": {
       -    "position": "0",
       -    "length": "2"
       -   },
       -   "channel_update": {
       -    "position": "2",
       -    "length": "len"
       -   }
       -  }
       - },
       - "unknown_payment_hash": {
       -  "type": "PERM|15",
       -  "payload": {}
       - },
       - "incorrect_payment_amount": {
       -  "type": "PERM|16",
       -  "payload": {}
       - },
       - "final_expiry_too_soon": {
       -  "type": "17",
       -  "payload": {}
       - },
       - "final_incorrect_cltv_expiry": {
       -  "type": "18",
       -  "payload": {
       -   "cltv_expiry": {
       -    "position": "0",
       -    "length": "4"
       -   }
       -  }
       - },
       - "final_incorrect_htlc_amount": {
       -  "type": "19",
       -  "payload": {
       -   "incoming_htlc_amt": {
       -    "position": "0",
       -    "length": "8"
       -   }
       -  }
       - },
       - "channel_disabled": {
       -  "type": "UPDATE|20",
       -  "payload": {}
       - },
       - "expiry_too_far": {
       -  "type": "21",
       -  "payload": {}
       - },
       - "announcement_signatures": {
       -  "type": "259",
       -  "payload": {
       -   "channel_id": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "short_channel_id": {
       -    "position": "32",
       -    "length": "8"
       -   },
       -   "node_signature": {
       -    "position": "40",
       -    "length": "64"
       -   },
       -   "bitcoin_signature": {
       -    "position": "104",
       -    "length": "64"
       -   }
       -  }
       - },
       - "channel_announcement": {
       -  "type": "256",
       -  "payload": {
       -   "node_signature_1": {
       -    "position": "0",
       -    "length": "64"
       -   },
       -   "node_signature_2": {
       -    "position": "64",
       -    "length": "64"
       -   },
       -   "bitcoin_signature_1": {
       -    "position": "128",
       -    "length": "64"
       -   },
       -   "bitcoin_signature_2": {
       -    "position": "192",
       -    "length": "64"
       -   },
       -   "len": {
       -    "position": "256",
       -    "length": "2"
       -   },
       -   "features": {
       -    "position": "258",
       -    "length": "len"
       -   },
       -   "chain_hash": {
       -    "position": "258+len",
       -    "length": "32"
       -   },
       -   "short_channel_id": {
       -    "position": "290+len",
       -    "length": "8"
       -   },
       -   "node_id_1": {
       -    "position": "298+len",
       -    "length": "33"
       -   },
       -   "node_id_2": {
       -    "position": "331+len",
       -    "length": "33"
       -   },
       -   "bitcoin_key_1": {
       -    "position": "364+len",
       -    "length": "33"
       -   },
       -   "bitcoin_key_2": {
       -    "position": "397+len",
       -    "length": "33"
       -   }
       -  }
       - },
       - "node_announcement": {
       -  "type": "257",
       -  "payload": {
       -   "signature": {
       -    "position": "0",
       -    "length": "64"
       -   },
       -   "flen": {
       -    "position": "64",
       -    "length": "2"
       -   },
       -   "features": {
       -    "position": "66",
       -    "length": "flen"
       -   },
       -   "timestamp": {
       -    "position": "66+flen",
       -    "length": "4"
       -   },
       -   "node_id": {
       -    "position": "70+flen",
       -    "length": "33"
       -   },
       -   "rgb_color": {
       -    "position": "103+flen",
       -    "length": "3"
       -   },
       -   "alias": {
       -    "position": "106+flen",
       -    "length": "32"
       -   },
       -   "addrlen": {
       -    "position": "138+flen",
       -    "length": "2"
       -   },
       -   "addresses": {
       -    "position": "140+flen",
       -    "length": "addrlen"
       -   }
       -  }
       - },
       - "channel_update": {
       -  "type": "258",
       -  "payload": {
       -   "signature": {
       -    "position": "0",
       -    "length": "64"
       -   },
       -   "chain_hash": {
       -    "position": "64",
       -    "length": "32"
       -   },
       -   "short_channel_id": {
       -    "position": "96",
       -    "length": "8"
       -   },
       -   "timestamp": {
       -    "position": "104",
       -    "length": "4"
       -   },
       -   "message_flags": {
       -    "position": "108",
       -    "length": "1"
       -   },
       -   "channel_flags": {
       -    "position": "109",
       -    "length": "1"
       -   },
       -   "cltv_expiry_delta": {
       -    "position": "110",
       -    "length": "2"
       -   },
       -   "htlc_minimum_msat": {
       -    "position": "112",
       -    "length": "8"
       -   },
       -   "fee_base_msat": {
       -    "position": "120",
       -    "length": "4"
       -   },
       -   "fee_proportional_millionths": {
       -    "position": "124",
       -    "length": "4"
       -   },
       -   "htlc_maximum_msat": {
       -    "position": "128",
       -    "length": "8",
       -    "feature": "option_channel_htlc_max"
       -   }
       -  }
       - },
       - "query_short_channel_ids": {
       -  "type": "261",
       -  "payload": {
       -   "chain_hash": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "len": {
       -    "position": "32",
       -    "length": "2"
       -   },
       -   "encoded_short_ids": {
       -    "position": "34",
       -    "length": "len"
       -   }
       -  }
       - },
       - "reply_short_channel_ids_end": {
       -  "type": "262",
       -  "payload": {
       -   "chain_hash": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "complete": {
       -    "position": "32",
       -    "length": "1"
       -   }
       -  }
       - },
       - "query_channel_range": {
       -  "type": "263",
       -  "payload": {
       -   "chain_hash": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "first_blocknum": {
       -    "position": "32",
       -    "length": "4"
       -   },
       -   "number_of_blocks": {
       -    "position": "36",
       -    "length": "4"
       -   }
       -  }
       - },
       - "reply_channel_range": {
       -  "type": "264",
       -  "payload": {
       -   "chain_hash": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "first_blocknum": {
       -    "position": "32",
       -    "length": "4"
       -   },
       -   "number_of_blocks": {
       -    "position": "36",
       -    "length": "4"
       -   },
       -   "complete": {
       -    "position": "40",
       -    "length": "1"
       -   },
       -   "len": {
       -    "position": "41",
       -    "length": "2"
       -   },
       -   "encoded_short_ids": {
       -    "position": "43",
       -    "length": "len"
       -   }
       -  }
       - },
       - "gossip_timestamp_filter": {
       -  "type": "265",
       -  "payload": {
       -   "chain_hash": {
       -    "position": "0",
       -    "length": "32"
       -   },
       -   "first_timestamp": {
       -    "position": "32",
       -    "length": "4"
       -   },
       -   "timestamp_range": {
       -    "position": "36",
       -    "length": "4"
       -   }
       -  }
       - }
       -}
   DIR diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py
       t@@ -141,6 +141,21 @@ def tagged(char, l):
        def tagged_bytes(char, l):
            return tagged(char, bitstring.BitArray(l))
        
       +def trim_to_min_length(bits):
       +    """Ensures 'bits' have min number of leading zeroes.
       +    Assumes 'bits' is big-endian, and that it needs to be encoded in 5 bit blocks.
       +    """
       +    bits = bits[:]  # copy
       +    # make sure we can be split into 5 bit blocks
       +    while bits.len % 5 != 0:
       +        bits.prepend('0b0')
       +    # Get minimal length by trimming leading 5 bits at a time.
       +    while bits.startswith('0b00000'):
       +        if len(bits) == 5:
       +            break  # v == 0
       +        bits = bits[5:]
       +    return bits
       +
        # Discard trailing bits, convert to bytes.
        def trim_to_bytes(barr):
            # Adds a byte if necessary.
       t@@ -155,7 +170,7 @@ def pull_tagged(stream):
            length = stream.read(5).uint * 32 + stream.read(5).uint
            return (CHARSET[tag], stream.read(length * 5), stream)
        
       -def lnencode(addr, privkey):
       +def lnencode(addr: 'LnAddr', privkey) -> str:
            if addr.amount:
                amount = Decimal(str(addr.amount))
                # We can only send down to millisatoshi.
       t@@ -172,16 +187,22 @@ def lnencode(addr, privkey):
            # Start with the timestamp
            data = bitstring.pack('uint:35', addr.date)
        
       +    tags_set = set()
       +
            # Payment hash
            data += tagged_bytes('p', addr.paymenthash)
       -    tags_set = set()
       +    tags_set.add('p')
       +
       +    if addr.payment_secret is not None:
       +        data += tagged_bytes('s', addr.payment_secret)
       +        tags_set.add('s')
        
            for k, v in addr.tags:
        
                # BOLT #11:
                #
                # A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
       -        if k in ('d', 'h', 'n', 'x'):
       +        if k in ('d', 'h', 'n', 'x', 'p', 's'):
                    if k in tags_set:
                        raise ValueError("Duplicate '{}' tag".format(k))
        
       t@@ -196,23 +217,23 @@ def lnencode(addr, privkey):
                elif k == 'd':
                    data += tagged_bytes('d', v.encode())
                elif k == 'x':
       -            # Get minimal length by trimming leading 5 bits at a time.
       -            expirybits = bitstring.pack('intbe:64', v)[4:64]
       -            while expirybits.startswith('0b00000'):
       -                if len(expirybits) == 5:
       -                    break  # v == 0
       -                expirybits = expirybits[5:]
       +            expirybits = bitstring.pack('intbe:64', v)
       +            expirybits = trim_to_min_length(expirybits)
                    data += tagged('x', expirybits)
                elif k == 'h':
                    data += tagged_bytes('h', sha256(v.encode('utf-8')).digest())
                elif k == 'n':
                    data += tagged_bytes('n', v)
                elif k == 'c':
       -            # Get minimal length by trimming leading 5 bits at a time.
       -            finalcltvbits = bitstring.pack('intbe:64', v)[4:64]
       -            while finalcltvbits.startswith('0b00000'):
       -                finalcltvbits = finalcltvbits[5:]
       +            finalcltvbits = bitstring.pack('intbe:64', v)
       +            finalcltvbits = trim_to_min_length(finalcltvbits)
                    data += tagged('c', finalcltvbits)
       +        elif k == '9':
       +            if v == 0:
       +                continue
       +            feature_bits = bitstring.BitArray(uint=v, length=v.bit_length())
       +            feature_bits = trim_to_min_length(feature_bits)
       +            data += tagged('9', feature_bits)
                else:
                    # FIXME: Support unknown tags?
                    raise ValueError("Unknown tag {}".format(k))
       t@@ -239,15 +260,17 @@ def lnencode(addr, privkey):
            return bech32_encode(hrp, bitarray_to_u5(data))
        
        class LnAddr(object):
       -    def __init__(self, paymenthash: bytes = None, amount=None, currency=None, tags=None, date=None):
       +    def __init__(self, *, paymenthash: bytes = None, amount=None, currency=None, tags=None, date=None,
       +                 payment_secret: bytes = None):
                self.date = int(time.time()) if not date else int(date)
                self.tags = [] if not tags else tags
                self.unknown_tags = []
                self.paymenthash = paymenthash
       +        self.payment_secret = payment_secret
                self.signature = None
                self.pubkey = None
                self.currency = constants.net.SEGWIT_HRP if currency is None else currency
       -        self.amount = amount
       +        self.amount = amount  # in bitcoins
                self._min_final_cltv_expiry = 9
        
            def __str__(self):
       t@@ -383,14 +406,28 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
                        continue
                    addr.paymenthash = trim_to_bytes(tagdata)
        
       +        elif tag == 's':
       +            if data_length != 52:
       +                addr.unknown_tags.append((tag, tagdata))
       +                continue
       +            addr.payment_secret = trim_to_bytes(tagdata)
       +
                elif tag == 'n':
                    if data_length != 53:
                        addr.unknown_tags.append((tag, tagdata))
                        continue
                    pubkeybytes = trim_to_bytes(tagdata)
                    addr.pubkey = pubkeybytes
       +
                elif tag == 'c':
                    addr._min_final_cltv_expiry = tagdata.int
       +
       +        elif tag == '9':
       +            features = tagdata.uint
       +            addr.tags.append(('9', features))
       +            from .lnutil import validate_features
       +            validate_features(features)
       +
                else:
                    addr.unknown_tags.append((tag, tagdata))
        
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -218,13 +218,13 @@ class Channel(Logger):
                    short_channel_id=self.short_channel_id,
                    channel_flags=channel_flags,
                    message_flags=b'\x01',
       -            cltv_expiry_delta=lnutil.NBLOCK_OUR_CLTV_EXPIRY_DELTA.to_bytes(2, byteorder="big"),
       -            htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat.to_bytes(8, byteorder="big"),
       -            htlc_maximum_msat=htlc_maximum_msat.to_bytes(8, byteorder="big"),
       -            fee_base_msat=lnutil.OUR_FEE_BASE_MSAT.to_bytes(4, byteorder="big"),
       -            fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS.to_bytes(4, byteorder="big"),
       +            cltv_expiry_delta=lnutil.NBLOCK_OUR_CLTV_EXPIRY_DELTA,
       +            htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat,
       +            htlc_maximum_msat=htlc_maximum_msat,
       +            fee_base_msat=lnutil.OUR_FEE_BASE_MSAT,
       +            fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS,
                    chain_hash=constants.net.rev_genesis_bytes(),
       -            timestamp=now.to_bytes(4, byteorder="big"),
       +            timestamp=now,
                )
                sighash = sha256d(chan_upd[2 + 64:])
                sig = ecc.ECPrivkey(self.lnworker.node_keypair.privkey).sign(sighash, ecc.sig_string_from_r_and_s)
       t@@ -249,7 +249,8 @@ class Channel(Logger):
                    node_ids = sorted_node_ids
                    bitcoin_keys.reverse()
        
       -        chan_ann = encode_msg("channel_announcement",
       +        chan_ann = encode_msg(
       +            "channel_announcement",
                    len=0,
                    features=b'',
                    chain_hash=constants.net.rev_genesis_bytes(),
       t@@ -257,7 +258,7 @@ class Channel(Logger):
                    node_id_1=node_ids[0],
                    node_id_2=node_ids[1],
                    bitcoin_key_1=bitcoin_keys[0],
       -            bitcoin_key_2=bitcoin_keys[1]
       +            bitcoin_key_2=bitcoin_keys[1],
                )
        
                self._chan_ann_without_sigs = chan_ann
   DIR diff --git a/electrum/lnmsg.py b/electrum/lnmsg.py
       t@@ -1,153 +1,513 @@
       -import json
        import os
       -from typing import Callable, Tuple
       +import csv
       +import io
       +from typing import Callable, Tuple, Any, Dict, List, Sequence, Union, Optional
        from collections import OrderedDict
        
       -def _eval_length_term(x, ma: dict) -> int:
       -    """
       -    Evaluate a term of the simple language used
       -    to specify lightning message field lengths.
       +from .lnutil import OnionFailureCodeMetaFlag
        
       -    If `x` is an integer, it is returned as is,
       -    otherwise it is treated as a variable and
       -    looked up in `ma`.
        
       -    If the value in `ma` was no integer, it is
       -    assumed big-endian bytes and decoded.
       +class MalformedMsg(Exception): pass
       +class UnknownMsgFieldType(MalformedMsg): pass
       +class UnexpectedEndOfStream(MalformedMsg): pass
       +class FieldEncodingNotMinimal(MalformedMsg): pass
       +class UnknownMandatoryTLVRecordType(MalformedMsg): pass
       +class MsgTrailingGarbage(MalformedMsg): pass
       +class MsgInvalidFieldOrder(MalformedMsg): pass
       +class UnexpectedFieldSizeForEncoder(MalformedMsg): pass
        
       -    Returns evaluated result as int
       -    """
       -    try:
       -        x = int(x)
       -    except ValueError:
       -        x = ma[x]
       +
       +def _num_remaining_bytes_to_read(fd: io.BytesIO) -> int:
       +    cur_pos = fd.tell()
       +    end_pos = fd.seek(0, io.SEEK_END)
       +    fd.seek(cur_pos)
       +    return end_pos - cur_pos
       +
       +
       +def _assert_can_read_at_least_n_bytes(fd: io.BytesIO, n: int) -> None:
       +    # note: it's faster to read n bytes and then check if we read n, than
       +    #       to assert we can read at least n and then read n bytes.
       +    nremaining = _num_remaining_bytes_to_read(fd)
       +    if nremaining < n:
       +        raise UnexpectedEndOfStream(f"wants to read {n} bytes but only {nremaining} bytes left")
       +
       +
       +def write_bigsize_int(i: int) -> bytes:
       +    assert i >= 0, i
       +    if i < 0xfd:
       +        return int.to_bytes(i, length=1, byteorder="big", signed=False)
       +    elif i < 0x1_0000:
       +        return b"\xfd" + int.to_bytes(i, length=2, byteorder="big", signed=False)
       +    elif i < 0x1_0000_0000:
       +        return b"\xfe" + int.to_bytes(i, length=4, byteorder="big", signed=False)
       +    else:
       +        return b"\xff" + int.to_bytes(i, length=8, byteorder="big", signed=False)
       +
       +
       +def read_bigsize_int(fd: io.BytesIO) -> Optional[int]:
            try:
       -        x = int(x)
       -    except ValueError:
       -        x = int.from_bytes(x, byteorder='big')
       -    return x
       +        first = fd.read(1)[0]
       +    except IndexError:
       +        return None  # end of file
       +    if first < 0xfd:
       +        return first
       +    elif first == 0xfd:
       +        buf = fd.read(2)
       +        if len(buf) != 2:
       +            raise UnexpectedEndOfStream()
       +        val = int.from_bytes(buf, byteorder="big", signed=False)
       +        if not (0xfd <= val < 0x1_0000):
       +            raise FieldEncodingNotMinimal()
       +        return val
       +    elif first == 0xfe:
       +        buf = fd.read(4)
       +        if len(buf) != 4:
       +            raise UnexpectedEndOfStream()
       +        val = int.from_bytes(buf, byteorder="big", signed=False)
       +        if not (0x1_0000 <= val < 0x1_0000_0000):
       +            raise FieldEncodingNotMinimal()
       +        return val
       +    elif first == 0xff:
       +        buf = fd.read(8)
       +        if len(buf) != 8:
       +            raise UnexpectedEndOfStream()
       +        val = int.from_bytes(buf, byteorder="big", signed=False)
       +        if not (0x1_0000_0000 <= val):
       +            raise FieldEncodingNotMinimal()
       +        return val
       +    raise Exception()
        
       -def _eval_exp_with_ctx(exp, ctx: dict) -> int:
       -    """
       -    Evaluate simple mathematical expression given
       -    in `exp` with context (variables assigned)
       -    from the dict `ctx`.
        
       -    Returns evaluated result as int
       -    """
       -    exp = str(exp)
       -    if "*" in exp:
       -        assert "+" not in exp
       -        result = 1
       -        for term in exp.split("*"):
       -            result *= _eval_length_term(term, ctx)
       -        return result
       -    return sum(_eval_length_term(x, ctx) for x in exp.split("+"))
       -
       -def _make_handler(msg_name: str, v: dict) -> Callable[[bytes], Tuple[str, dict]]:
       -    """
       -    Generate a message handler function (taking bytes)
       -    for message type `msg_name` with specification `v`
       +# TODO: maybe if field_type is not "byte", we could return a list of type_len sized chunks?
       +#       if field_type is a numeric, we could return a list of ints?
       +def _read_field(*, fd: io.BytesIO, field_type: str, count: Union[int, str]) -> Union[bytes, int]:
       +    if not fd: raise Exception()
       +    if isinstance(count, int):
       +        assert count >= 0, f"{count!r} must be non-neg int"
       +    elif count == "...":
       +        pass
       +    else:
       +        raise Exception(f"unexpected field count: {count!r}")
       +    if count == 0:
       +        return b""
       +    type_len = None
       +    if field_type == 'byte':
       +        type_len = 1
       +    elif field_type in ('u8', 'u16', 'u32', 'u64'):
       +        if field_type == 'u8':
       +            type_len = 1
       +        elif field_type == 'u16':
       +            type_len = 2
       +        elif field_type == 'u32':
       +            type_len = 4
       +        else:
       +            assert field_type == 'u64'
       +            type_len = 8
       +        assert count == 1, count
       +        buf = fd.read(type_len)
       +        if len(buf) != type_len:
       +            raise UnexpectedEndOfStream()
       +        return int.from_bytes(buf, byteorder="big", signed=False)
       +    elif field_type in ('tu16', 'tu32', 'tu64'):
       +        if field_type == 'tu16':
       +            type_len = 2
       +        elif field_type == 'tu32':
       +            type_len = 4
       +        else:
       +            assert field_type == 'tu64'
       +            type_len = 8
       +        assert count == 1, count
       +        raw = fd.read(type_len)
       +        if len(raw) > 0 and raw[0] == 0x00:
       +            raise FieldEncodingNotMinimal()
       +        return int.from_bytes(raw, byteorder="big", signed=False)
       +    elif field_type == 'varint':
       +        assert count == 1, count
       +        val = read_bigsize_int(fd)
       +        if val is None:
       +            raise UnexpectedEndOfStream()
       +        return val
       +    elif field_type == 'chain_hash':
       +        type_len = 32
       +    elif field_type == 'channel_id':
       +        type_len = 32
       +    elif field_type == 'sha256':
       +        type_len = 32
       +    elif field_type == 'signature':
       +        type_len = 64
       +    elif field_type == 'point':
       +        type_len = 33
       +    elif field_type == 'short_channel_id':
       +        type_len = 8
       +
       +    if count == "...":
       +        total_len = -1  # read all
       +    else:
       +        if type_len is None:
       +            raise UnknownMsgFieldType(f"unknown field type: {field_type!r}")
       +        total_len = count * type_len
       +
       +    buf = fd.read(total_len)
       +    if total_len >= 0 and len(buf) != total_len:
       +        raise UnexpectedEndOfStream()
       +    return buf
       +
        
       -    Check lib/lightning.json, `msg_name` could be 'init',
       -    and `v` could be
       +# TODO: maybe for "value" we could accept a list with len "count" of appropriate items
       +def _write_field(*, fd: io.BytesIO, field_type: str, count: Union[int, str],
       +                 value: Union[bytes, int]) -> None:
       +    if not fd: raise Exception()
       +    if isinstance(count, int):
       +        assert count >= 0, f"{count!r} must be non-neg int"
       +    elif count == "...":
       +        pass
       +    else:
       +        raise Exception(f"unexpected field count: {count!r}")
       +    if count == 0:
       +        return
       +    type_len = None
       +    if field_type == 'byte':
       +        type_len = 1
       +    elif field_type == 'u8':
       +        type_len = 1
       +    elif field_type == 'u16':
       +        type_len = 2
       +    elif field_type == 'u32':
       +        type_len = 4
       +    elif field_type == 'u64':
       +        type_len = 8
       +    elif field_type in ('tu16', 'tu32', 'tu64'):
       +        if field_type == 'tu16':
       +            type_len = 2
       +        elif field_type == 'tu32':
       +            type_len = 4
       +        else:
       +            assert field_type == 'tu64'
       +            type_len = 8
       +        assert count == 1, count
       +        if isinstance(value, int):
       +            value = int.to_bytes(value, length=type_len, byteorder="big", signed=False)
       +        if not isinstance(value, (bytes, bytearray)):
       +            raise Exception(f"can only write bytes into fd. got: {value!r}")
       +        while len(value) > 0 and value[0] == 0x00:
       +            value = value[1:]
       +        nbytes_written = fd.write(value)
       +        if nbytes_written != len(value):
       +            raise Exception(f"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?")
       +        return
       +    elif field_type == 'varint':
       +        assert count == 1, count
       +        if isinstance(value, int):
       +            value = write_bigsize_int(value)
       +        if not isinstance(value, (bytes, bytearray)):
       +            raise Exception(f"can only write bytes into fd. got: {value!r}")
       +        nbytes_written = fd.write(value)
       +        if nbytes_written != len(value):
       +            raise Exception(f"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?")
       +        return
       +    elif field_type == 'chain_hash':
       +        type_len = 32
       +    elif field_type == 'channel_id':
       +        type_len = 32
       +    elif field_type == 'sha256':
       +        type_len = 32
       +    elif field_type == 'signature':
       +        type_len = 64
       +    elif field_type == 'point':
       +        type_len = 33
       +    elif field_type == 'short_channel_id':
       +        type_len = 8
       +    total_len = -1
       +    if count != "...":
       +        if type_len is None:
       +            raise UnknownMsgFieldType(f"unknown field type: {field_type!r}")
       +        total_len = count * type_len
       +        if isinstance(value, int) and (count == 1 or field_type == 'byte'):
       +            value = int.to_bytes(value, length=total_len, byteorder="big", signed=False)
       +    if not isinstance(value, (bytes, bytearray)):
       +        raise Exception(f"can only write bytes into fd. got: {value!r}")
       +    if count != "..." and total_len != len(value):
       +        raise UnexpectedFieldSizeForEncoder(f"expected: {total_len}, got {len(value)}")
       +    nbytes_written = fd.write(value)
       +    if nbytes_written != len(value):
       +        raise Exception(f"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?")
        
       -      { type: 16, payload: { 'gflen': ..., ... }, ... }
        
       -    Returns function taking bytes
       +def _read_tlv_record(*, fd: io.BytesIO) -> Tuple[int, bytes]:
       +    if not fd: raise Exception()
       +    tlv_type = _read_field(fd=fd, field_type="varint", count=1)
       +    tlv_len = _read_field(fd=fd, field_type="varint", count=1)
       +    tlv_val = _read_field(fd=fd, field_type="byte", count=tlv_len)
       +    return tlv_type, tlv_val
       +
       +
       +def _write_tlv_record(*, fd: io.BytesIO, tlv_type: int, tlv_val: bytes) -> None:
       +    if not fd: raise Exception()
       +    tlv_len = len(tlv_val)
       +    _write_field(fd=fd, field_type="varint", count=1, value=tlv_type)
       +    _write_field(fd=fd, field_type="varint", count=1, value=tlv_len)
       +    _write_field(fd=fd, field_type="byte", count=tlv_len, value=tlv_val)
       +
       +
       +def _resolve_field_count(field_count_str: str, *, vars_dict: dict, allow_any=False) -> Union[int, str]:
       +    """Returns an evaluated field count, typically an int.
       +    If allow_any is True, the return value can be a str with value=="...".
            """
       -    def handler(data: bytes) -> Tuple[str, dict]:
       -        ma = {}  # map of field name -> field data; after parsing msg
       -        pos = 0
       -        for fieldname in v["payload"]:
       -            poslenMap = v["payload"][fieldname]
       -            if "feature" in poslenMap and pos == len(data):
       -                continue
       -            #assert pos == _eval_exp_with_ctx(poslenMap["position"], ma)  # this assert is expensive...
       -            length = poslenMap["length"]
       -            length = _eval_exp_with_ctx(length, ma)
       -            ma[fieldname] = data[pos:pos+length]
       -            pos += length
       -        # BOLT-01: "MUST ignore any additional data within a message beyond the length that it expects for that type."
       -        assert pos <= len(data), (msg_name, pos, len(data))
       -        return msg_name, ma
       -    return handler
       +    if field_count_str == "":
       +        field_count = 1
       +    elif field_count_str == "...":
       +        if not allow_any:
       +            raise Exception("field count is '...' but allow_any is False")
       +        return field_count_str
       +    else:
       +        try:
       +            field_count = int(field_count_str)
       +        except ValueError:
       +            field_count = vars_dict[field_count_str]
       +            if isinstance(field_count, (bytes, bytearray)):
       +                field_count = int.from_bytes(field_count, byteorder="big")
       +    assert isinstance(field_count, int)
       +    return field_count
       +
       +
       +def _parse_msgtype_intvalue_for_onion_wire(value: str) -> int:
       +    msg_type_int = 0
       +    for component in value.split("|"):
       +        try:
       +            msg_type_int |= int(component)
       +        except ValueError:
       +            msg_type_int |= OnionFailureCodeMetaFlag[component]
       +    return msg_type_int
       +
        
        class LNSerializer:
       -    def __init__(self):
       -        message_types = {}
       -        path = os.path.join(os.path.dirname(__file__), 'lightning.json')
       -        with open(path) as f:
       -            structured = json.loads(f.read(), object_pairs_hook=OrderedDict)
       -
       -        for msg_name in structured:
       -            v = structured[msg_name]
       -            # these message types are skipped since their types collide
       -            # (for example with pong, which also uses type=19)
       -            # we don't need them yet
       -            if msg_name in ["final_incorrect_cltv_expiry", "final_incorrect_htlc_amount"]:
       -                continue
       -            if len(v["payload"]) == 0:
       +
       +    def __init__(self, *, for_onion_wire: bool = False):
       +        # TODO msg_type could be 'int' everywhere...
       +        self.msg_scheme_from_type = {}  # type: Dict[bytes, List[Sequence[str]]]
       +        self.msg_type_from_name = {}  # type: Dict[str, bytes]
       +
       +        self.in_tlv_stream_get_tlv_record_scheme_from_type = {}  # type: Dict[str, Dict[int, List[Sequence[str]]]]
       +        self.in_tlv_stream_get_record_type_from_name = {}  # type: Dict[str, Dict[str, int]]
       +        self.in_tlv_stream_get_record_name_from_type = {}  # type: Dict[str, Dict[int, str]]
       +
       +        if for_onion_wire:
       +            path = os.path.join(os.path.dirname(__file__), "lnwire", "onion_wire.csv")
       +        else:
       +            path = os.path.join(os.path.dirname(__file__), "lnwire", "peer_wire.csv")
       +        with open(path, newline='') as f:
       +            csvreader = csv.reader(f)
       +            for row in csvreader:
       +                #print(f">>> {row!r}")
       +                if row[0] == "msgtype":
       +                    # msgtype,<msgname>,<value>[,<option>]
       +                    msg_type_name = row[1]
       +                    if for_onion_wire:
       +                        msg_type_int = _parse_msgtype_intvalue_for_onion_wire(str(row[2]))
       +                    else:
       +                        msg_type_int = int(row[2])
       +                    msg_type_bytes = msg_type_int.to_bytes(2, 'big')
       +                    assert msg_type_bytes not in self.msg_scheme_from_type, f"type collision? for {msg_type_name}"
       +                    assert msg_type_name not in self.msg_type_from_name, f"type collision? for {msg_type_name}"
       +                    row[2] = msg_type_int
       +                    self.msg_scheme_from_type[msg_type_bytes] = [tuple(row)]
       +                    self.msg_type_from_name[msg_type_name] = msg_type_bytes
       +                elif row[0] == "msgdata":
       +                    # msgdata,<msgname>,<fieldname>,<typename>,[<count>][,<option>]
       +                    assert msg_type_name == row[1]
       +                    self.msg_scheme_from_type[msg_type_bytes].append(tuple(row))
       +                elif row[0] == "tlvtype":
       +                    # tlvtype,<tlvstreamname>,<tlvname>,<value>[,<option>]
       +                    tlv_stream_name = row[1]
       +                    tlv_record_name = row[2]
       +                    tlv_record_type = int(row[3])
       +                    row[3] = tlv_record_type
       +                    if tlv_stream_name not in self.in_tlv_stream_get_tlv_record_scheme_from_type:
       +                        self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name] = OrderedDict()
       +                        self.in_tlv_stream_get_record_type_from_name[tlv_stream_name] = {}
       +                        self.in_tlv_stream_get_record_name_from_type[tlv_stream_name] = {}
       +                    assert tlv_record_type not in self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name], f"type collision? for {tlv_stream_name}/{tlv_record_name}"
       +                    assert tlv_record_name not in self.in_tlv_stream_get_record_type_from_name[tlv_stream_name], f"type collision? for {tlv_stream_name}/{tlv_record_name}"
       +                    assert tlv_record_type not in self.in_tlv_stream_get_record_type_from_name[tlv_stream_name], f"type collision? for {tlv_stream_name}/{tlv_record_name}"
       +                    self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name][tlv_record_type] = [tuple(row)]
       +                    self.in_tlv_stream_get_record_type_from_name[tlv_stream_name][tlv_record_name] = tlv_record_type
       +                    self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type] = tlv_record_name
       +                    if max(self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name].keys()) > tlv_record_type:
       +                        raise Exception(f"tlv record types must be listed in monotonically increasing order for stream. "
       +                                        f"stream={tlv_stream_name}")
       +                elif row[0] == "tlvdata":
       +                    # tlvdata,<tlvstreamname>,<tlvname>,<fieldname>,<typename>,[<count>][,<option>]
       +                    assert tlv_stream_name == row[1]
       +                    assert tlv_record_name == row[2]
       +                    self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name][tlv_record_type].append(tuple(row))
       +                else:
       +                    pass  # TODO
       +
       +    def write_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str, **kwargs) -> None:
       +        scheme_map = self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name]
       +        for tlv_record_type, scheme in scheme_map.items():  # note: tlv_record_type is monotonically increasing
       +            tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type]
       +            if tlv_record_name not in kwargs:
                        continue
       +            with io.BytesIO() as tlv_record_fd:
       +                for row in scheme:
       +                    if row[0] == "tlvtype":
       +                        pass
       +                    elif row[0] == "tlvdata":
       +                        # tlvdata,<tlvstreamname>,<tlvname>,<fieldname>,<typename>,[<count>][,<option>]
       +                        assert tlv_stream_name == row[1]
       +                        assert tlv_record_name == row[2]
       +                        field_name = row[3]
       +                        field_type = row[4]
       +                        field_count_str = row[5]
       +                        field_count = _resolve_field_count(field_count_str,
       +                                                           vars_dict=kwargs[tlv_record_name],
       +                                                           allow_any=True)
       +                        field_value = kwargs[tlv_record_name][field_name]
       +                        _write_field(fd=tlv_record_fd,
       +                                     field_type=field_type,
       +                                     count=field_count,
       +                                     value=field_value)
       +                    else:
       +                        raise Exception(f"unexpected row in scheme: {row!r}")
       +                _write_tlv_record(fd=fd, tlv_type=tlv_record_type, tlv_val=tlv_record_fd.getvalue())
       +
       +    def read_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str) -> Dict[str, Dict[str, Any]]:
       +        parsed = {}  # type: Dict[str, Dict[str, Any]]
       +        scheme_map = self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name]
       +        last_seen_tlv_record_type = -1  # type: int
       +        while _num_remaining_bytes_to_read(fd) > 0:
       +            tlv_record_type, tlv_record_val = _read_tlv_record(fd=fd)
       +            if not (tlv_record_type > last_seen_tlv_record_type):
       +                raise MsgInvalidFieldOrder(f"TLV records must be monotonically increasing by type. "
       +                                           f"cur: {tlv_record_type}. prev: {last_seen_tlv_record_type}")
       +            last_seen_tlv_record_type = tlv_record_type
                    try:
       -                num = int(v["type"])
       -            except ValueError:
       -                #print("skipping", k)
       -                continue
       -            byts = num.to_bytes(2, 'big')
       -            assert byts not in message_types, (byts, message_types[byts].__name__, msg_name)
       -            names = [x.__name__ for x in message_types.values()]
       -            assert msg_name + "_handler" not in names, (msg_name, names)
       -            message_types[byts] = _make_handler(msg_name, v)
       -            message_types[byts].__name__ = msg_name + "_handler"
       -
       -        assert message_types[b"\x00\x10"].__name__ == "init_handler"
       -        self.structured = structured
       -        self.message_types = message_types
       -
       -    def encode_msg(self, msg_type : str, **kwargs) -> bytes:
       +                scheme = scheme_map[tlv_record_type]
       +            except KeyError:
       +                if tlv_record_type % 2 == 0:
       +                    # unknown "even" type: hard fail
       +                    raise UnknownMandatoryTLVRecordType(f"{tlv_stream_name}/{tlv_record_type}") from None
       +                else:
       +                    # unknown "odd" type: skip it
       +                    continue
       +            tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type]
       +            parsed[tlv_record_name] = {}
       +            with io.BytesIO(tlv_record_val) as tlv_record_fd:
       +                for row in scheme:
       +                    #print(f"row: {row!r}")
       +                    if row[0] == "tlvtype":
       +                        pass
       +                    elif row[0] == "tlvdata":
       +                        # tlvdata,<tlvstreamname>,<tlvname>,<fieldname>,<typename>,[<count>][,<option>]
       +                        assert tlv_stream_name == row[1]
       +                        assert tlv_record_name == row[2]
       +                        field_name = row[3]
       +                        field_type = row[4]
       +                        field_count_str = row[5]
       +                        field_count = _resolve_field_count(field_count_str,
       +                                                           vars_dict=parsed[tlv_record_name],
       +                                                           allow_any=True)
       +                        #print(f">> count={field_count}. parsed={parsed}")
       +                        parsed[tlv_record_name][field_name] = _read_field(fd=tlv_record_fd,
       +                                                                          field_type=field_type,
       +                                                                          count=field_count)
       +                    else:
       +                        raise Exception(f"unexpected row in scheme: {row!r}")
       +                if _num_remaining_bytes_to_read(tlv_record_fd) > 0:
       +                    raise MsgTrailingGarbage(f"TLV record ({tlv_stream_name}/{tlv_record_name}) has extra trailing garbage")
       +        return parsed
       +
       +    def encode_msg(self, msg_type: str, **kwargs) -> bytes:
                """
                Encode kwargs into a Lightning message (bytes)
                of the type given in the msg_type string
                """
       -        typ = self.structured[msg_type]
       -        data = int(typ["type"]).to_bytes(2, 'big')
       -        lengths = {}
       -        for k in typ["payload"]:
       -            poslenMap = typ["payload"][k]
       -            if k not in kwargs and "feature" in poslenMap:
       -                continue
       -            param = kwargs.get(k, 0)
       -            leng = _eval_exp_with_ctx(poslenMap["length"], lengths)
       -            try:
       -                clone = dict(lengths)
       -                clone.update(kwargs)
       -                leng = _eval_exp_with_ctx(poslenMap["length"], clone)
       -            except KeyError:
       -                pass
       -            try:
       -                if not isinstance(param, bytes):
       -                    assert isinstance(param, int), "field {} is neither bytes or int".format(k)
       -                    param = param.to_bytes(leng, 'big')
       -            except ValueError:
       -                raise Exception("{} does not fit in {} bytes".format(k, leng))
       -            lengths[k] = len(param)
       -            if lengths[k] != leng:
       -                raise Exception("field {} is {} bytes long, should be {} bytes long".format(k, lengths[k], leng))
       -            data += param
       -        return data
       -
       -    def decode_msg(self, data : bytes) -> Tuple[str, dict]:
       +        #print(f">>> encode_msg. msg_type={msg_type}, payload={kwargs!r}")
       +        msg_type_bytes = self.msg_type_from_name[msg_type]
       +        scheme = self.msg_scheme_from_type[msg_type_bytes]
       +        with io.BytesIO() as fd:
       +            fd.write(msg_type_bytes)
       +            for row in scheme:
       +                if row[0] == "msgtype":
       +                    pass
       +                elif row[0] == "msgdata":
       +                    # msgdata,<msgname>,<fieldname>,<typename>,[<count>][,<option>]
       +                    field_name = row[2]
       +                    field_type = row[3]
       +                    field_count_str = row[4]
       +                    #print(f">>> encode_msg. msgdata. field_name={field_name!r}. field_type={field_type!r}. field_count_str={field_count_str!r}")
       +                    field_count = _resolve_field_count(field_count_str, vars_dict=kwargs)
       +                    if field_name == "tlvs":
       +                        tlv_stream_name = field_type
       +                        if tlv_stream_name in kwargs:
       +                            self.write_tlv_stream(fd=fd, tlv_stream_name=tlv_stream_name, **(kwargs[tlv_stream_name]))
       +                        continue
       +                    try:
       +                        field_value = kwargs[field_name]
       +                    except KeyError:
       +                        if len(row) > 5:
       +                            break  # optional feature field not present
       +                        else:
       +                            field_value = 0  # default mandatory fields to zero
       +                    #print(f">>> encode_msg. writing field: {field_name}. value={field_value!r}. field_type={field_type!r}. count={field_count!r}")
       +                    _write_field(fd=fd,
       +                                 field_type=field_type,
       +                                 count=field_count,
       +                                 value=field_value)
       +                    #print(f">>> encode_msg. so far: {fd.getvalue().hex()}")
       +                else:
       +                    raise Exception(f"unexpected row in scheme: {row!r}")
       +            return fd.getvalue()
       +
       +    def decode_msg(self, data: bytes) -> Tuple[str, dict]:
                """
                Decode Lightning message by reading the first
                two bytes to determine message type.
        
                Returns message type string and parsed message contents dict
                """
       -        typ = data[:2]
       -        k, parsed = self.message_types[typ](data[2:])
       -        return k, parsed
       +        #print(f"decode_msg >>> {data.hex()}")
       +        assert len(data) >= 2
       +        msg_type_bytes = data[:2]
       +        msg_type_int = int.from_bytes(msg_type_bytes, byteorder="big", signed=False)
       +        scheme = self.msg_scheme_from_type[msg_type_bytes]
       +        assert scheme[0][2] == msg_type_int
       +        msg_type_name = scheme[0][1]
       +        parsed = {}
       +        with io.BytesIO(data[2:]) as fd:
       +            for row in scheme:
       +                #print(f"row: {row!r}")
       +                if row[0] == "msgtype":
       +                    pass
       +                elif row[0] == "msgdata":
       +                    field_name = row[2]
       +                    field_type = row[3]
       +                    field_count_str = row[4]
       +                    field_count = _resolve_field_count(field_count_str, vars_dict=parsed)
       +                    if field_name == "tlvs":
       +                        tlv_stream_name = field_type
       +                        d = self.read_tlv_stream(fd=fd, tlv_stream_name=tlv_stream_name)
       +                        parsed[tlv_stream_name] = d
       +                        continue
       +                    #print(f">> count={field_count}. parsed={parsed}")
       +                    try:
       +                        parsed[field_name] = _read_field(fd=fd,
       +                                                         field_type=field_type,
       +                                                         count=field_count)
       +                    except UnexpectedEndOfStream as e:
       +                        if len(row) > 5:
       +                            break  # optional feature field not present
       +                        else:
       +                            raise
       +                else:
       +                    raise Exception(f"unexpected row in scheme: {row!r}")
       +        return msg_type_name, parsed
       +
        
        _inst = LNSerializer()
        encode_msg = _inst.encode_msg
        decode_msg = _inst.decode_msg
       +
       +
       +OnionWireSerializer = LNSerializer(for_onion_wire=True)
   DIR diff --git a/electrum/lnonion.py b/electrum/lnonion.py
       t@@ -23,6 +23,7 @@
        # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        # SOFTWARE.
        
       +import io
        import hashlib
        from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING
        from enum import IntEnum, IntFlag
       t@@ -31,15 +32,16 @@ from . import ecc
        from .crypto import sha256, hmac_oneshot, chacha20_encrypt
        from .util import bh2u, profiler, xor_bytes, bfh
        from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH,
       -                     NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID)
       +                     NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, OnionFailureCodeMetaFlag)
       +from .lnmsg import OnionWireSerializer, read_bigsize_int, write_bigsize_int
        
        if TYPE_CHECKING:
            from .lnrouter import LNPaymentRoute
        
        
        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
       +LEGACY_PER_HOP_FULL_SIZE = 65
       +NUM_STREAM_BYTES = 2 * HOPS_DATA_SIZE
        PER_HOP_HMAC_SIZE = 32
        
        
       t@@ -48,64 +50,127 @@ class InvalidOnionMac(Exception): pass
        class InvalidOnionPubkey(Exception): pass
        
        
       -class OnionPerHop:
       +class LegacyHopDataPayload:
        
       -    def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes):
       +    def __init__(self, *, short_channel_id: bytes, amt_to_forward: int, outgoing_cltv_value: int):
                self.short_channel_id = ShortChannelID(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 += int.to_bytes(self.amt_to_forward, length=8, byteorder="big", signed=False)
       +        ret += int.to_bytes(self.outgoing_cltv_value, length=4, byteorder="big", signed=False)
                ret += bytes(12)  # padding
                if len(ret) != 32:
                    raise Exception('unexpected length {}'.format(len(ret)))
                return ret
        
       +    def to_tlv_dict(self) -> dict:
       +        d = {
       +            "amt_to_forward": {"amt_to_forward": self.amt_to_forward},
       +            "outgoing_cltv_value": {"outgoing_cltv_value": self.outgoing_cltv_value},
       +            "short_channel_id": {"short_channel_id": self.short_channel_id},
       +        }
       +        return d
       +
            @classmethod
       -    def from_bytes(cls, b: bytes):
       +    def from_bytes(cls, b: bytes) -> 'LegacyHopDataPayload':
                if len(b) != 32:
                    raise Exception('unexpected length {}'.format(len(b)))
       -        return OnionPerHop(
       +        return LegacyHopDataPayload(
                    short_channel_id=b[:8],
       -            amt_to_forward=b[8:16],
       -            outgoing_cltv_value=b[16:20]
       +            amt_to_forward=int.from_bytes(b[8:16], byteorder="big", signed=False),
       +            outgoing_cltv_value=int.from_bytes(b[16:20], byteorder="big", signed=False),
       +        )
       +
       +    @classmethod
       +    def from_tlv_dict(cls, d: dict) -> 'LegacyHopDataPayload':
       +        return LegacyHopDataPayload(
       +            short_channel_id=d["short_channel_id"]["short_channel_id"] if "short_channel_id" in d else b"\x00" * 8,
       +            amt_to_forward=d["amt_to_forward"]["amt_to_forward"],
       +            outgoing_cltv_value=d["outgoing_cltv_value"]["outgoing_cltv_value"],
                )
        
        
        class OnionHopsDataSingle:  # called HopData in lnd
        
       -    def __init__(self, per_hop: OnionPerHop = None):
       -        self.realm = 0
       -        self.per_hop = per_hop
       +    def __init__(self, *, is_tlv_payload: bool, payload: dict = None):
       +        self.is_tlv_payload = is_tlv_payload
       +        if payload is None:
       +            payload = {}
       +        self.payload = payload
                self.hmac = None
       +        self._raw_bytes_payload = None  # used in unit tests
        
            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
       +        hmac_ = self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE)
       +        if self._raw_bytes_payload is not None:
       +            ret = write_bigsize_int(len(self._raw_bytes_payload))
       +            ret += self._raw_bytes_payload
       +            ret += hmac_
       +            return ret
       +        if not self.is_tlv_payload:
       +            ret = b"\x00"  # realm==0
       +            legacy_payload = LegacyHopDataPayload.from_tlv_dict(self.payload)
       +            ret += legacy_payload.to_bytes()
       +            ret += hmac_
       +            if len(ret) != LEGACY_PER_HOP_FULL_SIZE:
       +                raise Exception('unexpected length {}'.format(len(ret)))
       +            return ret
       +        else:  # tlv
       +            payload_fd = io.BytesIO()
       +            OnionWireSerializer.write_tlv_stream(fd=payload_fd,
       +                                                 tlv_stream_name="tlv_payload",
       +                                                 **self.payload)
       +            payload_bytes = payload_fd.getvalue()
       +            with io.BytesIO() as fd:
       +                fd.write(write_bigsize_int(len(payload_bytes)))
       +                fd.write(payload_bytes)
       +                fd.write(hmac_)
       +                return fd.getvalue()
        
            @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
       +    def from_fd(cls, fd: io.BytesIO) -> 'OnionHopsDataSingle':
       +        first_byte = fd.read(1)
       +        if len(first_byte) == 0:
       +            raise Exception(f"unexpected EOF")
       +        fd.seek(-1, io.SEEK_CUR)  # undo read
       +        if first_byte == b'\x00':
       +            # legacy hop data format
       +            b = fd.read(LEGACY_PER_HOP_FULL_SIZE)
       +            if len(b) != LEGACY_PER_HOP_FULL_SIZE:
       +                raise Exception(f'unexpected length {len(b)}')
       +            ret = OnionHopsDataSingle(is_tlv_payload=False)
       +            legacy_payload = LegacyHopDataPayload.from_bytes(b[1:33])
       +            ret.payload = legacy_payload.to_tlv_dict()
       +            ret.hmac = b[33:]
       +            return ret
       +        elif first_byte == b'\x01':
       +            # reserved for future use
       +            raise Exception("unsupported hop payload: length==1")
       +        else:
       +            hop_payload_length = read_bigsize_int(fd)
       +            hop_payload = fd.read(hop_payload_length)
       +            if hop_payload_length != len(hop_payload):
       +                raise Exception(f"unexpected EOF")
       +            ret = OnionHopsDataSingle(is_tlv_payload=True)
       +            ret.payload = OnionWireSerializer.read_tlv_stream(fd=io.BytesIO(hop_payload),
       +                                                              tlv_stream_name="tlv_payload")
       +            ret.hmac = fd.read(PER_HOP_HMAC_SIZE)
       +            assert len(ret.hmac) == PER_HOP_HMAC_SIZE
       +            return ret
       +
       +    def __repr__(self):
       +        return f"<OnionHopsDataSingle. is_tlv_payload={self.is_tlv_payload}. payload={self.payload}. hmac={self.hmac}>"
        
        
        class OnionPacket:
        
            def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes):
       +        assert len(public_key) == 33
       +        assert len(hops_data) == HOPS_DATA_SIZE
       +        assert len(hmac) == PER_HOP_HMAC_SIZE
                self.version = 0
                self.public_key = public_key
                self.hops_data = hops_data  # also called RoutingInfo in bolt-04
       t@@ -163,13 +228,14 @@ def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes],
        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)
       +    assert num_hops == len(hops_data)
            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)
       +    filler = _generate_filler(b'rho', hops_data, hop_shared_secrets)
            next_hmac = bytes(PER_HOP_HMAC_SIZE)
        
            # Our starting packet needs to be filled out with random bytes, we
       -    # generate some determinstically using the session private key.
       +    # generate some deterministically using the session private key.
            pad_key = get_bolt04_onion_key(b'pad', session_key)
            mix_header = generate_cipher_stream(pad_key, HOPS_DATA_SIZE)
        
       t@@ -178,9 +244,10 @@ def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
                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
       +        stream_bytes = generate_cipher_stream(rho_key, HOPS_DATA_SIZE)
       +        hop_data_bytes = hops_data[i].to_bytes()
       +        mix_header = mix_header[:-len(hop_data_bytes)]
       +        mix_header = hop_data_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
       t@@ -193,7 +260,8 @@ def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
                hmac=next_hmac)
        
        
       -def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int, final_cltv: int) \
       +def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int,
       +                               final_cltv: int, *, payment_secret: bytes = None) \
                -> Tuple[List[OnionHopsDataSingle], int, int]:
            """Returns the hops_data to be used for constructing an onion packet,
            and the amount_msat and cltv to be used on our immediate channel.
       t@@ -201,34 +269,59 @@ def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int, final_
            if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
                raise PaymentFailure(f"too long route ({len(route)} edges)")
        
       +    # payload that will be seen by the last hop:
            amt = amount_msat
            cltv = final_cltv
       -    hops_data = [OnionHopsDataSingle(OnionPerHop(b"\x00" * 8,
       -                                                 amt.to_bytes(8, "big"),
       -                                                 cltv.to_bytes(4, "big")))]
       -    for route_edge in reversed(route[1:]):
       -        hops_data += [OnionHopsDataSingle(OnionPerHop(route_edge.short_channel_id,
       -                                                      amt.to_bytes(8, "big"),
       -                                                      cltv.to_bytes(4, "big")))]
       +    hop_payload = {
       +        "amt_to_forward": {"amt_to_forward": amt},
       +        "outgoing_cltv_value": {"outgoing_cltv_value": cltv},
       +    }
       +    if payment_secret is not None:
       +        hop_payload["payment_data"] = {"payment_secret": payment_secret, "total_msat": amt}
       +    hops_data = [OnionHopsDataSingle(is_tlv_payload=route[-1].has_feature_varonion(),
       +                                     payload=hop_payload)]
       +    # payloads, backwards from last hop (but excluding the first edge):
       +    for edge_index in range(len(route) - 1, 0, -1):
       +        route_edge = route[edge_index]
       +        hop_payload = {
       +            "amt_to_forward": {"amt_to_forward": amt},
       +            "outgoing_cltv_value": {"outgoing_cltv_value": cltv},
       +            "short_channel_id": {"short_channel_id": route_edge.short_channel_id},
       +        }
       +        hops_data += [OnionHopsDataSingle(is_tlv_payload=route[edge_index-1].has_feature_varonion(),
       +                                          payload=hop_payload)]
                amt += route_edge.fee_for_edge(amt)
                cltv += route_edge.cltv_expiry_delta
            hops_data.reverse()
            return hops_data, amt, cltv
        
        
       -def generate_filler(key_type: bytes, num_hops: int, hop_size: int,
       -                    shared_secrets: Sequence[bytes]) -> bytes:
       -    filler_size = (NUM_MAX_HOPS_IN_PAYMENT_PATH + 1) * hop_size
       +def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],
       +                     shared_secrets: Sequence[bytes]) -> bytes:
       +    num_hops = len(hops_data)
       +
       +    # generate filler that matches all but the last hop (no HMAC for last hop)
       +    filler_size = 0
       +    for hop_data in hops_data[:-1]:
       +        filler_size += len(hop_data.to_bytes())
            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)
       +        # Sum up how many frames were used by prior hops.
       +        filler_start = HOPS_DATA_SIZE
       +        for hop_data in hops_data[:i]:
       +            filler_start -= len(hop_data.to_bytes())
       +        # The filler is the part dangling off of the end of the
       +        # routingInfo, so offset it from there, and use the current
       +        # hop's frame count as its size.
       +        filler_end = HOPS_DATA_SIZE + len(hops_data[i].to_bytes())
       +
                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)
       +        stream_bytes = generate_cipher_stream(stream_key, NUM_STREAM_BYTES)
       +        filler = xor_bytes(filler, stream_bytes[filler_start:filler_end])
       +        filler += bytes(filler_size - len(filler))  # right pad with zeroes
        
       -    return filler[(NUM_MAX_HOPS_IN_PAYMENT_PATH-num_hops+2)*hop_size:]
       +    return filler
        
        
        def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes:
       t@@ -260,8 +353,9 @@ def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
            # 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)
       +    padded_header = onion_packet.hops_data + bytes(HOPS_DATA_SIZE)
            next_hops_data = xor_bytes(padded_header, stream_bytes)
       +    next_hops_data_fd = io.BytesIO(next_hops_data)
        
            # calc next ephemeral key
            blinding_factor = sha256(onion_packet.public_key + shared_secret)
       t@@ -269,10 +363,10 @@ def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
            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])
       +    hop_data = OnionHopsDataSingle.from_fd(next_hops_data_fd)
            next_onion_packet = OnionPacket(
                public_key=next_public_key,
       -        hops_data=next_hops_data[PER_HOP_FULL_SIZE:],
       +        hops_data=next_hops_data_fd.read(HOPS_DATA_SIZE),
                hmac=hop_data.hmac
            )
            if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):
       t@@ -365,12 +459,7 @@ def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRout
            return OnionRoutingFailureMessage(failure_code, failure_data)
        
        
       -class OnionFailureCodeMetaFlag(IntFlag):
       -    BADONION = 0x8000
       -    PERM     = 0x4000
       -    NODE     = 0x2000
       -    UPDATE   = 0x1000
       -
       +# TODO maybe we should rm this and just use OnionWireSerializer and onion_wire.csv
        BADONION = OnionFailureCodeMetaFlag.BADONION
        PERM     = OnionFailureCodeMetaFlag.PERM
        NODE     = OnionFailureCodeMetaFlag.NODE
       t@@ -398,6 +487,7 @@ class OnionFailureCode(IntEnum):
            FINAL_INCORRECT_HTLC_AMOUNT =             19
            CHANNEL_DISABLED =                        UPDATE | 20
            EXPIRY_TOO_FAR =                          21
       +    INVALID_ONION_PAYLOAD =                   PERM | 22
        
        
        # don't use these elsewhere, the names are ambiguous without context
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -37,14 +37,14 @@ from . import lnutil
        from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
                             RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
                             funding_output_script, get_per_commitment_secret_from_seed,
       -                     secret_to_pubkey, PaymentFailure, LnLocalFeatures,
       +                     secret_to_pubkey, PaymentFailure, LnFeatures,
                             LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily,
                             ln_compare_features, privkey_to_pubkey, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_ACCEPTED,
                             LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate,
                             MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED,
                             MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY,
                             NBLOCK_OUR_CLTV_EXPIRY_DELTA, format_short_channel_id, ShortChannelID,
       -                     IncompatibleLightningFeatures)
       +                     IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage)
        from .lnutil import FeeUpdate
        from .lntransport import LNTransport, LNTransportBase
        from .lnmsg import encode_msg, decode_msg
       t@@ -77,7 +77,7 @@ class Peer(Logger):
                self.pubkey = pubkey  # remote pubkey
                self.lnworker = lnworker
                self.privkey = lnworker.node_keypair.privkey  # local privkey
       -        self.localfeatures = self.lnworker.localfeatures
       +        self.features = self.lnworker.features
                self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]
                self.network = lnworker.network
                self.channel_db = lnworker.network.channel_db
       t@@ -131,7 +131,12 @@ class Peer(Logger):
            async def initialize(self):
                if isinstance(self.transport, LNTransport):
                    await self.transport.handshake()
       -        self.send_message("init", gflen=0, lflen=2, localfeatures=self.localfeatures)
       +        # FIXME: "flen" hardcoded but actually it depends on "features"...:
       +        self.send_message("init", gflen=0, flen=2, features=self.features.for_init_message(),
       +                          init_tlvs={
       +                              'networks':
       +                                  {'chains': constants.net.rev_genesis_bytes()}
       +                          })
                self._sent_init = True
                self.maybe_set_initialized()
        
       t@@ -180,7 +185,7 @@ class Peer(Logger):
                self.ordered_message_queues[chan_id].put_nowait((None, {'error':payload['data']}))
        
            def on_ping(self, payload):
       -        l = int.from_bytes(payload['num_pong_bytes'], 'big')
       +        l = payload['num_pong_bytes']
                self.send_message('pong', byteslen=l)
        
            def on_pong(self, payload):
       t@@ -199,14 +204,25 @@ class Peer(Logger):
                if self._received_init:
                    self.logger.info("ALREADY INITIALIZED BUT RECEIVED INIT")
                    return
       -        # if they required some even flag we don't have, they will close themselves
       -        # but if we require an even flag they don't have, we close
       -        their_localfeatures = int.from_bytes(payload['localfeatures'], byteorder="big")
       +        their_features = LnFeatures(int.from_bytes(payload['features'], byteorder="big"))
       +        their_globalfeatures = int.from_bytes(payload['globalfeatures'], byteorder="big")
       +        their_features |= their_globalfeatures
       +        # check transitive dependencies for received features
       +        if not their_features.validate_transitive_dependecies():
       +            raise GracefulDisconnect("remote did not set all dependencies for the features they sent")
       +        # check if features are compatible, and set self.features to what we negotiated
                try:
       -            self.localfeatures = ln_compare_features(self.localfeatures, their_localfeatures)
       +            self.features = ln_compare_features(self.features, their_features)
                except IncompatibleLightningFeatures as e:
                    self.initialized.set_exception(e)
                    raise GracefulDisconnect(f"{str(e)}")
       +        # check that they are on the same chain as us, if provided
       +        their_networks = payload["init_tlvs"].get("networks")
       +        if their_networks:
       +            their_chains = list(chunks(their_networks["chains"], 32))
       +            if constants.net.rev_genesis_bytes() not in their_chains:
       +                raise GracefulDisconnect(f"no common chain found with remote. (they sent: {their_chains})")
       +        # all checks passed
                if isinstance(self.transport, LNTransport):
                    self.channel_db.add_recent_peer(self.transport.peer_addr)
                    for chan in self.channels.values():
       t@@ -417,8 +433,8 @@ class Peer(Logger):
                return ids
        
            def on_reply_channel_range(self, payload):
       -        first = int.from_bytes(payload['first_blocknum'], 'big')
       -        num = int.from_bytes(payload['number_of_blocks'], 'big')
       +        first = payload['first_blocknum']
       +        num = payload['number_of_blocks']
                complete = bool(int.from_bytes(payload['complete'], 'big'))
                encoded = payload['encoded_short_ids']
                ids = self.decode_short_ids(encoded)
       t@@ -465,7 +481,7 @@ class Peer(Logger):
                self.lnworker.peer_closed(self)
        
            def is_static_remotekey(self):
       -        return bool(self.localfeatures & LnLocalFeatures.OPTION_STATIC_REMOTEKEY_OPT)
       +        return bool(self.features & LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
        
            def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> LocalConfig:
                # key derivation
       t@@ -541,27 +557,27 @@ class Peer(Logger):
                )
                payload = await self.wait_for_message('accept_channel', temp_channel_id)
                remote_per_commitment_point = payload['first_per_commitment_point']
       -        funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big')
       +        funding_txn_minimum_depth = payload['minimum_depth']
                if funding_txn_minimum_depth <= 0:
                    raise Exception(f"minimum depth too low, {funding_txn_minimum_depth}")
                if funding_txn_minimum_depth > 30:
                    raise Exception(f"minimum depth too high, {funding_txn_minimum_depth}")
       -        remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big')
       +        remote_dust_limit_sat = payload['dust_limit_satoshis']
                remote_reserve_sat = self.validate_remote_reserve(payload["channel_reserve_satoshis"], remote_dust_limit_sat, funding_sat)
                if remote_dust_limit_sat > remote_reserve_sat:
                    raise Exception(f"Remote Lightning peer reports dust_limit_sat > reserve_sat which is a BOLT-02 protocol violation.")
       -        htlc_min = int.from_bytes(payload['htlc_minimum_msat'], 'big')
       +        htlc_min = payload['htlc_minimum_msat']
                if htlc_min > MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED:
                    raise Exception(f"Remote Lightning peer reports htlc_minimum_msat={htlc_min} mSAT," +
                            f" which is above Electrums required maximum limit of that parameter ({MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED} mSAT).")
       -        remote_max = int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big')
       +        remote_max = payload['max_htlc_value_in_flight_msat']
                if remote_max < MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED:
                    raise Exception(f"Remote Lightning peer reports max_htlc_value_in_flight_msat at only {remote_max} mSAT" +
                            f" which is below Electrums required minimum ({MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED} mSAT).")
       -        max_accepted_htlcs = int.from_bytes(payload["max_accepted_htlcs"], 'big')
       +        max_accepted_htlcs = payload["max_accepted_htlcs"]
                if max_accepted_htlcs > 483:
                    raise Exception("Remote Lightning peer reports max_accepted_htlcs > 483, which is a BOLT-02 protocol violation.")
       -        remote_to_self_delay = int.from_bytes(payload['to_self_delay'], byteorder='big')
       +        remote_to_self_delay = payload['to_self_delay']
                if remote_to_self_delay > MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED:
                    raise Exception(f"Remote Lightning peer reports to_self_delay={remote_to_self_delay}," +
                            f" which is above Electrums required maximum ({MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED})")
       t@@ -647,9 +663,9 @@ class Peer(Logger):
                # payload['channel_flags']
                if payload['chain_hash'] != constants.net.rev_genesis_bytes():
                    raise Exception('wrong chain_hash')
       -        funding_sat = int.from_bytes(payload['funding_satoshis'], 'big')
       -        push_msat = int.from_bytes(payload['push_msat'], 'big')
       -        feerate = int.from_bytes(payload['feerate_per_kw'], 'big')
       +        funding_sat = payload['funding_satoshis']
       +        push_msat = payload['push_msat']
       +        feerate = payload['feerate_per_kw']
                temp_chan_id = payload['temporary_channel_id']
                local_config = self.make_local_config(funding_sat, push_msat, REMOTE)
                # for the first commitment transaction
       t@@ -674,11 +690,11 @@ class Peer(Logger):
                    first_per_commitment_point=per_commitment_point_first,
                )
                funding_created = await self.wait_for_message('funding_created', temp_chan_id)
       -        funding_idx = int.from_bytes(funding_created['funding_output_index'], 'big')
       +        funding_idx = funding_created['funding_output_index']
                funding_txid = bh2u(funding_created['funding_txid'][::-1])
                channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx)
                remote_balance_sat = funding_sat * 1000 - push_msat
       -        remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') # TODO validate
       +        remote_dust_limit_sat = payload['dust_limit_satoshis']  # TODO validate
                remote_reserve_sat = self.validate_remote_reserve(payload['channel_reserve_satoshis'], remote_dust_limit_sat, funding_sat)
                remote_config = RemoteConfig(
                    payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
       t@@ -686,13 +702,13 @@ class Peer(Logger):
                    htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']),
                    delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']),
                    revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']),
       -            to_self_delay=int.from_bytes(payload['to_self_delay'], 'big'),
       +            to_self_delay=payload['to_self_delay'],
                    dust_limit_sat=remote_dust_limit_sat,
       -            max_htlc_value_in_flight_msat=int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big'), # TODO validate
       -            max_accepted_htlcs=int.from_bytes(payload['max_accepted_htlcs'], 'big'), # TODO validate
       +            max_htlc_value_in_flight_msat=payload['max_htlc_value_in_flight_msat'],  # TODO validate
       +            max_accepted_htlcs=payload['max_accepted_htlcs'],  # TODO validate
                    initial_msat=remote_balance_sat,
                    reserve_sat = remote_reserve_sat,
       -            htlc_minimum_msat=int.from_bytes(payload['htlc_minimum_msat'], 'big'), # TODO validate
       +            htlc_minimum_msat=payload['htlc_minimum_msat'], # TODO validate
                    next_per_commitment_point=payload['first_per_commitment_point'],
                    current_per_commitment_point=None,
                )
       t@@ -718,8 +734,7 @@ class Peer(Logger):
                chan.set_state(channel_states.OPENING)
                self.lnworker.add_new_channel(chan)
        
       -    def validate_remote_reserve(self, payload_field: bytes, dust_limit: int, funding_sat: int) -> int:
       -        remote_reserve_sat = int.from_bytes(payload_field, 'big')
       +    def validate_remote_reserve(self, remote_reserve_sat: int, dust_limit: int, funding_sat: int) -> int:
                if remote_reserve_sat < dust_limit:
                    raise Exception('protocol violation: reserve < dust_limit')
                if remote_reserve_sat > funding_sat/100:
       t@@ -745,7 +760,7 @@ class Peer(Logger):
                oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)
                latest_remote_ctn = chan.get_latest_ctn(REMOTE)
                next_remote_ctn = chan.get_next_ctn(REMOTE)
       -        assert self.localfeatures & LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        assert self.features & LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
                # send message
                srk_enabled = chan.is_static_remotekey_enabled()
                if srk_enabled:
       t@@ -760,16 +775,16 @@ class Peer(Logger):
                self.send_message(
                    "channel_reestablish",
                    channel_id=chan_id,
       -            next_local_commitment_number=next_local_ctn,
       -            next_remote_revocation_number=oldest_unrevoked_remote_ctn,
       +            next_commitment_number=next_local_ctn,
       +            next_revocation_number=oldest_unrevoked_remote_ctn,
                    your_last_per_commitment_secret=last_rev_secret,
                    my_current_per_commitment_point=latest_point)
                self.logger.info(f'channel_reestablish ({chan.get_id_for_log()}): sent channel_reestablish with '
                                 f'(next_local_ctn={next_local_ctn}, '
                                 f'oldest_unrevoked_remote_ctn={oldest_unrevoked_remote_ctn})')
                msg = await self.wait_for_message('channel_reestablish', chan_id)
       -        their_next_local_ctn = int.from_bytes(msg["next_local_commitment_number"], 'big')
       -        their_oldest_unrevoked_remote_ctn = int.from_bytes(msg["next_remote_revocation_number"], 'big')
       +        their_next_local_ctn = msg["next_commitment_number"]
       +        their_oldest_unrevoked_remote_ctn = msg["next_revocation_number"]
                their_local_pcp = msg.get("my_current_per_commitment_point")
                their_claim_of_our_last_per_commitment_secret = msg.get("your_last_per_commitment_secret")
                self.logger.info(f'channel_reestablish ({chan.get_id_for_log()}): received channel_reestablish with '
       t@@ -818,7 +833,7 @@ class Peer(Logger):
                if oldest_unrevoked_local_ctn != their_oldest_unrevoked_remote_ctn:
                    if oldest_unrevoked_local_ctn - 1 == their_oldest_unrevoked_remote_ctn:
                        # A node:
       -                #    if next_remote_revocation_number is equal to the commitment number of the last revoke_and_ack
       +                #    if next_revocation_number is equal to the commitment number of the last revoke_and_ack
                        #    the receiving node sent, AND the receiving node hasn't already received a closing_signed:
                        #        MUST re-send the revoke_and_ack.
                        last_secret, last_point = chan.get_secret_and_point(LOCAL, oldest_unrevoked_local_ctn - 1)
       t@@ -1005,7 +1020,7 @@ class Peer(Logger):
                return msg_hash, node_signature, bitcoin_signature
        
            def on_update_fail_htlc(self, chan: Channel, payload):
       -        htlc_id = int.from_bytes(payload["id"], "big")
       +        htlc_id = payload["id"]
                reason = payload["reason"]
                self.logger.info(f"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
                chan.receive_fail_htlc(htlc_id, error_bytes=reason)  # TODO handle exc and maybe fail channel (e.g. bad htlc_id)
       t@@ -1022,15 +1037,19 @@ class Peer(Logger):
                sig_64, htlc_sigs = chan.sign_next_commitment()
                self.send_message("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs))
        
       -    def pay(self, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
       -            payment_hash: bytes, min_final_cltv_expiry: int) -> UpdateAddHtlc:
       +    def pay(self, *, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
       +            payment_hash: bytes, min_final_cltv_expiry: int, payment_secret: bytes = None) -> UpdateAddHtlc:
                assert amount_msat > 0, "amount_msat is not greater zero"
       +        assert len(route) > 0
                if not chan.can_send_update_add_htlc():
                    raise PaymentFailure("Channel cannot send update_add_htlc")
       +        # add features learned during "init" for direct neighbour:
       +        route[0].node_features |= self.features
                local_height = self.network.get_local_height()
                # create onion packet
                final_cltv = local_height + min_final_cltv_expiry
       -        hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv)
       +        hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv,
       +                                                                  payment_secret=payment_secret)
                assert final_cltv <= cltv, (final_cltv, cltv)
                secret_key = os.urandom(32)
                onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash)
       t@@ -1040,7 +1059,8 @@ class Peer(Logger):
                htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time()))
                htlc = chan.add_htlc(htlc)
                chan.set_onion_key(htlc.htlc_id, secret_key)
       -        self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. htlc: {htlc}")
       +        self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. "
       +                         f"htlc: {htlc}. hops_data={hops_data!r}")
                self.send_message(
                    "update_add_htlc",
                    channel_id=chan.channel_id,
       t@@ -1083,7 +1103,7 @@ class Peer(Logger):
            def on_update_fulfill_htlc(self, chan: Channel, payload):
                preimage = payload["payment_preimage"]
                payment_hash = sha256(preimage)
       -        htlc_id = int.from_bytes(payload["id"], "big")
       +        htlc_id = payload["id"]
                self.logger.info(f"on_update_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
                chan.receive_htlc_settle(preimage, htlc_id)  # TODO handle exc and maybe fail channel (e.g. bad htlc_id)
                self.lnworker.save_preimage(payment_hash, preimage)
       t@@ -1103,10 +1123,10 @@ class Peer(Logger):
        
            def on_update_add_htlc(self, chan: Channel, payload):
                payment_hash = payload["payment_hash"]
       -        htlc_id = int.from_bytes(payload["id"], 'big')
       +        htlc_id = payload["id"]
                self.logger.info(f"on_update_add_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
       -        cltv_expiry = int.from_bytes(payload["cltv_expiry"], 'big')
       -        amount_msat_htlc = int.from_bytes(payload["amount_msat"], 'big')
       +        cltv_expiry = payload["cltv_expiry"]
       +        amount_msat_htlc = payload["amount_msat"]
                onion_packet = payload["onion_routing_packet"]
                if chan.get_state() != channel_states.OPEN:
                    raise RemoteMisbehaving(f"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()}")
       t@@ -1130,9 +1150,11 @@ class Peer(Logger):
                if not forwarding_enabled:
                    self.logger.info(f"forwarding is disabled. failing htlc.")
                    return OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
       -        dph = processed_onion.hop_data.per_hop
       -        next_chan = self.lnworker.get_channel_by_short_id(dph.short_channel_id)
       -        next_chan_scid = dph.short_channel_id
       +        try:
       +            next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"]
       +        except:
       +            return OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       +        next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid)
                local_height = self.network.get_local_height()
                if next_chan is None:
                    self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}")
       t@@ -1144,7 +1166,10 @@ class Peer(Logger):
                                     f"chan state {next_chan.get_state()}, peer state: {next_chan.peer_state}")
                    data = outgoing_chan_upd_len + outgoing_chan_upd
                    return OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
       -        next_cltv_expiry = int.from_bytes(dph.outgoing_cltv_value, 'big')
       +        try:
       +            next_cltv_expiry = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
       +        except:
       +            return OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
                if htlc.cltv_expiry - next_cltv_expiry < NBLOCK_OUR_CLTV_EXPIRY_DELTA:
                    data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd
                    return OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data)
       t@@ -1154,7 +1179,10 @@ class Peer(Logger):
                    return OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_SOON, data=data)
                if max(htlc.cltv_expiry, next_cltv_expiry) > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
                    return OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'')
       -        next_amount_msat_htlc = int.from_bytes(dph.amt_to_forward, 'big')
       +        try:
       +            next_amount_msat_htlc = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
       +        except:
       +            return OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
                forwarding_fees = fee_for_edge_msat(
                    forwarded_amount_msat=next_amount_msat_htlc,
                    fee_base_msat=lnutil.OUR_FEE_BASE_MSAT,
       t@@ -1175,8 +1203,8 @@ class Peer(Logger):
                        "update_add_htlc",
                        channel_id=next_chan.channel_id,
                        id=next_htlc.htlc_id,
       -                cltv_expiry=dph.outgoing_cltv_value,
       -                amount_msat=dph.amt_to_forward,
       +                cltv_expiry=next_cltv_expiry,
       +                amount_msat=next_amount_msat_htlc,
                        payment_hash=next_htlc.payment_hash,
                        onion_routing_packet=processed_onion.next_packet.to_bytes()
                    )
       t@@ -1194,6 +1222,14 @@ class Peer(Logger):
                except UnknownPaymentHash:
                    reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
                    return False, reason
       +        try:
       +            payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
       +        except:
       +            pass  # skip
       +        else:
       +            if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
       +                reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
       +                return False, reason
                expected_received_msat = int(info.amount * 1000) if info.amount is not None else None
                if expected_received_msat is not None and \
                        not (expected_received_msat <= htlc.amount_msat <= 2 * expected_received_msat):
       t@@ -1203,12 +1239,24 @@ class Peer(Logger):
                if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry:
                    reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'')
                    return False, reason
       -        cltv_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.outgoing_cltv_value, byteorder="big")
       +        try:
       +            cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
       +        except:
       +            reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       +            return False, reason
                if cltv_from_onion != htlc.cltv_expiry:
                    reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
                                                        data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
                    return False, reason
       -        amount_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.amt_to_forward, byteorder="big")
       +        try:
       +            amount_from_onion = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
       +        except:
       +            reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
       +            return False, reason
       +        try:
       +            amount_from_onion = processed_onion.hop_data.payload["payment_data"]["total_msat"]
       +        except:
       +            pass  # fall back to "amt_to_forward"
                if amount_from_onion > htlc.amount_msat:
                    reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
                                                        data=htlc.amount_msat.to_bytes(8, byteorder="big"))
       t@@ -1258,7 +1306,7 @@ class Peer(Logger):
                self.maybe_send_commitment(chan)
        
            def on_update_fee(self, chan: Channel, payload):
       -        feerate = int.from_bytes(payload["feerate_per_kw"], "big")
       +        feerate = payload["feerate_per_kw"]
                chan.update_fee(feerate, False)
        
            async def maybe_update_fee(self, chan: Channel):
       t@@ -1378,7 +1426,7 @@ class Peer(Logger):
                while True:
                    # FIXME: the remote SHOULD send closing_signed, but some don't.
                    cs_payload = await self.wait_for_message('closing_signed', chan.channel_id)
       -            their_fee = int.from_bytes(cs_payload['fee_satoshis'], 'big')
       +            their_fee = cs_payload['fee_satoshis']
                    if their_fee > max_fee:
                        raise Exception(f'the proposed fee exceeds the base fee of the latest commitment transaction {is_local, their_fee, max_fee}')
                    their_sig = cs_payload['signature']
       t@@ -1445,6 +1493,9 @@ class Peer(Logger):
                                error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_KEY, data=sha256(onion_packet_bytes))
                            except InvalidOnionMac:
                                error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_HMAC, data=sha256(onion_packet_bytes))
       +                    except Exception as e:
       +                        self.logger.info(f"error processing onion packet: {e!r}")
       +                        error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
                            else:
                                if processed_onion.are_we_final:
                                    preimage, error_reason = self.maybe_fulfill_htlc(
   DIR diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py
       t@@ -27,11 +27,13 @@ import queue
        from collections import defaultdict
        from typing import Sequence, List, Tuple, Optional, Dict, NamedTuple, TYPE_CHECKING, Set
        
       +import attr
       +
        from .util import bh2u, profiler
        from .logging import Logger
       -from .lnutil import NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID
       -from .channel_db import ChannelDB, Policy
       -from .lnutil import NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE
       +from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures,
       +                     NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE)
       +from .channel_db import ChannelDB, Policy, NodeInfo
        
        if TYPE_CHECKING:
            from .lnchannel import Channel
       t@@ -48,13 +50,15 @@ def fee_for_edge_msat(forwarded_amount_msat: int, fee_base_msat: int, fee_propor
                   + (forwarded_amount_msat * fee_proportional_millionths // 1_000_000)
        
        
       -class RouteEdge(NamedTuple):
       +@attr.s
       +class RouteEdge:
            """if you travel through short_channel_id, you will reach node_id"""
       -    node_id: bytes
       -    short_channel_id: ShortChannelID
       -    fee_base_msat: int
       -    fee_proportional_millionths: int
       -    cltv_expiry_delta: int
       +    node_id = attr.ib(type=bytes, kw_only=True)
       +    short_channel_id = attr.ib(type=ShortChannelID, kw_only=True)
       +    fee_base_msat = attr.ib(type=int, kw_only=True)
       +    fee_proportional_millionths = attr.ib(type=int, kw_only=True)
       +    cltv_expiry_delta = attr.ib(type=int, kw_only=True)
       +    node_features = attr.ib(type=int, kw_only=True)  # note: for end node!
        
            def fee_for_edge(self, amount_msat: int) -> int:
                return fee_for_edge_msat(forwarded_amount_msat=amount_msat,
       t@@ -63,14 +67,16 @@ class RouteEdge(NamedTuple):
        
            @classmethod
            def from_channel_policy(cls, channel_policy: 'Policy',
       -                            short_channel_id: bytes, end_node: bytes) -> 'RouteEdge':
       +                            short_channel_id: bytes, end_node: bytes, *,
       +                            node_info: Optional[NodeInfo]) -> 'RouteEdge':
                assert isinstance(short_channel_id, bytes)
                assert type(end_node) is bytes
       -        return RouteEdge(end_node,
       -                         ShortChannelID.normalize(short_channel_id),
       -                         channel_policy.fee_base_msat,
       -                         channel_policy.fee_proportional_millionths,
       -                         channel_policy.cltv_expiry_delta)
       +        return RouteEdge(node_id=end_node,
       +                         short_channel_id=ShortChannelID.normalize(short_channel_id),
       +                         fee_base_msat=channel_policy.fee_base_msat,
       +                         fee_proportional_millionths=channel_policy.fee_proportional_millionths,
       +                         cltv_expiry_delta=channel_policy.cltv_expiry_delta,
       +                         node_features=node_info.features if node_info else 0)
        
            def is_sane_to_use(self, amount_msat: int) -> bool:
                # TODO revise ad-hoc heuristics
       t@@ -82,6 +88,10 @@ class RouteEdge(NamedTuple):
                    return False
                return True
        
       +    def has_feature_varonion(self) -> bool:
       +        features = self.node_features
       +        return bool(features & LnFeatures.VAR_ONION_REQ or features & LnFeatures.VAR_ONION_OPT)
       +
        
        LNPaymentRoute = Sequence[RouteEdge]
        
       t@@ -154,7 +164,9 @@ class LNPathFinder(Logger):
                if channel_policy.htlc_maximum_msat is not None and \
                        payment_amt_msat > channel_policy.htlc_maximum_msat:
                    return float('inf'), 0  # payment amount too large
       -        route_edge = RouteEdge.from_channel_policy(channel_policy, short_channel_id, end_node)
       +        node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node)
       +        route_edge = RouteEdge.from_channel_policy(channel_policy, short_channel_id, end_node,
       +                                                   node_info=node_info)
                if not route_edge.is_sane_to_use(payment_amt_msat):
                    return float('inf'), 0  # thanks but no thanks
        
       t@@ -268,6 +280,8 @@ class LNPathFinder(Logger):
                                                                         my_channels=my_channels)
                    if channel_policy is None:
                        raise NoChannelPolicy(short_channel_id)
       -            route.append(RouteEdge.from_channel_policy(channel_policy, short_channel_id, node_id))
       +            node_info = self.channel_db.get_node_info_for_node_id(node_id=node_id)
       +            route.append(RouteEdge.from_channel_policy(channel_policy, short_channel_id, node_id,
       +                                                       node_info=node_info))
                    prev_node_id = node_id
                return route
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -3,12 +3,13 @@
        # file LICENCE or http://www.opensource.org/licenses/mit-license.php
        
        from enum import IntFlag, IntEnum
       +import enum
        import json
       -from collections import namedtuple
       +from collections import namedtuple, defaultdict
        from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
        import re
       -import attr
        
       +import attr
        from aiorpcx import NetAddress
        
        from .util import bfh, bh2u, inv_dict, UserFacingException
       t@@ -708,19 +709,137 @@ def get_ecdh(priv: bytes, pub: bytes) -> bytes:
            return sha256(pt.get_public_key_bytes())
        
        
       -class LnLocalFeatures(IntFlag):
       +class LnFeatureContexts(enum.Flag):
       +    INIT = enum.auto()
       +    NODE_ANN = enum.auto()
       +    CHAN_ANN_AS_IS = enum.auto()
       +    CHAN_ANN_ALWAYS_ODD = enum.auto()
       +    CHAN_ANN_ALWAYS_EVEN = enum.auto()
       +    INVOICE = enum.auto()
       +
       +LNFC = LnFeatureContexts
       +
       +_ln_feature_direct_dependencies = defaultdict(set)  # type: Dict[LnFeatures, Set[LnFeatures]]
       +_ln_feature_contexts = {}  # type: Dict[LnFeatures, LnFeatureContexts]
       +
       +class LnFeatures(IntFlag):
            OPTION_DATA_LOSS_PROTECT_REQ = 1 << 0
            OPTION_DATA_LOSS_PROTECT_OPT = 1 << 1
       +    _ln_feature_contexts[OPTION_DATA_LOSS_PROTECT_OPT] = (LNFC.INIT | LnFeatureContexts.NODE_ANN)
       +    _ln_feature_contexts[OPTION_DATA_LOSS_PROTECT_REQ] = (LNFC.INIT | LnFeatureContexts.NODE_ANN)
       +
            INITIAL_ROUTING_SYNC = 1 << 3
       +    _ln_feature_contexts[INITIAL_ROUTING_SYNC] = LNFC.INIT
       +
            OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ = 1 << 4
            OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT = 1 << 5
       +    _ln_feature_contexts[OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
       +    _ln_feature_contexts[OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
       +
            GOSSIP_QUERIES_REQ = 1 << 6
            GOSSIP_QUERIES_OPT = 1 << 7
       +    _ln_feature_contexts[GOSSIP_QUERIES_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
       +    _ln_feature_contexts[GOSSIP_QUERIES_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
       +
       +    VAR_ONION_REQ = 1 << 8
       +    VAR_ONION_OPT = 1 << 9
       +    _ln_feature_contexts[VAR_ONION_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
       +    _ln_feature_contexts[VAR_ONION_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
       +
       +    GOSSIP_QUERIES_EX_REQ = 1 << 10
       +    GOSSIP_QUERIES_EX_OPT = 1 << 11
       +    _ln_feature_direct_dependencies[GOSSIP_QUERIES_EX_OPT] = {GOSSIP_QUERIES_OPT}
       +    _ln_feature_contexts[GOSSIP_QUERIES_EX_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
       +    _ln_feature_contexts[GOSSIP_QUERIES_EX_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
       +
            OPTION_STATIC_REMOTEKEY_REQ = 1 << 12
            OPTION_STATIC_REMOTEKEY_OPT = 1 << 13
       -
       -# note that these are powers of two, not the bits themselves
       -LN_LOCAL_FEATURES_KNOWN_SET = set(LnLocalFeatures)
       +    _ln_feature_contexts[OPTION_STATIC_REMOTEKEY_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
       +    _ln_feature_contexts[OPTION_STATIC_REMOTEKEY_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
       +
       +    PAYMENT_SECRET_REQ = 1 << 14
       +    PAYMENT_SECRET_OPT = 1 << 15
       +    _ln_feature_direct_dependencies[PAYMENT_SECRET_OPT] = {VAR_ONION_OPT}
       +    _ln_feature_contexts[PAYMENT_SECRET_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
       +    _ln_feature_contexts[PAYMENT_SECRET_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
       +
       +    BASIC_MPP_REQ = 1 << 16
       +    BASIC_MPP_OPT = 1 << 17
       +    _ln_feature_direct_dependencies[BASIC_MPP_OPT] = {PAYMENT_SECRET_OPT}
       +    _ln_feature_contexts[BASIC_MPP_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
       +    _ln_feature_contexts[BASIC_MPP_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
       +
       +    OPTION_SUPPORT_LARGE_CHANNEL_REQ = 1 << 18
       +    OPTION_SUPPORT_LARGE_CHANNEL_OPT = 1 << 19
       +    _ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.CHAN_ANN_ALWAYS_EVEN)
       +    _ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.CHAN_ANN_ALWAYS_EVEN)
       +
       +    def validate_transitive_dependecies(self) -> bool:
       +        # for all even bit set, set corresponding odd bit:
       +        features = self  # copy
       +        flags = list_enabled_bits(features)
       +        for flag in flags:
       +            if flag % 2 == 0:
       +                features |= 1 << get_ln_flag_pair_of_bit(flag)
       +        # Check dependencies. We only check that the direct dependencies of each flag set
       +        # are satisfied: this implies that transitive dependencies are also satisfied.
       +        flags = list_enabled_bits(features)
       +        for flag in flags:
       +            for dependency in _ln_feature_direct_dependencies[1 << flag]:
       +                if not (dependency & features):
       +                    return False
       +        return True
       +
       +    def for_init_message(self) -> 'LnFeatures':
       +        features = LnFeatures(0)
       +        for flag in list_enabled_bits(self):
       +            if LnFeatureContexts.INIT & _ln_feature_contexts[1 << flag]:
       +                features |= (1 << flag)
       +        return features
       +
       +    def for_node_announcement(self) -> 'LnFeatures':
       +        features = LnFeatures(0)
       +        for flag in list_enabled_bits(self):
       +            if LnFeatureContexts.NODE_ANN & _ln_feature_contexts[1 << flag]:
       +                features |= (1 << flag)
       +        return features
       +
       +    def for_invoice(self) -> 'LnFeatures':
       +        features = LnFeatures(0)
       +        for flag in list_enabled_bits(self):
       +            if LnFeatureContexts.INVOICE & _ln_feature_contexts[1 << flag]:
       +                features |= (1 << flag)
       +        return features
       +
       +    def for_channel_announcement(self) -> 'LnFeatures':
       +        features = LnFeatures(0)
       +        for flag in list_enabled_bits(self):
       +            ctxs = _ln_feature_contexts[1 << flag]
       +            if LnFeatureContexts.CHAN_ANN_AS_IS & ctxs:
       +                features |= (1 << flag)
       +            elif LnFeatureContexts.CHAN_ANN_ALWAYS_EVEN & ctxs:
       +                if flag % 2 == 0:
       +                    features |= (1 << flag)
       +            elif LnFeatureContexts.CHAN_ANN_ALWAYS_ODD & ctxs:
       +                if flag % 2 == 0:
       +                    flag = get_ln_flag_pair_of_bit(flag)
       +                features |= (1 << flag)
       +        return features
       +
       +
       +del LNFC  # name is ambiguous without context
       +
       +# features that are actually implemented and understood in our codebase:
       +# (note: this is not what we send in e.g. init!)
       +# (note: specify both OPT and REQ here)
       +LN_FEATURES_IMPLEMENTED = (
       +        LnFeatures(0)
       +        | LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
       +        | LnFeatures.GOSSIP_QUERIES_OPT | LnFeatures.GOSSIP_QUERIES_REQ
       +        | LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
       +        | LnFeatures.VAR_ONION_OPT | LnFeatures.VAR_ONION_REQ
       +        | LnFeatures.PAYMENT_SECRET_OPT | LnFeatures.PAYMENT_SECRET_REQ
       +)
        
        
        def get_ln_flag_pair_of_bit(flag_bit: int) -> int:
       t@@ -735,23 +854,24 @@ def get_ln_flag_pair_of_bit(flag_bit: int) -> int:
                return flag_bit - 1
        
        
       -class LnGlobalFeatures(IntFlag):
       -    pass
        
       -# note that these are powers of two, not the bits themselves
       -LN_GLOBAL_FEATURES_KNOWN_SET = set(LnGlobalFeatures)
       +class IncompatibleOrInsaneFeatures(Exception): pass
       +class UnknownEvenFeatureBits(IncompatibleOrInsaneFeatures): pass
       +class IncompatibleLightningFeatures(IncompatibleOrInsaneFeatures): pass
        
       -class IncompatibleLightningFeatures(ValueError): pass
        
       -def ln_compare_features(our_features, their_features) -> int:
       -    """raises IncompatibleLightningFeatures if incompatible"""
       +def ln_compare_features(our_features: 'LnFeatures', their_features: int) -> 'LnFeatures':
       +    """Returns negotiated features.
       +    Raises IncompatibleLightningFeatures if incompatible.
       +    """
            our_flags = set(list_enabled_bits(our_features))
            their_flags = set(list_enabled_bits(their_features))
       +    # check that they have our required features, and disable the optional features they don't have
            for flag in our_flags:
                if flag not in their_flags and get_ln_flag_pair_of_bit(flag) not in their_flags:
                    # they don't have this feature we wanted :(
                    if flag % 2 == 0:  # even flags are compulsory
       -                raise IncompatibleLightningFeatures(f"remote does not support {LnLocalFeatures(1 << flag)!r}")
       +                raise IncompatibleLightningFeatures(f"remote does not support {LnFeatures(1 << flag)!r}")
                    our_features ^= 1 << flag  # disable flag
                else:
                    # They too have this flag.
       t@@ -759,9 +879,42 @@ def ln_compare_features(our_features, their_features) -> int:
                    # set the corresponding odd flag now.
                    if flag % 2 == 0 and our_features & (1 << flag):
                        our_features |= 1 << get_ln_flag_pair_of_bit(flag)
       +    # check that we have their required features
       +    for flag in their_flags:
       +        if flag not in our_flags and get_ln_flag_pair_of_bit(flag) not in our_flags:
       +            # we don't have this feature they wanted :(
       +            if flag % 2 == 0:  # even flags are compulsory
       +                raise IncompatibleLightningFeatures(f"remote wanted feature we don't have: {LnFeatures(1 << flag)!r}")
            return our_features
        
        
       +def validate_features(features: int) -> None:
       +    """Raises IncompatibleOrInsaneFeatures if
       +    - a mandatory feature is listed that we don't recognize, or
       +    - the features are inconsistent
       +    """
       +    features = LnFeatures(features)
       +    enabled_features = list_enabled_bits(features)
       +    for fbit in enabled_features:
       +        if (1 << fbit) & LN_FEATURES_IMPLEMENTED == 0 and fbit % 2 == 0:
       +            raise UnknownEvenFeatureBits(fbit)
       +    if not features.validate_transitive_dependecies():
       +        raise IncompatibleOrInsaneFeatures("not all transitive dependencies are set")
       +
       +
       +def derive_payment_secret_from_payment_preimage(payment_preimage: bytes) -> bytes:
       +    """Returns secret to be put into invoice.
       +    Derivation is deterministic, based on the preimage.
       +    Crucially the payment_hash must be derived in an independent way from this.
       +    """
       +    # Note that this could be random data too, but then we would need to store it.
       +    # We derive it identically to clightning, so that we cannot be distinguished:
       +    # https://github.com/ElementsProject/lightning/blob/faac4b28adee5221e83787d64cd5d30b16b62097/lightningd/invoice.c#L115
       +    modified = bytearray(payment_preimage)
       +    modified[0] ^= 1
       +    return sha256(bytes(modified))
       +
       +
        class LNPeerAddr:
        
            def __init__(self, host: str, port: int, pubkey: bytes):
       t@@ -955,3 +1108,11 @@ class UpdateAddHtlc:
        
            def to_tuple(self):
                return (self.amount_msat, self.payment_hash, self.cltv_expiry, self.htlc_id, self.timestamp)
       +
       +
       +class OnionFailureCodeMetaFlag(IntFlag):
       +    BADONION = 0x8000
       +    PERM     = 0x4000
       +    NODE     = 0x2000
       +    UPDATE   = 0x1000
       +
   DIR diff --git a/electrum/lnwire/README.md b/electrum/lnwire/README.md
       t@@ -0,0 +1,5 @@
       +These files are generated from the BOLT repository:
       +```
       +$ python3 tools/extract-formats.py 01-*.md 02-*.md 07-*.md  > peer_wire.csv
       +$ python3 tools/extract-formats.py 04-*.md  > onion_wire.csv
       +```
   DIR diff --git a/electrum/lnwire/onion_wire.csv b/electrum/lnwire/onion_wire.csv
       t@@ -0,0 +1,53 @@
       +tlvtype,tlv_payload,amt_to_forward,2
       +tlvdata,tlv_payload,amt_to_forward,amt_to_forward,tu64,
       +tlvtype,tlv_payload,outgoing_cltv_value,4
       +tlvdata,tlv_payload,outgoing_cltv_value,outgoing_cltv_value,tu32,
       +tlvtype,tlv_payload,short_channel_id,6
       +tlvdata,tlv_payload,short_channel_id,short_channel_id,short_channel_id,
       +tlvtype,tlv_payload,payment_data,8
       +tlvdata,tlv_payload,payment_data,payment_secret,byte,32
       +tlvdata,tlv_payload,payment_data,total_msat,tu64,
       +msgtype,invalid_realm,PERM|1
       +msgtype,temporary_node_failure,NODE|2
       +msgtype,permanent_node_failure,PERM|NODE|2
       +msgtype,required_node_feature_missing,PERM|NODE|3
       +msgtype,invalid_onion_version,BADONION|PERM|4
       +msgdata,invalid_onion_version,sha256_of_onion,sha256,
       +msgtype,invalid_onion_hmac,BADONION|PERM|5
       +msgdata,invalid_onion_hmac,sha256_of_onion,sha256,
       +msgtype,invalid_onion_key,BADONION|PERM|6
       +msgdata,invalid_onion_key,sha256_of_onion,sha256,
       +msgtype,temporary_channel_failure,UPDATE|7
       +msgdata,temporary_channel_failure,len,u16,
       +msgdata,temporary_channel_failure,channel_update,byte,len
       +msgtype,permanent_channel_failure,PERM|8
       +msgtype,required_channel_feature_missing,PERM|9
       +msgtype,unknown_next_peer,PERM|10
       +msgtype,amount_below_minimum,UPDATE|11
       +msgdata,amount_below_minimum,htlc_msat,u64,
       +msgdata,amount_below_minimum,len,u16,
       +msgdata,amount_below_minimum,channel_update,byte,len
       +msgtype,fee_insufficient,UPDATE|12
       +msgdata,fee_insufficient,htlc_msat,u64,
       +msgdata,fee_insufficient,len,u16,
       +msgdata,fee_insufficient,channel_update,byte,len
       +msgtype,incorrect_cltv_expiry,UPDATE|13
       +msgdata,incorrect_cltv_expiry,cltv_expiry,u32,
       +msgdata,incorrect_cltv_expiry,len,u16,
       +msgdata,incorrect_cltv_expiry,channel_update,byte,len
       +msgtype,expiry_too_soon,UPDATE|14
       +msgdata,expiry_too_soon,len,u16,
       +msgdata,expiry_too_soon,channel_update,byte,len
       +msgtype,incorrect_or_unknown_payment_details,PERM|15
       +msgdata,incorrect_or_unknown_payment_details,htlc_msat,u64,
       +msgdata,incorrect_or_unknown_payment_details,height,u32,
       +msgtype,final_incorrect_cltv_expiry,18
       +msgdata,final_incorrect_cltv_expiry,cltv_expiry,u32,
       +msgtype,final_incorrect_htlc_amount,19
       +msgdata,final_incorrect_htlc_amount,incoming_htlc_amt,u64,
       +msgtype,channel_disabled,UPDATE|20
       +msgtype,expiry_too_far,21
       +msgtype,invalid_onion_payload,PERM|22
       +msgdata,invalid_onion_payload,type,varint,
       +msgdata,invalid_onion_payload,offset,u16,
       +msgtype,mpp_timeout,23
   DIR diff --git a/electrum/lnwire/peer_wire.csv b/electrum/lnwire/peer_wire.csv
       t@@ -0,0 +1,210 @@
       +msgtype,init,16
       +msgdata,init,gflen,u16,
       +msgdata,init,globalfeatures,byte,gflen
       +msgdata,init,flen,u16,
       +msgdata,init,features,byte,flen
       +msgdata,init,tlvs,init_tlvs,
       +tlvtype,init_tlvs,networks,1
       +tlvdata,init_tlvs,networks,chains,chain_hash,...
       +msgtype,error,17
       +msgdata,error,channel_id,channel_id,
       +msgdata,error,len,u16,
       +msgdata,error,data,byte,len
       +msgtype,ping,18
       +msgdata,ping,num_pong_bytes,u16,
       +msgdata,ping,byteslen,u16,
       +msgdata,ping,ignored,byte,byteslen
       +msgtype,pong,19
       +msgdata,pong,byteslen,u16,
       +msgdata,pong,ignored,byte,byteslen
       +tlvtype,n1,tlv1,1
       +tlvdata,n1,tlv1,amount_msat,tu64,
       +tlvtype,n1,tlv2,2
       +tlvdata,n1,tlv2,scid,short_channel_id,
       +tlvtype,n1,tlv3,3
       +tlvdata,n1,tlv3,node_id,point,
       +tlvdata,n1,tlv3,amount_msat_1,u64,
       +tlvdata,n1,tlv3,amount_msat_2,u64,
       +tlvtype,n1,tlv4,254
       +tlvdata,n1,tlv4,cltv_delta,u16,
       +tlvtype,n2,tlv1,0
       +tlvdata,n2,tlv1,amount_msat,tu64,
       +tlvtype,n2,tlv2,11
       +tlvdata,n2,tlv2,cltv_expiry,tu32,
       +msgtype,open_channel,32
       +msgdata,open_channel,chain_hash,chain_hash,
       +msgdata,open_channel,temporary_channel_id,byte,32
       +msgdata,open_channel,funding_satoshis,u64,
       +msgdata,open_channel,push_msat,u64,
       +msgdata,open_channel,dust_limit_satoshis,u64,
       +msgdata,open_channel,max_htlc_value_in_flight_msat,u64,
       +msgdata,open_channel,channel_reserve_satoshis,u64,
       +msgdata,open_channel,htlc_minimum_msat,u64,
       +msgdata,open_channel,feerate_per_kw,u32,
       +msgdata,open_channel,to_self_delay,u16,
       +msgdata,open_channel,max_accepted_htlcs,u16,
       +msgdata,open_channel,funding_pubkey,point,
       +msgdata,open_channel,revocation_basepoint,point,
       +msgdata,open_channel,payment_basepoint,point,
       +msgdata,open_channel,delayed_payment_basepoint,point,
       +msgdata,open_channel,htlc_basepoint,point,
       +msgdata,open_channel,first_per_commitment_point,point,
       +msgdata,open_channel,channel_flags,byte,
       +msgdata,open_channel,shutdown_len,u16,,option_upfront_shutdown_script
       +msgdata,open_channel,shutdown_scriptpubkey,byte,shutdown_len,option_upfront_shutdown_script
       +msgtype,accept_channel,33
       +msgdata,accept_channel,temporary_channel_id,byte,32
       +msgdata,accept_channel,dust_limit_satoshis,u64,
       +msgdata,accept_channel,max_htlc_value_in_flight_msat,u64,
       +msgdata,accept_channel,channel_reserve_satoshis,u64,
       +msgdata,accept_channel,htlc_minimum_msat,u64,
       +msgdata,accept_channel,minimum_depth,u32,
       +msgdata,accept_channel,to_self_delay,u16,
       +msgdata,accept_channel,max_accepted_htlcs,u16,
       +msgdata,accept_channel,funding_pubkey,point,
       +msgdata,accept_channel,revocation_basepoint,point,
       +msgdata,accept_channel,payment_basepoint,point,
       +msgdata,accept_channel,delayed_payment_basepoint,point,
       +msgdata,accept_channel,htlc_basepoint,point,
       +msgdata,accept_channel,first_per_commitment_point,point,
       +msgdata,accept_channel,shutdown_len,u16,,option_upfront_shutdown_script
       +msgdata,accept_channel,shutdown_scriptpubkey,byte,shutdown_len,option_upfront_shutdown_script
       +msgtype,funding_created,34
       +msgdata,funding_created,temporary_channel_id,byte,32
       +msgdata,funding_created,funding_txid,sha256,
       +msgdata,funding_created,funding_output_index,u16,
       +msgdata,funding_created,signature,signature,
       +msgtype,funding_signed,35
       +msgdata,funding_signed,channel_id,channel_id,
       +msgdata,funding_signed,signature,signature,
       +msgtype,funding_locked,36
       +msgdata,funding_locked,channel_id,channel_id,
       +msgdata,funding_locked,next_per_commitment_point,point,
       +msgtype,shutdown,38
       +msgdata,shutdown,channel_id,channel_id,
       +msgdata,shutdown,len,u16,
       +msgdata,shutdown,scriptpubkey,byte,len
       +msgtype,closing_signed,39
       +msgdata,closing_signed,channel_id,channel_id,
       +msgdata,closing_signed,fee_satoshis,u64,
       +msgdata,closing_signed,signature,signature,
       +msgtype,update_add_htlc,128
       +msgdata,update_add_htlc,channel_id,channel_id,
       +msgdata,update_add_htlc,id,u64,
       +msgdata,update_add_htlc,amount_msat,u64,
       +msgdata,update_add_htlc,payment_hash,sha256,
       +msgdata,update_add_htlc,cltv_expiry,u32,
       +msgdata,update_add_htlc,onion_routing_packet,byte,1366
       +msgtype,update_fulfill_htlc,130
       +msgdata,update_fulfill_htlc,channel_id,channel_id,
       +msgdata,update_fulfill_htlc,id,u64,
       +msgdata,update_fulfill_htlc,payment_preimage,byte,32
       +msgtype,update_fail_htlc,131
       +msgdata,update_fail_htlc,channel_id,channel_id,
       +msgdata,update_fail_htlc,id,u64,
       +msgdata,update_fail_htlc,len,u16,
       +msgdata,update_fail_htlc,reason,byte,len
       +msgtype,update_fail_malformed_htlc,135
       +msgdata,update_fail_malformed_htlc,channel_id,channel_id,
       +msgdata,update_fail_malformed_htlc,id,u64,
       +msgdata,update_fail_malformed_htlc,sha256_of_onion,sha256,
       +msgdata,update_fail_malformed_htlc,failure_code,u16,
       +msgtype,commitment_signed,132
       +msgdata,commitment_signed,channel_id,channel_id,
       +msgdata,commitment_signed,signature,signature,
       +msgdata,commitment_signed,num_htlcs,u16,
       +msgdata,commitment_signed,htlc_signature,signature,num_htlcs
       +msgtype,revoke_and_ack,133
       +msgdata,revoke_and_ack,channel_id,channel_id,
       +msgdata,revoke_and_ack,per_commitment_secret,byte,32
       +msgdata,revoke_and_ack,next_per_commitment_point,point,
       +msgtype,update_fee,134
       +msgdata,update_fee,channel_id,channel_id,
       +msgdata,update_fee,feerate_per_kw,u32,
       +msgtype,channel_reestablish,136
       +msgdata,channel_reestablish,channel_id,channel_id,
       +msgdata,channel_reestablish,next_commitment_number,u64,
       +msgdata,channel_reestablish,next_revocation_number,u64,
       +msgdata,channel_reestablish,your_last_per_commitment_secret,byte,32,option_data_loss_protect,option_static_remotekey
       +msgdata,channel_reestablish,my_current_per_commitment_point,point,,option_data_loss_protect,option_static_remotekey
       +msgtype,announcement_signatures,259
       +msgdata,announcement_signatures,channel_id,channel_id,
       +msgdata,announcement_signatures,short_channel_id,short_channel_id,
       +msgdata,announcement_signatures,node_signature,signature,
       +msgdata,announcement_signatures,bitcoin_signature,signature,
       +msgtype,channel_announcement,256
       +msgdata,channel_announcement,node_signature_1,signature,
       +msgdata,channel_announcement,node_signature_2,signature,
       +msgdata,channel_announcement,bitcoin_signature_1,signature,
       +msgdata,channel_announcement,bitcoin_signature_2,signature,
       +msgdata,channel_announcement,len,u16,
       +msgdata,channel_announcement,features,byte,len
       +msgdata,channel_announcement,chain_hash,chain_hash,
       +msgdata,channel_announcement,short_channel_id,short_channel_id,
       +msgdata,channel_announcement,node_id_1,point,
       +msgdata,channel_announcement,node_id_2,point,
       +msgdata,channel_announcement,bitcoin_key_1,point,
       +msgdata,channel_announcement,bitcoin_key_2,point,
       +msgtype,node_announcement,257
       +msgdata,node_announcement,signature,signature,
       +msgdata,node_announcement,flen,u16,
       +msgdata,node_announcement,features,byte,flen
       +msgdata,node_announcement,timestamp,u32,
       +msgdata,node_announcement,node_id,point,
       +msgdata,node_announcement,rgb_color,byte,3
       +msgdata,node_announcement,alias,byte,32
       +msgdata,node_announcement,addrlen,u16,
       +msgdata,node_announcement,addresses,byte,addrlen
       +msgtype,channel_update,258
       +msgdata,channel_update,signature,signature,
       +msgdata,channel_update,chain_hash,chain_hash,
       +msgdata,channel_update,short_channel_id,short_channel_id,
       +msgdata,channel_update,timestamp,u32,
       +msgdata,channel_update,message_flags,byte,
       +msgdata,channel_update,channel_flags,byte,
       +msgdata,channel_update,cltv_expiry_delta,u16,
       +msgdata,channel_update,htlc_minimum_msat,u64,
       +msgdata,channel_update,fee_base_msat,u32,
       +msgdata,channel_update,fee_proportional_millionths,u32,
       +msgdata,channel_update,htlc_maximum_msat,u64,,option_channel_htlc_max
       +msgtype,query_short_channel_ids,261,gossip_queries
       +msgdata,query_short_channel_ids,chain_hash,chain_hash,
       +msgdata,query_short_channel_ids,len,u16,
       +msgdata,query_short_channel_ids,encoded_short_ids,byte,len
       +msgdata,query_short_channel_ids,tlvs,query_short_channel_ids_tlvs,
       +tlvtype,query_short_channel_ids_tlvs,query_flags,1
       +tlvdata,query_short_channel_ids_tlvs,query_flags,encoding_type,u8,
       +tlvdata,query_short_channel_ids_tlvs,query_flags,encoded_query_flags,byte,...
       +msgtype,reply_short_channel_ids_end,262,gossip_queries
       +msgdata,reply_short_channel_ids_end,chain_hash,chain_hash,
       +msgdata,reply_short_channel_ids_end,complete,byte,
       +msgtype,query_channel_range,263,gossip_queries
       +msgdata,query_channel_range,chain_hash,chain_hash,
       +msgdata,query_channel_range,first_blocknum,u32,
       +msgdata,query_channel_range,number_of_blocks,u32,
       +msgdata,query_channel_range,tlvs,query_channel_range_tlvs,
       +tlvtype,query_channel_range_tlvs,query_option,1
       +tlvdata,query_channel_range_tlvs,query_option,query_option_flags,varint,
       +msgtype,reply_channel_range,264,gossip_queries
       +msgdata,reply_channel_range,chain_hash,chain_hash,
       +msgdata,reply_channel_range,first_blocknum,u32,
       +msgdata,reply_channel_range,number_of_blocks,u32,
       +msgdata,reply_channel_range,complete,byte,
       +msgdata,reply_channel_range,len,u16,
       +msgdata,reply_channel_range,encoded_short_ids,byte,len
       +msgdata,reply_channel_range,tlvs,reply_channel_range_tlvs,
       +tlvtype,reply_channel_range_tlvs,timestamps_tlv,1
       +tlvdata,reply_channel_range_tlvs,timestamps_tlv,encoding_type,u8,
       +tlvdata,reply_channel_range_tlvs,timestamps_tlv,encoded_timestamps,byte,...
       +tlvtype,reply_channel_range_tlvs,checksums_tlv,3
       +tlvdata,reply_channel_range_tlvs,checksums_tlv,checksums,channel_update_checksums,...
       +subtype,channel_update_timestamps
       +subtypedata,channel_update_timestamps,timestamp_node_id_1,u32,
       +subtypedata,channel_update_timestamps,timestamp_node_id_2,u32,
       +subtype,channel_update_checksums
       +subtypedata,channel_update_checksums,checksum_node_id_1,u32,
       +subtypedata,channel_update_checksums,checksum_node_id_2,u32,
       +msgtype,gossip_timestamp_filter,265,gossip_queries
       +msgdata,gossip_timestamp_filter,chain_hash,chain_hash,
       +msgdata,gossip_timestamp_filter,first_timestamp,u32,
       +msgdata,gossip_timestamp_filter,timestamp_range,u32,
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -52,10 +52,10 @@ from .lnutil import (Outpoint, LNPeerAddr,
                             generate_keypair, LnKeyFamily, LOCAL, REMOTE,
                             UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE,
                             NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
       -                     UpdateAddHtlc, Direction, LnLocalFeatures,
       +                     UpdateAddHtlc, Direction, LnFeatures,
                             ShortChannelID, PaymentAttemptLog, PaymentAttemptFailureDetails,
       -                     BarePaymentAttemptLog)
       -from .lnutil import ln_dummy_address, ln_compare_features
       +                     BarePaymentAttemptLog, derive_payment_secret_from_payment_preimage)
       +from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures
        from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
        from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket
        from .lnmsg import decode_msg
       t@@ -147,9 +147,11 @@ class LNWorker(Logger):
                self.taskgroup = SilentTaskGroup()
                # set some feature flags as baseline for both LNWallet and LNGossip
                # note that e.g. DATA_LOSS_PROTECT is needed for LNGossip as many peers require it
       -        self.localfeatures = LnLocalFeatures(0)
       -        self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       -        self.localfeatures |= LnLocalFeatures.OPTION_STATIC_REMOTEKEY_OPT
       +        self.features = LnFeatures(0)
       +        self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT
       +        self.features |= LnFeatures.VAR_ONION_OPT
       +        self.features |= LnFeatures.PAYMENT_SECRET_OPT
        
            def channels_for_peer(self, node_id):
                return {}
       t@@ -248,8 +250,8 @@ class LNWorker(Logger):
                if not node:
                    return False
                try:
       -            ln_compare_features(self.localfeatures, node.features)
       -        except ValueError:
       +            ln_compare_features(self.features, node.features)
       +        except IncompatibleLightningFeatures:
                    return False
                #self.logger.info(f'is_good {peer.host}')
                return True
       t@@ -366,8 +368,8 @@ class LNGossip(LNWorker):
                node = BIP32Node.from_rootseed(seed, xtype='standard')
                xprv = node.to_xprv()
                super().__init__(xprv)
       -        self.localfeatures |= LnLocalFeatures.GOSSIP_QUERIES_OPT
       -        self.localfeatures |= LnLocalFeatures.GOSSIP_QUERIES_REQ
       +        self.features |= LnFeatures.GOSSIP_QUERIES_OPT
       +        self.features |= LnFeatures.GOSSIP_QUERIES_REQ
                self.unknown_ids = set()
        
            def start_network(self, network: 'Network'):
       t@@ -419,8 +421,8 @@ class LNWallet(LNWorker):
                self.db = wallet.db
                self.config = wallet.config
                LNWorker.__init__(self, xprv)
       -        self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ
       -        self.localfeatures |= LnLocalFeatures.OPTION_STATIC_REMOTEKEY_REQ
       +        self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
       +        self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
                self.payments = self.db.get_dict('lightning_payments')     # RHASH -> amount, direction, is_paid
                self.preimages = self.db.get_dict('lightning_preimages')   # RHASH -> preimage
                self.sweep_address = wallet.get_receiving_address()
       t@@ -952,7 +954,12 @@ class LNWallet(LNWorker):
                if not peer:
                    raise Exception('Dropped peer')
                await peer.initialized
       -        htlc = peer.pay(route, chan, int(lnaddr.amount * COIN * 1000), lnaddr.paymenthash, lnaddr.get_min_final_cltv_expiry())
       +        htlc = peer.pay(route=route,
       +                        chan=chan,
       +                        amount_msat=int(lnaddr.amount * COIN * 1000),
       +                        payment_hash=lnaddr.paymenthash,
       +                        min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
       +                        payment_secret=lnaddr.payment_secret)
                self.network.trigger_callback('htlc_added', htlc, lnaddr, SENT)
                payment_attempt = await self.await_payment(lnaddr.paymenthash)
                if payment_attempt.success:
       t@@ -1047,7 +1054,7 @@ class LNWallet(LNWorker):
                return addr
        
            @profiler
       -    def _create_route_from_invoice(self, decoded_invoice) -> LNPaymentRoute:
       +    def _create_route_from_invoice(self, decoded_invoice: 'LnAddr') -> LNPaymentRoute:
                amount_msat = int(decoded_invoice.amount * COIN * 1000)
                invoice_pubkey = decoded_invoice.pubkey.serialize()
                # use 'r' field from invoice
       t@@ -1091,8 +1098,13 @@ class LNWallet(LNWorker):
                            fee_base_msat = channel_policy.fee_base_msat
                            fee_proportional_millionths = channel_policy.fee_proportional_millionths
                            cltv_expiry_delta = channel_policy.cltv_expiry_delta
       -                route.append(RouteEdge(node_pubkey, short_channel_id, fee_base_msat, fee_proportional_millionths,
       -                                       cltv_expiry_delta))
       +                node_info = self.channel_db.get_node_info_for_node_id(node_id=node_pubkey)
       +                route.append(RouteEdge(node_id=node_pubkey,
       +                                       short_channel_id=short_channel_id,
       +                                       fee_base_msat=fee_base_msat,
       +                                       fee_proportional_millionths=fee_proportional_millionths,
       +                                       cltv_expiry_delta=cltv_expiry_delta,
       +                                       node_features=node_info.features if node_info else 0))
                        prev_node_id = node_pubkey
                    # test sanity
                    if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
       t@@ -1111,6 +1123,11 @@ class LNWallet(LNWorker):
                    if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
                        self.logger.info(f"rejecting insane route {route}")
                        raise NoPathFound()
       +        assert len(route) > 0
       +        assert route[-1].node_id == invoice_pubkey
       +        # add features from invoice
       +        invoice_features = decoded_invoice.get_tag('9') or 0
       +        route[-1].node_features |= invoice_features
                return route
        
            def add_request(self, amount_sat, message, expiry):
       t@@ -1130,6 +1147,7 @@ class LNWallet(LNWorker):
                                     "Other clients will likely not be able to send to us.")
                payment_preimage = os.urandom(32)
                payment_hash = sha256(payment_preimage)
       +
                info = PaymentInfo(payment_hash, amount_sat, RECEIVED, PR_UNPAID)
                amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
                if expiry == 0:
       t@@ -1138,12 +1156,15 @@ class LNWallet(LNWorker):
                    # Our higher level invoices code however uses 0 for "never".
                    # Hence set some high expiration here
                    expiry = 100 * 365 * 24 * 60 * 60  # 100 years
       -        lnaddr = LnAddr(payment_hash, amount_btc,
       +        lnaddr = LnAddr(paymenthash=payment_hash,
       +                        amount=amount_btc,
                                tags=[('d', message),
                                      ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
       -                              ('x', expiry)]
       +                              ('x', expiry),
       +                              ('9', self.features.for_invoice())]
                                + routing_hints,
       -                        date = timestamp)
       +                        date=timestamp,
       +                        payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage))
                invoice = lnencode(lnaddr, self.node_keypair.privkey)
                key = bh2u(lnaddr.paymenthash)
                req = {
   DIR diff --git a/electrum/tests/test_bolt11.py b/electrum/tests/test_bolt11.py
       t@@ -6,6 +6,7 @@ import unittest
        
        from electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, lndecode, u5_to_bitarray, bitarray_to_u5
        from electrum.segwit_addr import bech32_encode, bech32_decode
       +from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage
        
        from . import ElectrumTestCase
        
       t@@ -61,16 +62,28 @@ class TestBolt11(ElectrumTestCase):
        
        
                tests = [
       -            LnAddr(RHASH, tags=[('d', '')]),
       -            LnAddr(RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]),
       -            LnAddr(RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),
       -            LnAddr(RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),
       -            LnAddr(RHASH, amount=24, tags=[
       -                ('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3), (unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]), ('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'), ('h', longdescription)]),
       -            LnAddr(RHASH, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription)]),
       -            LnAddr(RHASH, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription)]),
       -            LnAddr(RHASH, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription)]),
       -            LnAddr(RHASH, amount=24, tags=[('n', PUBKEY), ('h', longdescription)]),
       +            LnAddr(paymenthash=RHASH, tags=[('d', '')]),
       +            LnAddr(paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]),
       +            LnAddr(paymenthash=RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),
       +            LnAddr(paymenthash=RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[
       +                ('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3),
       +                       (unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]),
       +                ('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'),
       +                ('h', longdescription)]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription)]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription)]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription)]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('n', PUBKEY), ('h', longdescription)]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 514)]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 8))]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9))]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 7) + (1 << 11))]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 12))]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 13))]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 14))]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 15))]),
       +            LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 33282)], payment_secret=b"\x11" * 32),
                ]
        
                # Roundtrip
       t@@ -81,14 +94,14 @@ class TestBolt11(ElectrumTestCase):
            def test_n_decoding(self):
                # We flip the signature recovery bit, which would normally give a different
                # pubkey.
       -        hrp, data = bech32_decode(lnencode(LnAddr(RHASH, amount=24, tags=[('d', '')]), PRIVKEY), True)
       +        hrp, data = bech32_decode(lnencode(LnAddr(paymenthash=RHASH, amount=24, tags=[('d', '')]), PRIVKEY), True)
                databits = u5_to_bitarray(data)
                databits.invert(-1)
                lnaddr = lndecode(bech32_encode(hrp, bitarray_to_u5(databits)), verbose=True)
                assert lnaddr.pubkey.serialize() != PUBKEY
        
                # But not if we supply expliciy `n` specifier!
       -        hrp, data = bech32_decode(lnencode(LnAddr(RHASH, amount=24,
       +        hrp, data = bech32_decode(lnencode(LnAddr(paymenthash=RHASH, amount=24,
                                                          tags=[('d', ''),
                                                                ('n', PUBKEY)]),
                                                   PRIVKEY), True)
       t@@ -98,9 +111,28 @@ class TestBolt11(ElectrumTestCase):
                assert lnaddr.pubkey.serialize() == PUBKEY
        
            def test_min_final_cltv_expiry_decoding(self):
       -        self.assertEqual(144, lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe", expected_hrp="sb").get_min_final_cltv_expiry())
       +        lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe",
       +                          expected_hrp="sb")
       +        self.assertEqual(144, lnaddr.get_min_final_cltv_expiry())
        
            def test_min_final_cltv_expiry_roundtrip(self):
       -        lnaddr = LnAddr(RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', 150)])
       +        lnaddr = LnAddr(paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', 150)])
                invoice = lnencode(lnaddr, PRIVKEY)
                self.assertEqual(150, lndecode(invoice).get_min_final_cltv_expiry())
       +
       +    def test_features(self):
       +        lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl")
       +        self.assertEqual(514, lnaddr.get_tag('9'))
       +
       +        with self.assertRaises(UnknownEvenFeatureBits):
       +            lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7")
       +
       +    def test_payment_secret(self):
       +        lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9q5sqqqqqqqqqqqqqqqpqsqvvh7ut50r00p3pg34ea68k7zfw64f8yx9jcdk35lh5ft8qdr8g4r0xzsdcrmcy9hex8un8d8yraewvhqc9l0sh8l0e0yvmtxde2z0hgpzsje5l")
       +        self.assertEqual((1 << 9) + (1 << 15) + (1 << 99), lnaddr.get_tag('9'))
       +        self.assertEqual(b"\x11" * 32, lnaddr.payment_secret)
       +
       +    def test_derive_payment_secret_from_payment_preimage(self):
       +        preimage = bytes.fromhex("cc3fc000bdeff545acee53ada12ff96060834be263f77d645abbebc3a8d53b92")
       +        self.assertEqual("bfd660b559b3f452c6bb05b8d2906f520c151c107b733863ed0cc53fc77021a8",
       +                         derive_payment_secret_from_payment_preimage(preimage).hex())
   DIR diff --git a/electrum/tests/test_lnmsg.py b/electrum/tests/test_lnmsg.py
       t@@ -0,0 +1,385 @@
       +import io
       +
       +from electrum.lnmsg import (read_bigsize_int, write_bigsize_int, FieldEncodingNotMinimal,
       +                            UnexpectedEndOfStream, LNSerializer, UnknownMandatoryTLVRecordType,
       +                            MalformedMsg, MsgTrailingGarbage, MsgInvalidFieldOrder, encode_msg,
       +                            decode_msg, UnexpectedFieldSizeForEncoder)
       +from electrum.util import bfh
       +from electrum.lnutil import ShortChannelID, LnFeatures
       +from electrum import constants
       +
       +from . import TestCaseForTestnet
       +
       +
       +class TestLNMsg(TestCaseForTestnet):
       +
       +    def test_write_bigsize_int(self):
       +        self.assertEqual(bfh("00"), write_bigsize_int(0))
       +        self.assertEqual(bfh("fc"), write_bigsize_int(252))
       +        self.assertEqual(bfh("fd00fd"), write_bigsize_int(253))
       +        self.assertEqual(bfh("fdffff"), write_bigsize_int(65535))
       +        self.assertEqual(bfh("fe00010000"), write_bigsize_int(65536))
       +        self.assertEqual(bfh("feffffffff"), write_bigsize_int(4294967295))
       +        self.assertEqual(bfh("ff0000000100000000"), write_bigsize_int(4294967296))
       +        self.assertEqual(bfh("ffffffffffffffffff"), write_bigsize_int(18446744073709551615))
       +
       +    def test_read_bigsize_int(self):
       +        self.assertEqual(0, read_bigsize_int(io.BytesIO(bfh("00"))))
       +        self.assertEqual(252, read_bigsize_int(io.BytesIO(bfh("fc"))))
       +        self.assertEqual(253, read_bigsize_int(io.BytesIO(bfh("fd00fd"))))
       +        self.assertEqual(65535, read_bigsize_int(io.BytesIO(bfh("fdffff"))))
       +        self.assertEqual(65536, read_bigsize_int(io.BytesIO(bfh("fe00010000"))))
       +        self.assertEqual(4294967295, read_bigsize_int(io.BytesIO(bfh("feffffffff"))))
       +        self.assertEqual(4294967296, read_bigsize_int(io.BytesIO(bfh("ff0000000100000000"))))
       +        self.assertEqual(18446744073709551615, read_bigsize_int(io.BytesIO(bfh("ffffffffffffffffff"))))
       +
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            read_bigsize_int(io.BytesIO(bfh("fd00fc")))
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            read_bigsize_int(io.BytesIO(bfh("fe0000ffff")))
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            read_bigsize_int(io.BytesIO(bfh("ff00000000ffffffff")))
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            read_bigsize_int(io.BytesIO(bfh("fd00")))
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            read_bigsize_int(io.BytesIO(bfh("feffff")))
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            read_bigsize_int(io.BytesIO(bfh("ffffffffff")))
       +        self.assertEqual(None, read_bigsize_int(io.BytesIO(bfh(""))))
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            read_bigsize_int(io.BytesIO(bfh("fd")))
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            read_bigsize_int(io.BytesIO(bfh("fe")))
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            read_bigsize_int(io.BytesIO(bfh("ff")))
       +
       +    def test_read_tlv_stream_tests1(self):
       +        # from https://github.com/lightningnetwork/lightning-rfc/blob/452a0eb916fedf4c954137b4fd0b61b5002b34ad/01-messaging.md#tlv-decoding-failures
       +        lnser = LNSerializer()
       +        for tlv_stream_name in ("n1", "n2"):
       +            with self.subTest(tlv_stream_name=tlv_stream_name):
       +                with self.assertRaises(UnexpectedEndOfStream):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(UnexpectedEndOfStream):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd01")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(FieldEncodingNotMinimal):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd000100")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(UnexpectedEndOfStream):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd0101")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(UnexpectedEndOfStream):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("0ffd")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(UnexpectedEndOfStream):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("0ffd26")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(UnexpectedEndOfStream):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("0ffd2602")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(FieldEncodingNotMinimal):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("0ffd000100")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(UnexpectedEndOfStream):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("0ffd0201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")), tlv_stream_name="n1")
       +                with self.assertRaises(UnknownMandatoryTLVRecordType):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("1200")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(UnknownMandatoryTLVRecordType):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd010200")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(UnknownMandatoryTLVRecordType):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("fe0100000200")), tlv_stream_name=tlv_stream_name)
       +                with self.assertRaises(UnknownMandatoryTLVRecordType):
       +                    lnser.read_tlv_stream(fd=io.BytesIO(bfh("ff010000000000000200")), tlv_stream_name=tlv_stream_name)
       +        with self.assertRaises(MsgTrailingGarbage):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0109ffffffffffffffffff")), tlv_stream_name="n1")
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("010100")), tlv_stream_name="n1")
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("01020001")), tlv_stream_name="n1")
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0103000100")), tlv_stream_name="n1")
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("010400010000")), tlv_stream_name="n1")
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("01050001000000")), tlv_stream_name="n1")
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0106000100000000")), tlv_stream_name="n1")
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("010700010000000000")), tlv_stream_name="n1")
       +        with self.assertRaises(FieldEncodingNotMinimal):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("01080001000000000000")), tlv_stream_name="n1")
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("020701010101010101")), tlv_stream_name="n1")
       +        with self.assertRaises(MsgTrailingGarbage):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0209010101010101010101")), tlv_stream_name="n1")
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0321023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb")), tlv_stream_name="n1")
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0329023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb0000000000000001")), tlv_stream_name="n1")
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0330023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb000000000000000100000000000001")), tlv_stream_name="n1")
       +        # check if ECC point is valid?... skip for now.
       +        #with self.assertRaises(Exception):
       +        #    lnser.read_tlv_stream(fd=io.BytesIO(bfh("0331043da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb00000000000000010000000000000002")), tlv_stream_name="n1")
       +        with self.assertRaises(MsgTrailingGarbage):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0332023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb0000000000000001000000000000000001")), tlv_stream_name="n1")
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd00fe00")), tlv_stream_name="n1")
       +        with self.assertRaises(UnexpectedEndOfStream):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd00fe0101")), tlv_stream_name="n1")
       +        with self.assertRaises(MsgTrailingGarbage):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd00fe03010101")), tlv_stream_name="n1")
       +        with self.assertRaises(UnknownMandatoryTLVRecordType):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0000")), tlv_stream_name="n1")
       +
       +    def test_read_tlv_stream_tests2(self):
       +        # from https://github.com/lightningnetwork/lightning-rfc/blob/452a0eb916fedf4c954137b4fd0b61b5002b34ad/01-messaging.md#tlv-decoding-successes
       +        lnser = LNSerializer()
       +        for tlv_stream_name in ("n1", "n2"):
       +            with self.subTest(tlv_stream_name=tlv_stream_name):
       +                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh("")), tlv_stream_name=tlv_stream_name))
       +                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh("2100")), tlv_stream_name=tlv_stream_name))
       +                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd020100")), tlv_stream_name=tlv_stream_name))
       +                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd00fd00")), tlv_stream_name=tlv_stream_name))
       +                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd00ff00")), tlv_stream_name=tlv_stream_name))
       +                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh("fe0200000100")), tlv_stream_name=tlv_stream_name))
       +                self.assertEqual({}, lnser.read_tlv_stream(fd=io.BytesIO(bfh("ff020000000000000100")), tlv_stream_name=tlv_stream_name))
       +
       +        self.assertEqual({"tlv1": {"amount_msat": 0}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("0100")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv1": {"amount_msat": 1}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("010101")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv1": {"amount_msat": 256}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("01020100")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv1": {"amount_msat": 65536}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("0103010000")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv1": {"amount_msat": 16777216}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("010401000000")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv1": {"amount_msat": 4294967296}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("01050100000000")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv1": {"amount_msat": 1099511627776}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("0106010000000000")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv1": {"amount_msat": 281474976710656}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("010701000000000000")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv1": {"amount_msat": 72057594037927936}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("01080100000000000000")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv2": {"scid": ShortChannelID.from_components(0, 0, 550)}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("02080000000000000226")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv3": {"node_id": bfh("023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb"),
       +                                   "amount_msat_1": 1,
       +                                   "amount_msat_2": 2}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("0331023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb00000000000000010000000000000002")), tlv_stream_name="n1"))
       +        self.assertEqual({"tlv4": {"cltv_delta": 550}},
       +                         lnser.read_tlv_stream(fd=io.BytesIO(bfh("fd00fe020226")), tlv_stream_name="n1"))
       +
       +    def test_read_tlv_stream_tests3(self):
       +        # from https://github.com/lightningnetwork/lightning-rfc/blob/452a0eb916fedf4c954137b4fd0b61b5002b34ad/01-messaging.md#tlv-stream-decoding-failure
       +        lnser = LNSerializer()
       +        with self.assertRaises(MsgInvalidFieldOrder):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0208000000000000022601012a")), tlv_stream_name="n1")
       +        with self.assertRaises(MsgInvalidFieldOrder):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("0208000000000000023102080000000000000451")), tlv_stream_name="n1")
       +        with self.assertRaises(MsgInvalidFieldOrder):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("1f000f012a")), tlv_stream_name="n1")
       +        with self.assertRaises(MsgInvalidFieldOrder):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("1f001f012a")), tlv_stream_name="n1")
       +        with self.assertRaises(MsgInvalidFieldOrder):
       +            lnser.read_tlv_stream(fd=io.BytesIO(bfh("ffffffffffffffffff000000")), tlv_stream_name="n2")
       +
       +    def test_encode_decode_msg__missing_mandatory_field_gets_set_to_zeroes(self):
       +        # "channel_update": "signature" missing -> gets set to zeroes
       +        self.assertEqual(bfh("01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023000000003b9aca00"),
       +                         encode_msg(
       +                             "channel_update",
       +                             short_channel_id=ShortChannelID.from_components(54321, 111, 2),
       +                             channel_flags=b'\x00',
       +                             message_flags=b'\x01',
       +                             cltv_expiry_delta=144,
       +                             htlc_minimum_msat=200,
       +                             htlc_maximum_msat=1_000_000_000,
       +                             fee_base_msat=500,
       +                             fee_proportional_millionths=35,
       +                             chain_hash=constants.net.rev_genesis_bytes(),
       +                             timestamp=1584320643,
       +                         ))
       +        self.assertEqual(('channel_update',
       +                         {'chain_hash': b'CI\x7f\xd7\xf8&\x95q\x08\xf4\xa3\x0f\xd9\xce\xc3\xae\xbay\x97 \x84\xe9\x0e\xad\x01\xea3\t\x00\x00\x00\x00',
       +                          'channel_flags': b'\x00',
       +                          'cltv_expiry_delta': 144,
       +                          'fee_base_msat': 500,
       +                          'fee_proportional_millionths': 35,
       +                          'htlc_maximum_msat': 1000000000,
       +                          'htlc_minimum_msat': 200,
       +                          'message_flags': b'\x01',
       +                          'short_channel_id': b'\x00\xd41\x00\x00o\x00\x02',
       +                          'signature': bytes(64),
       +                          'timestamp': 1584320643}
       +                          ),
       +                         decode_msg(bfh("01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023000000003b9aca00")))
       +
       +    def test_encode_decode_msg__missing_optional_field_will_not_appear_in_decoded_dict(self):
       +        # "channel_update": optional field "htlc_maximum_msat" missing -> does not get put into dict
       +        self.assertEqual(bfh("01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023"),
       +                         encode_msg(
       +                             "channel_update",
       +                             short_channel_id=ShortChannelID.from_components(54321, 111, 2),
       +                             channel_flags=b'\x00',
       +                             message_flags=b'\x01',
       +                             cltv_expiry_delta=144,
       +                             htlc_minimum_msat=200,
       +                             fee_base_msat=500,
       +                             fee_proportional_millionths=35,
       +                             chain_hash=constants.net.rev_genesis_bytes(),
       +                             timestamp=1584320643,
       +                         ))
       +        self.assertEqual(('channel_update',
       +                         {'chain_hash': b'CI\x7f\xd7\xf8&\x95q\x08\xf4\xa3\x0f\xd9\xce\xc3\xae\xbay\x97 \x84\xe9\x0e\xad\x01\xea3\t\x00\x00\x00\x00',
       +                          'channel_flags': b'\x00',
       +                          'cltv_expiry_delta': 144,
       +                          'fee_base_msat': 500,
       +                          'fee_proportional_millionths': 35,
       +                          'htlc_minimum_msat': 200,
       +                          'message_flags': b'\x01',
       +                          'short_channel_id': b'\x00\xd41\x00\x00o\x00\x02',
       +                          'signature': bytes(64),
       +                          'timestamp': 1584320643}
       +                          ),
       +                         decode_msg(bfh("01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023")))
       +
       +    def test_encode_decode_msg__ints_can_be_passed_as_bytes(self):
       +        self.assertEqual(bfh("01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023000000003b9aca00"),
       +                         encode_msg(
       +                             "channel_update",
       +                             short_channel_id=ShortChannelID.from_components(54321, 111, 2),
       +                             channel_flags=b'\x00',
       +                             message_flags=b'\x01',
       +                             cltv_expiry_delta=int.to_bytes(144, length=2, byteorder="big", signed=False),
       +                             htlc_minimum_msat=int.to_bytes(200, length=8, byteorder="big", signed=False),
       +                             htlc_maximum_msat=int.to_bytes(1_000_000_000, length=8, byteorder="big", signed=False),
       +                             fee_base_msat=int.to_bytes(500, length=4, byteorder="big", signed=False),
       +                             fee_proportional_millionths=int.to_bytes(35, length=4, byteorder="big", signed=False),
       +                             chain_hash=constants.net.rev_genesis_bytes(),
       +                             timestamp=int.to_bytes(1584320643, length=4, byteorder="big", signed=False),
       +                         ))
       +        self.assertEqual(('channel_update',
       +                         {'chain_hash': b'CI\x7f\xd7\xf8&\x95q\x08\xf4\xa3\x0f\xd9\xce\xc3\xae\xbay\x97 \x84\xe9\x0e\xad\x01\xea3\t\x00\x00\x00\x00',
       +                          'channel_flags': b'\x00',
       +                          'cltv_expiry_delta': 144,
       +                          'fee_base_msat': 500,
       +                          'fee_proportional_millionths': 35,
       +                          'htlc_maximum_msat': 1000000000,
       +                          'htlc_minimum_msat': 200,
       +                          'message_flags': b'\x01',
       +                          'short_channel_id': b'\x00\xd41\x00\x00o\x00\x02',
       +                          'signature': bytes(64),
       +                          'timestamp': 1584320643}
       +                          ),
       +                         decode_msg(bfh("01020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea33090000000000d43100006f00025e6ed0830100009000000000000000c8000001f400000023000000003b9aca00")))
       +        # "htlc_minimum_msat" is passed as bytes but with incorrect length
       +        with self.assertRaises(UnexpectedFieldSizeForEncoder):
       +            encode_msg(
       +                "channel_update",
       +                short_channel_id=ShortChannelID.from_components(54321, 111, 2),
       +                channel_flags=b'\x00',
       +                message_flags=b'\x01',
       +                cltv_expiry_delta=int.to_bytes(144, length=2, byteorder="big", signed=False),
       +                htlc_minimum_msat=int.to_bytes(200, length=4, byteorder="big", signed=False),
       +                htlc_maximum_msat=int.to_bytes(1_000_000_000, length=8, byteorder="big", signed=False),
       +                fee_base_msat=int.to_bytes(500, length=4, byteorder="big", signed=False),
       +                fee_proportional_millionths=int.to_bytes(35, length=4, byteorder="big", signed=False),
       +                chain_hash=constants.net.rev_genesis_bytes(),
       +                timestamp=int.to_bytes(1584320643, length=4, byteorder="big", signed=False),
       +            )
       +
       +    def test_encode_decode_msg__commitment_signed(self):
       +        # "commitment_signed" is interesting because of the "htlc_signature" field,
       +        #  which is a concatenation of multiple ("num_htlcs") signatures.
       +        # 5 htlcs
       +        self.assertEqual(bfh("0084010101010101010101010101010101010101010101010101010101010101010106112951d0a6d7fc1dbca3bd1cdbda9acfee7f668b3c0a36bd944f7e2f305b274ba46a61279e15163b2d376c664bb3481d7c5e107a5b268301e39aebbda27d2d00056548bd093a2bd2f4f053f0c6eb2c5f541d55eb8a2ede4d35fe974e5d3cd0eec3138bfd4115f4483c3b14e7988b48811d2da75f29f5e6eee691251fb4fba5a2610ba8fe7007117fe1c9fa1a6b01805c84cfffbb0eba674b64342c7cac567dea50728c1bb1aadc6d23fc2f4145027eafca82d6072cc9ce6529542099f728a0521e4b2044df5d02f7f2cdf84404762b1979528aa689a3e060a2a90ba8ef9a83d24d31ffb0d95c71d9fb9049b24ecf2c949c1486e7eb3ae160d70d54e441dc785dc57f7f3c9901b9537398c66f546cfc1d65e0748895d14699342c407fe119ac17db079b103720124a5ba22d4ba14c12832324dea9cb60c61ee74376ee7dcffdd1836e354aa8838ce3b37854fa91465cc40c73b702915e3580bfebaace805d52373b57ac755ebe4a8fe97e5fc21669bea124b809c79968479148f7174f39b8014542"),
       +                         encode_msg(
       +                             "commitment_signed",
       +                             channel_id=b'\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01',
       +                             signature=b"\x06\x11)Q\xd0\xa6\xd7\xfc\x1d\xbc\xa3\xbd\x1c\xdb\xda\x9a\xcf\xee\x7ff\x8b<\n6\xbd\x94O~/0['K\xa4ja'\x9e\x15\x16;-7lfK\xb3H\x1d|^\x10z[&\x83\x01\xe3\x9a\xeb\xbd\xa2}-",
       +                             num_htlcs=5,
       +                             htlc_signature=bfh("6548bd093a2bd2f4f053f0c6eb2c5f541d55eb8a2ede4d35fe974e5d3cd0eec3138bfd4115f4483c3b14e7988b48811d2da75f29f5e6eee691251fb4fba5a2610ba8fe7007117fe1c9fa1a6b01805c84cfffbb0eba674b64342c7cac567dea50728c1bb1aadc6d23fc2f4145027eafca82d6072cc9ce6529542099f728a0521e4b2044df5d02f7f2cdf84404762b1979528aa689a3e060a2a90ba8ef9a83d24d31ffb0d95c71d9fb9049b24ecf2c949c1486e7eb3ae160d70d54e441dc785dc57f7f3c9901b9537398c66f546cfc1d65e0748895d14699342c407fe119ac17db079b103720124a5ba22d4ba14c12832324dea9cb60c61ee74376ee7dcffdd1836e354aa8838ce3b37854fa91465cc40c73b702915e3580bfebaace805d52373b57ac755ebe4a8fe97e5fc21669bea124b809c79968479148f7174f39b8014542"),
       +                         ))
       +        self.assertEqual(('commitment_signed',
       +                         {'channel_id': b'\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01',
       +                          'signature': b"\x06\x11)Q\xd0\xa6\xd7\xfc\x1d\xbc\xa3\xbd\x1c\xdb\xda\x9a\xcf\xee\x7ff\x8b<\n6\xbd\x94O~/0['K\xa4ja'\x9e\x15\x16;-7lfK\xb3H\x1d|^\x10z[&\x83\x01\xe3\x9a\xeb\xbd\xa2}-",
       +                          'num_htlcs': 5,
       +                          'htlc_signature': bfh("6548bd093a2bd2f4f053f0c6eb2c5f541d55eb8a2ede4d35fe974e5d3cd0eec3138bfd4115f4483c3b14e7988b48811d2da75f29f5e6eee691251fb4fba5a2610ba8fe7007117fe1c9fa1a6b01805c84cfffbb0eba674b64342c7cac567dea50728c1bb1aadc6d23fc2f4145027eafca82d6072cc9ce6529542099f728a0521e4b2044df5d02f7f2cdf84404762b1979528aa689a3e060a2a90ba8ef9a83d24d31ffb0d95c71d9fb9049b24ecf2c949c1486e7eb3ae160d70d54e441dc785dc57f7f3c9901b9537398c66f546cfc1d65e0748895d14699342c407fe119ac17db079b103720124a5ba22d4ba14c12832324dea9cb60c61ee74376ee7dcffdd1836e354aa8838ce3b37854fa91465cc40c73b702915e3580bfebaace805d52373b57ac755ebe4a8fe97e5fc21669bea124b809c79968479148f7174f39b8014542")}
       +                          ),
       +                         decode_msg(bfh("0084010101010101010101010101010101010101010101010101010101010101010106112951d0a6d7fc1dbca3bd1cdbda9acfee7f668b3c0a36bd944f7e2f305b274ba46a61279e15163b2d376c664bb3481d7c5e107a5b268301e39aebbda27d2d00056548bd093a2bd2f4f053f0c6eb2c5f541d55eb8a2ede4d35fe974e5d3cd0eec3138bfd4115f4483c3b14e7988b48811d2da75f29f5e6eee691251fb4fba5a2610ba8fe7007117fe1c9fa1a6b01805c84cfffbb0eba674b64342c7cac567dea50728c1bb1aadc6d23fc2f4145027eafca82d6072cc9ce6529542099f728a0521e4b2044df5d02f7f2cdf84404762b1979528aa689a3e060a2a90ba8ef9a83d24d31ffb0d95c71d9fb9049b24ecf2c949c1486e7eb3ae160d70d54e441dc785dc57f7f3c9901b9537398c66f546cfc1d65e0748895d14699342c407fe119ac17db079b103720124a5ba22d4ba14c12832324dea9cb60c61ee74376ee7dcffdd1836e354aa8838ce3b37854fa91465cc40c73b702915e3580bfebaace805d52373b57ac755ebe4a8fe97e5fc21669bea124b809c79968479148f7174f39b8014542")))
       +        # single htlc
       +        self.assertEqual(bfh("008401010101010101010101010101010101010101010101010101010101010101013b14af0c549dfb1fb287ff57c012371b3932996db5929eda5f251704751fb49d0dc2dcb88e5021575cb572fb71693758543f97d89e9165f913bfb7488d7cc26500012d31103b9f6e71131e4fee86fdfbdeba90e52b43fcfd11e8e53811cd4d59b2575ae6c3c82f85bea144c88cc35e568f1e6bdd0c57337e86de0b5da7cd9994067a"),
       +                         encode_msg(
       +                             "commitment_signed",
       +                             channel_id=b'\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01',
       +                             signature=b';\x14\xaf\x0cT\x9d\xfb\x1f\xb2\x87\xffW\xc0\x127\x1b92\x99m\xb5\x92\x9e\xda_%\x17\x04u\x1f\xb4\x9d\r\xc2\xdc\xb8\x8eP!W\\\xb5r\xfbqi7XT?\x97\xd8\x9e\x91e\xf9\x13\xbf\xb7H\x8d|\xc2e',
       +                             num_htlcs=1,
       +                             htlc_signature=bfh("2d31103b9f6e71131e4fee86fdfbdeba90e52b43fcfd11e8e53811cd4d59b2575ae6c3c82f85bea144c88cc35e568f1e6bdd0c57337e86de0b5da7cd9994067a"),
       +                         ))
       +        self.assertEqual(('commitment_signed',
       +                         {'channel_id': b'\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01',
       +                          'signature': b';\x14\xaf\x0cT\x9d\xfb\x1f\xb2\x87\xffW\xc0\x127\x1b92\x99m\xb5\x92\x9e\xda_%\x17\x04u\x1f\xb4\x9d\r\xc2\xdc\xb8\x8eP!W\\\xb5r\xfbqi7XT?\x97\xd8\x9e\x91e\xf9\x13\xbf\xb7H\x8d|\xc2e',
       +                          'num_htlcs': 1,
       +                          'htlc_signature': bfh("2d31103b9f6e71131e4fee86fdfbdeba90e52b43fcfd11e8e53811cd4d59b2575ae6c3c82f85bea144c88cc35e568f1e6bdd0c57337e86de0b5da7cd9994067a")}
       +                          ),
       +                         decode_msg(bfh("008401010101010101010101010101010101010101010101010101010101010101013b14af0c549dfb1fb287ff57c012371b3932996db5929eda5f251704751fb49d0dc2dcb88e5021575cb572fb71693758543f97d89e9165f913bfb7488d7cc26500012d31103b9f6e71131e4fee86fdfbdeba90e52b43fcfd11e8e53811cd4d59b2575ae6c3c82f85bea144c88cc35e568f1e6bdd0c57337e86de0b5da7cd9994067a")))
       +        # zero htlcs
       +        self.assertEqual(bfh("008401010101010101010101010101010101010101010101010101010101010101014e206ecf904d9237b1c5b4e08513555e9a5932c45b5f68be8764ce998df635ae04f6ce7bbcd3b4fd08e2daab7f9059b287ecab4155367b834682633497173f450000"),
       +                         encode_msg(
       +                             "commitment_signed",
       +                             channel_id=b'\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01',
       +                             signature=b'N n\xcf\x90M\x927\xb1\xc5\xb4\xe0\x85\x13U^\x9aY2\xc4[_h\xbe\x87d\xce\x99\x8d\xf65\xae\x04\xf6\xce{\xbc\xd3\xb4\xfd\x08\xe2\xda\xab\x7f\x90Y\xb2\x87\xec\xabAU6{\x83F\x82c4\x97\x17?E',
       +                             num_htlcs=0,
       +                             htlc_signature=bfh(""),
       +                         ))
       +        self.assertEqual(('commitment_signed',
       +                         {'channel_id': b'\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01',
       +                          'signature': b'N n\xcf\x90M\x927\xb1\xc5\xb4\xe0\x85\x13U^\x9aY2\xc4[_h\xbe\x87d\xce\x99\x8d\xf65\xae\x04\xf6\xce{\xbc\xd3\xb4\xfd\x08\xe2\xda\xab\x7f\x90Y\xb2\x87\xec\xabAU6{\x83F\x82c4\x97\x17?E',
       +                          'num_htlcs': 0,
       +                          'htlc_signature': bfh("")}
       +                          ),
       +                         decode_msg(bfh("008401010101010101010101010101010101010101010101010101010101010101014e206ecf904d9237b1c5b4e08513555e9a5932c45b5f68be8764ce998df635ae04f6ce7bbcd3b4fd08e2daab7f9059b287ecab4155367b834682633497173f450000")))
       +
       +    def test_encode_decode_msg__init(self):
       +        # "init" is interesting because it has TLVs optionally
       +        self.assertEqual(bfh("00100000000220c2"),
       +                         encode_msg(
       +                             "init",
       +                             gflen=0,
       +                             flen=2,
       +                             features=(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT |
       +                                       LnFeatures.GOSSIP_QUERIES_OPT |
       +                                       LnFeatures.GOSSIP_QUERIES_REQ |
       +                                       LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT),
       +                         ))
       +        self.assertEqual(bfh("00100000000220c2"),
       +                         encode_msg("init", gflen=0, flen=2, features=bfh("20c2")))
       +        self.assertEqual(bfh("00100000000220c2012043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000"),
       +                         encode_msg(
       +                             "init",
       +                             gflen=0,
       +                             flen=2,
       +                             features=(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT |
       +                                       LnFeatures.GOSSIP_QUERIES_OPT |
       +                                       LnFeatures.GOSSIP_QUERIES_REQ |
       +                                       LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT),
       +                             init_tlvs={
       +                                 'networks':
       +                                     {'chains': b'CI\x7f\xd7\xf8&\x95q\x08\xf4\xa3\x0f\xd9\xce\xc3\xae\xbay\x97 \x84\xe9\x0e\xad\x01\xea3\t\x00\x00\x00\x00'}
       +                             }
       +                         ))
       +        self.assertEqual(('init',
       +                         {'gflen': 2,
       +                          'globalfeatures': b'"\x00',
       +                          'flen': 3,
       +                          'features': b'\x02\xa2\xa1',
       +                          'init_tlvs': {}}
       +                          ),
       +                         decode_msg(bfh("001000022200000302a2a1")))
       +        self.assertEqual(('init',
       +                         {'gflen': 2,
       +                          'globalfeatures': b'"\x00',
       +                          'flen': 3,
       +                          'features': b'\x02\xaa\xa2',
       +                          'init_tlvs': {
       +                              'networks':
       +                                  {'chains': b'CI\x7f\xd7\xf8&\x95q\x08\xf4\xa3\x0f\xd9\xce\xc3\xae\xbay\x97 \x84\xe9\x0e\xad\x01\xea3\t\x00\x00\x00\x00'}
       +                          }}),
       +                         decode_msg(bfh("001000022200000302aaa2012043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000")))
   DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py
       t@@ -21,7 +21,7 @@ from electrum.util import bh2u, create_and_start_event_loop
        from electrum.lnpeer import Peer
        from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey
        from electrum.lnutil import LightningPeerConnectionClosed, RemoteMisbehaving
       -from electrum.lnutil import PaymentFailure, LnLocalFeatures, HTLCOwner
       +from electrum.lnutil import PaymentFailure, LnFeatures, HTLCOwner
        from electrum.lnchannel import channel_states, peer_states, Channel
        from electrum.lnrouter import LNPathFinder
        from electrum.channel_db import ChannelDB
       t@@ -95,8 +95,8 @@ class MockLNWallet(Logger):
                self.payments = {}
                self.logs = defaultdict(list)
                self.wallet = MockWallet()
       -        self.localfeatures = LnLocalFeatures(0)
       -        self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT
       +        self.features = LnFeatures(0)
       +        self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
                self.pending_payments = defaultdict(asyncio.Future)
                chan.lnworker = self
                chan.node_id = remote_keypair.pubkey
       t@@ -235,8 +235,8 @@ class TestPeer(ElectrumTestCase):
                w2.save_preimage(RHASH, payment_preimage)
                w2.save_payment_info(info)
                lnaddr = LnAddr(
       -                    RHASH,
       -                    amount_btc,
       +                    paymenthash=RHASH,
       +                    amount=amount_btc,
                            tags=[('c', lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
                                  ('d', 'coffee')
                                 ])
       t@@ -317,8 +317,9 @@ class TestPeer(ElectrumTestCase):
                alice_init_balance_msat = alice_channel.balance(HTLCOwner.LOCAL)
                bob_init_balance_msat = bob_channel.balance(HTLCOwner.LOCAL)
                num_payments = 50
       +        payment_value_sat = 10000  # make it large enough so that there are actually HTLCs on the ctx
                #pay_reqs1 = [self.prepare_invoice(w1, amount_sat=1) for i in range(num_payments)]
       -        pay_reqs2 = [self.prepare_invoice(w2, amount_sat=1) for i in range(num_payments)]
       +        pay_reqs2 = [self.prepare_invoice(w2, amount_sat=payment_value_sat) for i in range(num_payments)]
                max_htlcs_in_flight = asyncio.Semaphore(5)
                async def single_payment(pay_req):
                    async with max_htlcs_in_flight:
       t@@ -333,10 +334,10 @@ class TestPeer(ElectrumTestCase):
                    await gath
                with self.assertRaises(concurrent.futures.CancelledError):
                    run(f())
       -        self.assertEqual(alice_init_balance_msat - num_payments * 1000, alice_channel.balance(HTLCOwner.LOCAL))
       -        self.assertEqual(alice_init_balance_msat - num_payments * 1000, bob_channel.balance(HTLCOwner.REMOTE))
       -        self.assertEqual(bob_init_balance_msat + num_payments * 1000, bob_channel.balance(HTLCOwner.LOCAL))
       -        self.assertEqual(bob_init_balance_msat + num_payments * 1000, alice_channel.balance(HTLCOwner.REMOTE))
       +        self.assertEqual(alice_init_balance_msat - num_payments * payment_value_sat * 1000, alice_channel.balance(HTLCOwner.LOCAL))
       +        self.assertEqual(alice_init_balance_msat - num_payments * payment_value_sat * 1000, bob_channel.balance(HTLCOwner.REMOTE))
       +        self.assertEqual(bob_init_balance_msat + num_payments * payment_value_sat * 1000, bob_channel.balance(HTLCOwner.LOCAL))
       +        self.assertEqual(bob_init_balance_msat + num_payments * payment_value_sat * 1000, alice_channel.balance(HTLCOwner.REMOTE))
        
            @needs_test_with_all_chacha20_implementations
            def test_close(self):
       t@@ -354,7 +355,12 @@ class TestPeer(ElectrumTestCase):
                    await asyncio.wait_for(p2.initialized, 1)
                    # alice sends htlc
                    route = w1._create_route_from_invoice(decoded_invoice=lnaddr)
       -            htlc = p1.pay(route, alice_channel, int(lnaddr.amount * COIN * 1000), lnaddr.paymenthash, lnaddr.get_min_final_cltv_expiry())
       +            htlc = p1.pay(route=route,
       +                          chan=alice_channel,
       +                          amount_msat=int(lnaddr.amount * COIN * 1000),
       +                          payment_hash=lnaddr.paymenthash,
       +                          min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
       +                          payment_secret=lnaddr.payment_secret)
                    # alice closes
                    await p1.close_channel(alice_channel.channel_id)
                    gath.cancel()
   DIR diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py
       t@@ -4,9 +4,9 @@ import shutil
        import asyncio
        
        from electrum.util import bh2u, bfh, create_and_start_event_loop
       -from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet, OnionPerHop,
       +from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet,
                                      process_onion_packet, _decode_onion_error, decode_onion_error,
       -                              OnionFailureCode)
       +                              OnionFailureCode, OnionPacket)
        from electrum import bitcoin, lnrouter
        from electrum.constants import BitcoinTestnet
        from electrum.simple_config import SimpleConfig
       t@@ -57,46 +57,45 @@ class Test_LNRouter(TestCaseForTestnet):
                                             'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02cccccccccccccccccccccccccccccccc',
                                             'short_channel_id': bfh('0000000000000001'),
                                             'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
       -                                     'len': b'\x00\x00', 'features': b''}, trusted=True)
       +                                     'len': 0, 'features': b''}, trusted=True)
                self.assertEqual(cdb.num_channels, 1)
                cdb.add_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
                                             'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
                                             'short_channel_id': bfh('0000000000000002'),
                                             'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
       -                                     'len': b'\x00\x00', 'features': b''}, trusted=True)
       +                                     'len': 0, 'features': b''}, trusted=True)
                cdb.add_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
                                             'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
                                             'short_channel_id': bfh('0000000000000003'),
                                             'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
       -                                     'len': b'\x00\x00', 'features': b''}, trusted=True)
       +                                     'len': 0, 'features': b''}, trusted=True)
                cdb.add_channel_announcement({'node_id_1': b'\x02cccccccccccccccccccccccccccccccc', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
                                             'bitcoin_key_1': b'\x02cccccccccccccccccccccccccccccccc', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd',
                                             'short_channel_id': bfh('0000000000000004'),
                                             'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
       -                                     'len': b'\x00\x00', 'features': b''}, trusted=True)
       +                                     'len': 0, 'features': b''}, trusted=True)
                cdb.add_channel_announcement({'node_id_1': b'\x02dddddddddddddddddddddddddddddddd', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
                                             'bitcoin_key_1': b'\x02dddddddddddddddddddddddddddddddd', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
                                             'short_channel_id': bfh('0000000000000005'),
                                             'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
       -                                     'len': b'\x00\x00', 'features': b''}, trusted=True)
       +                                     'len': 0, 'features': b''}, trusted=True)
                cdb.add_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
                                             'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd',
                                             'short_channel_id': bfh('0000000000000006'),
                                             'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
       -                                     'len': b'\x00\x00', 'features': b''}, trusted=True)
       -        o = lambda i: i.to_bytes(8, "big")
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(99), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(999), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(99999999), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       -        cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
       +                                     'len': 0, 'features': b''}, trusted=True)
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 99, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 999, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 99999999, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
       +        cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
                path = path_finder.find_path_for_payment(b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 100000)
                self.assertEqual([(b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', b'\x00\x00\x00\x00\x00\x00\x00\x03'),
                                  (b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', b'\x00\x00\x00\x00\x00\x00\x00\x02'),
       t@@ -112,7 +111,7 @@ class Test_LNRouter(TestCaseForTestnet):
                cdb.sql_thread.join(timeout=1)
        
            @needs_test_with_all_chacha20_implementations
       -    def test_new_onion_packet(self):
       +    def test_new_onion_packet_legacy(self):
                # test vector from bolt-04
                payment_path_pubkeys = [
                    bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
       t@@ -124,28 +123,127 @@ class Test_LNRouter(TestCaseForTestnet):
                session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
                associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
                hops_data = [
       -            OnionHopsDataSingle(OnionPerHop(
       -                bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000')
       -            )),
       -            OnionHopsDataSingle(OnionPerHop(
       -                bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001')
       -            )),
       -            OnionHopsDataSingle(OnionPerHop(
       -                bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002')
       -            )),
       -            OnionHopsDataSingle(OnionPerHop(
       -                bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003')
       -            )),
       -            OnionHopsDataSingle(OnionPerHop(
       -                bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004')
       -            )),
       +            OnionHopsDataSingle(is_tlv_payload=False, payload={
       +                "amt_to_forward": {"amt_to_forward": 0},
       +                "outgoing_cltv_value": {"outgoing_cltv_value": 0},
       +                "short_channel_id": {"short_channel_id": bfh('0000000000000000')},
       +            }),
       +            OnionHopsDataSingle(is_tlv_payload=False, payload={
       +                "amt_to_forward": {"amt_to_forward": 1},
       +                "outgoing_cltv_value": {"outgoing_cltv_value": 1},
       +                "short_channel_id": {"short_channel_id": bfh('0101010101010101')},
       +            }),
       +            OnionHopsDataSingle(is_tlv_payload=False, payload={
       +                "amt_to_forward": {"amt_to_forward": 2},
       +                "outgoing_cltv_value": {"outgoing_cltv_value": 2},
       +                "short_channel_id": {"short_channel_id": bfh('0202020202020202')},
       +            }),
       +            OnionHopsDataSingle(is_tlv_payload=False, payload={
       +                "amt_to_forward": {"amt_to_forward": 3},
       +                "outgoing_cltv_value": {"outgoing_cltv_value": 3},
       +                "short_channel_id": {"short_channel_id": bfh('0303030303030303')},
       +            }),
       +            OnionHopsDataSingle(is_tlv_payload=False, payload={
       +                "amt_to_forward": {"amt_to_forward": 4},
       +                "outgoing_cltv_value": {"outgoing_cltv_value": 4},
       +                "short_channel_id": {"short_channel_id": bfh('0404040404040404')},
       +            }),
                ]
                packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data)
parazyd.org:70 /git/electrum/commit/158854f94e4405243cc9cf4c94c76665de7db571.gph:4118: line too long