electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 158854f94e4405243cc9cf4c94c76665de7db571
parent 371f55a0f922cf57196ae3d8d7c2fabc7a9f0f08
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:
Melectrum/channel_db.py | 54+++++++++++++++++++++++++++---------------------------
Delectrum/lightning.json | 903-------------------------------------------------------------------------------
Melectrum/lnaddr.py | 67++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Melectrum/lnchannel.py | 17+++++++++--------
Melectrum/lnmsg.py | 604+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Melectrum/lnonion.py | 210++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Melectrum/lnpeer.py | 161++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Melectrum/lnrouter.py | 48+++++++++++++++++++++++++++++++-----------------
Melectrum/lnutil.py | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Aelectrum/lnwire/README.md | 5+++++
Aelectrum/lnwire/onion_wire.csv | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrum/lnwire/peer_wire.csv | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/lnworker.py | 59++++++++++++++++++++++++++++++++++++++++-------------------
Melectrum/tests/test_bolt11.py | 60++++++++++++++++++++++++++++++++++++++++++++++--------------
Aelectrum/tests/test_lnmsg.py | 385+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/tests/test_lnpeer.py | 28+++++++++++++++++-----------
Melectrum/tests/test_lnrouter.py | 216+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Melectrum/tests/test_lnutil.py | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
18 files changed, 2001 insertions(+), 1320 deletions(-)

diff --git a/electrum/channel_db.py b/electrum/channel_db.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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: @@ -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 @@ -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) @@ -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) @@ -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""") @@ -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) diff --git a/electrum/lightning.json b/electrum/lightning.json @@ -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" - } - } - } -} diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py @@ -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. @@ -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. @@ -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)) @@ -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)) @@ -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): @@ -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)) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py @@ -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) @@ -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(), @@ -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 diff --git a/electrum/lnmsg.py b/electrum/lnmsg.py @@ -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) diff --git a/electrum/lnonion.py b/electrum/lnonion.py @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 @@ -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. @@ -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: @@ -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) @@ -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): @@ -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 @@ -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 diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -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 @@ -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 @@ -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() @@ -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): @@ -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(): @@ -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) @@ -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 @@ -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})") @@ -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 @@ -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']), @@ -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, ) @@ -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: @@ -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: @@ -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 ' @@ -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) @@ -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) @@ -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) @@ -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, @@ -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) @@ -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()}") @@ -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}") @@ -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) @@ -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, @@ -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() ) @@ -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): @@ -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")) @@ -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): @@ -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'] @@ -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( diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py @@ -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 @@ -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, @@ -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 @@ -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] @@ -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 @@ -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 diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -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 @@ -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: @@ -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. @@ -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): @@ -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 + diff --git a/electrum/lnwire/README.md b/electrum/lnwire/README.md @@ -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 +``` diff --git a/electrum/lnwire/onion_wire.csv b/electrum/lnwire/onion_wire.csv @@ -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 diff --git a/electrum/lnwire/peer_wire.csv b/electrum/lnwire/peer_wire.csv @@ -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, diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -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 @@ -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 {} @@ -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 @@ -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'): @@ -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() @@ -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: @@ -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 @@ -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()): @@ -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): @@ -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: @@ -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 = { diff --git a/electrum/tests/test_bolt11.py b/electrum/tests/test_bolt11.py @@ -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 @@ -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 @@ -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) @@ -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()) diff --git a/electrum/tests/test_lnmsg.py b/electrum/tests/test_lnmsg.py @@ -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"))) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py @@ -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 @@ -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 @@ -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') ]) @@ -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: @@ -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): @@ -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() diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py @@ -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 @@ -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'), @@ -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'), @@ -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) self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71e87f9aab8f6378c6ff744c1f34b393ad28d065b535c1a8668d85d3b34a1b3befd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a1f9e7abc789266cc861cabd95818c0fc8efbdfdc14e3f7c2bc7eb8d6a79ef75ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d014698cf05d742557763d9cb743faeae65dcc79dddaecf27fe5942be5380d15e9a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040a2a2fba158a0d8085926dc2e44f0c88bf487da56e13ef2d5e676a8589881b4869ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565a9f99728426ce2380a9580e2a9442481ceae7679906c30b1a0e21a10f26150e0645ab6edfdab1ce8f8bea7b1dee511c5fd38ac0e702c1c15bb86b52bca1b71e15b96982d262a442024c33ceb7dd8f949063c2e5e613e873250e2f8708bd4e1924abd45f65c2fa5617bfb10ee9e4a42d6b5811acc8029c16274f937dac9e8817c7e579fdb767ffe277f26d413ced06b620ede8362081da21cf67c2ca9d6f15fe5bc05f82f5bb93f8916bad3d63338ca824f3bbc11b57ce94a5fa1bc239533679903d6fec92a8c792fd86e2960188c14f21e399cfd72a50c620e10aefc6249360b463df9a89bf6836f4f26359207b765578e5ed76ae9f31b1cc48324be576e3d8e44d217445dba466f9b6293fdf05448584eb64f61e02903f834518622b7d4732471c6e0e22e22d1f45e31f0509eab39cdea5980a492a1da2aaac55a98a01216cd4bfe7abaa682af0fbff2dfed030ba28f1285df750e4d3477190dd193f8643b61d8ac1c427d590badb1f61a05d480908fbdc7c6f0502dd0c4abb51d725e92f95da2a8facb79881a844e2026911adcc659d1fb20a2fce63787c8bb0d9f6789c4b231c76da81c3f0718eb7156565a081d2be6b4170c0e0bcebddd459f53db2590c974bca0d705c055dee8c629bf854a5d58edc85228499ec6dde80cce4c8910b81b1e9e8b0f43bd39c8d69c3a80672729b7dc952dd9448688b6bd06afc2d2819cda80b66c57b52ccf7ac1a86601410d18d0c732f69de792e0894a9541684ef174de766fd4ce55efea8f53812867be6a391ac865802dbc26d93959df327ec2667c7256aa5a1d3c45a69a6158f285d6c97c3b8eedb09527848500517995a9eae4cd911df531544c77f5a9a2f22313e3eb72ca7a07dba243476bc926992e0d1e58b4a2fc8c7b01e0cad726237933ea319bad7537d39f3ed635d1e6c1d29e97b3d2160a09e30ee2b65ac5bce00996a73c008bcf351cecb97b6833b6d121dcf4644260b2946ea204732ac9954b228f0beaa15071930fd9583dfc466d12b5f0eeeba6dcf23d5ce8ae62ee5796359d97a4a15955c778d868d0ef9991d9f2833b5bb66119c5f8b396fd108baed7906cbb3cc376d13551caed97fece6f42a4c908ee279f1127fda1dd3ee77d8de0a6f3c135fa3f1cffe38591b6738dc97b55f0acc52be9753ce53e64d7e497bb00ca6123758df3b68fad99e35c04389f7514a8e36039f541598a417275e77869989782325a15b5342ac5011ff07af698584b476b35d941a4981eac590a07a092bb50342da5d3341f901aa07964a8d02b623c7b106dd0ae50bfa007a22d46c8772fa55558176602946cb1d11ea5460db7586fb89c6d3bcd3ab6dd20df4a4db63d2e7d52380800ad812b8640887e027e946df96488b47fbc4a4fadaa8beda4abe446fafea5403fae2ef'), packet.to_bytes()) @needs_test_with_all_chacha20_implementations - def test_process_onion_packet(self): + def test_new_onion_packet_mixed_payloads(self): + # test vector from bolt-04 + payment_path_pubkeys = [ + bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), + bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'), + bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'), + bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'), + bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'), + ] + session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') + associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') + hops_data = [ + 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=True), + OnionHopsDataSingle(is_tlv_payload=True), + OnionHopsDataSingle(is_tlv_payload=True), + 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')}, + }), + ] + hops_data[1]._raw_bytes_payload = bfh("0101010101010101000000000000000100000001") + hops_data[2]._raw_bytes_payload = bfh("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") + hops_data[3]._raw_bytes_payload = bfh("0303030303030303000000000000000300000003") + packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data) + self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a710f8eaf9ccc768f66bb5dec1f7827f33c43fe2ddd05614c8283aa78e9e7573f87c50f7d61ab590531cf08000178a333a347f8b4072e1cea42da7552402b10765adae3f581408f35ff0a71a34b78b1d8ecae77df96c6404bae9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf057f5995d9731c4bf796fb0e41c885d488dcbc68eb742e27f44310b276edc6f652658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d710ee5c193921909bdd75db331cd9d7581a39fca50814ed8d9d402b86e7f8f6ac2f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa3379834bc2380d58e9d23237821475a1874484783a15d68f47d3dc339f38d9bf925655d5c946778680fd6d1f062f84128895aff09d35d6c92cca63d3f95a9ee8f2a84f383b4d6a087533e65de12fc8dcaf85777736a2088ff4b22462265028695b37e70963c10df8ef2458756c73007dc3e544340927f9e9f5ea4816a9fd9832c311d122e9512739a6b4714bba590e31caa143ce83cb84b36c738c60c3190ff70cd9ac286a9fd2ab619399b68f1f7447be376ce884b5913c8496d01cbf7a44a60b6e6747513f69dc538f340bc1388e0fde5d0c1db50a4dcb9cc0576e0e2474e4853af9623212578d502757ffb2e0e749695ed70f61c116560d0d4154b64dcf3cbf3c91d89fb6dd004dc19588e3479fcc63c394a4f9e8a3b8b961fce8a532304f1337f1a697a1bb14b94d2953f39b73b6a3125d24f27fcd4f60437881185370bde68a5454d816e7a70d4cea582effab9a4f1b730437e35f7a5c4b769c7b72f0346887c1e63576b2f1e2b3706142586883f8cf3a23595cc8e35a52ad290afd8d2f8bcd5b4c1b891583a4159af7110ecde092079209c6ec46d2bda60b04c519bb8bc6dffb5c87f310814ef2f3003671b3c90ddf5d0173a70504c2280d31f17c061f4bb12a978122c8a2a618bb7d1edcf14f84bf0fa181798b826a254fca8b6d7c81e0beb01bd77f6461be3c8647301d02b04753b0771105986aa0cbc13f7718d64e1b3437e8eef1d319359914a7932548c91570ef3ea741083ca5be5ff43c6d9444d29df06f76ec3dc936e3d180f4b6d0fbc495487c7d44d7c8fe4a70d5ff1461d0d9593f3f898c919c363fa18341ce9dae54f898ccf3fe792136682272941563387263c51b2a2f32363b804672cc158c9230472b554090a661aa81525d11876eefdcc45442249e61e07284592f1606491de5c0324d3af4be035d7ede75b957e879e9770cdde2e1bbc1ef75d45fe555f1ff6ac296a2f648eeee59c7c08260226ea333c285bcf37a9bbfa57ba2ab8083c4be6fc2ebe279537d22da96a07392908cf22b233337a74fe5c603b51712b43c3ee55010ee3d44dd9ba82bba3145ec358f863e04bbfa53799a7a9216718fd5859da2f0deb77b8e315ad6868fdec9400f45a48e6dc8ddbaeb3'), + packet.to_bytes()) + + @needs_test_with_all_chacha20_implementations + def test_process_onion_packet_mixed_payloads(self): + # this test is not from bolt-04, but is based on the one there; + # here the TLV payloads are actually sane... + payment_path_pubkeys = [ + bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), + bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'), + bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'), + bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'), + bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'), + ] + payment_path_privkeys = [ + bfh('4141414141414141414141414141414141414141414141414141414141414141'), + bfh('4242424242424242424242424242424242424242424242424242424242424242'), + bfh('4343434343434343434343434343434343434343434343434343434343434343'), + bfh('4444444444444444444444444444444444444444444444444444444444444444'), + bfh('4545454545454545454545454545454545454545454545454545454545454545'), + ] + session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') + associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') + hops_data = [ + 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=True, 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=True, 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=True, 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) + self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71bde5adfa90b337f34616d8673d09dd055937273045566ce537ffbe3f9d1f263dc10c7d61ae590536c609010079a232a247922a5395359a63dfbefb85f40317e23254f3023f7d4a98f746c9ab06647645ce55c67308e3c77dc87a1caeac51b03b23c60f05e536e1d757c8c1093e34accfc4f97b5920f6dd2069d5b9ddbb384c3ac575e999a92a4434470ab0aa040c4c3cace3162a405842a88be783e64fad54bd6727c23fc446b7ec0dc3eec5a03eb6c70ec2784911c9e6d274322ec465f0972eb8e771b149f319582ba64dbc2b8e56a3ea79002801c09354f1541cf79bd1dccf5d6bd6b6bacc87a0f24ce497e14e8037e5a79fb4d9ca63fe47f17765963e8f17468a5eaec19a6cca2bfc4e4a366fea3a92112a945856be55e45197ecbab523025e7589529c30cc8addc8fa39d23ef64fa2e51a219c3bd4d3c484832f8e5af16bc46cdba0403991f4fc1b74beef857acf15fefed82ac8678ca66d26262c681beddfdb485aa498813b1a6c5833f1339c1a35244ab76baa0ccaf681ec1f54004e387063335648a77b65d90dde74f1c4b0a729ca25fa53256f7db6d35818b4e5910ba78ec69cf3646bf248ef46cf9cc33062662de2afe4dcf005951b85fd759429fa1ae490b78b14132ccb791232a6c680f03634c0136817f51bf9603a0dba405e7b347830be4327fceccd4734456842b82cf6275393b279bc6ac93d743e00a2d6042960089f70c782ce554b9f73eeeefeea50df7f6f80de1c4e869a7b502f9a5df30d1175402fa780812d35c6d489a30bb0cea53a1088669a238cccf416ecb37f8d8e6ea1327b64979d48e937db69a44a902923a75113685a4aca4a8d9c62b388b48d9c9e2ab9c2df4d529223144de6e16f2dd95a063da79163b3fe006a80263cde4410648f7c3e1f4a7707f82eb0e209002d972c7e57b4ff8ce063fa7b4140f52f569f0cc8793a97a170613efb6b27ba3a0370f8ea74fc0d6aabba54e0ee967abc70e87b580d2aac244236b7752db9d83b159afc1faf6b44b697643235bf59e99f43428caff409d26b9139538865b1f5cf4699f9296088aca461209024ad1dd00e3566e4fde2117b7b3ffced6696b735816a00199890056de86dcbb1b930228143dbf04f07c0eb34370089ea55c43b2c4546cbe1ff0c3a6217d994af9b4225f4b5acb1e3129f5f5b98d381a4692a8561c670b2ee95869f9614e76bb07f623c5194e1c9d26334026f8f5437ec1cde526f914fa094a465f0adcea32b79bfa44d2562536b0d8366da9ee577666c1d5e39615444ca5c900b8199fafac002b8235688eaa0c6887475a913b37d9a4ed43a894ea4576102e5d475ae0b962240ea95fc367b7ac214a4f8682448a9c0d2eea35727bdedc235a975ecc8148a5b03d6291a051dbefe19c8b344d2713c6664dd94ced53c6be39a837fbf1169cca6a12b0a2710f443ba1afeecb51e94236b2a6ed1c2f365b595443b1515de86dcb8c67282807789b47c331cde2fdd721262bef165fa96b7919d11bc5f2022f5affffdd747c7dbe3de8add829a0a8913519fdf7dba4e8a7a25456d2d559746d39ea6ffa31c7b904792fb734bba30f2e1adf7457a994513a1807785fe7b22bf419d1f407f8e2db8b22c0512b078c0cfdfd599e6c4a9d0cc624b9e24b87f30541c3248cd6643df15d251775cc457df4ea6b4e4c5990d87541028c6f0eb28502db1c11a92797168d0b68cb0a0d345b3a3ad05fc4016862f403c64670c41a2c0c6d4e384f5f7da6a204a24530a51182fd7164f120e74a78decb1ab6cda6b9cfc68ac0a35f7a57e750ead65a8e0429cc16e733b9e4feaea25d06c1a4768'), + packet.to_bytes()) + for i, privkey in enumerate(payment_path_privkeys): + processed_packet = process_onion_packet(packet, associated_data, privkey) + self.assertEqual(hops_data[i].to_bytes(), processed_packet.hop_data.to_bytes()) + packet = processed_packet.next_packet + + @needs_test_with_all_chacha20_implementations + def test_process_onion_packet_legacy(self): # this test is not from bolt-04, but is based on the one there; # except here we have the privkeys for these pubkeys payment_path_pubkeys = [ @@ -165,28 +263,38 @@ 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) self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661954176cd9869da33d713aa219fcef1e5c806fef11e696bcc66844de8271c27974a049d041ffc5be934b8575c6ff4371f2f88d4edfd73e445534d3f6ae15b64b0d8308390bebf8d149002e31bdc283056477ba27c8054c248ad7306de31663a7c99ec65b251704041f7c4cc40a0016ba172fbf805ec59132a65a4c7eb1f41337931c5df0f840704535729262d30c6132d1b390f073edec8fa057176c6268b6ad06a82ff0229c3be444ee50b40686bc1306838b93c65771de1b6ca05dace1ff9814a6e58b2dd71e8244c83e28b2ed5a3b09e9e7df5c8c747e5765ba366a4f7407a6c6b0a32fb5521cce7cd668f7434c909c1be027d8595d85893e5f612c49a93eeeed80a78bab9c4a621ce0f6f5df7d64a9c8d435db19de192d9db522c7f7b4e201fc1b61a9bd3efd062ae24455d463818b01e2756c7d0691bc3ac4c017be34c9a8b2913bb1b94056bf7a21730afc3f254ffa41ca140a5d87ff470f536d08619e8004d50de2fe5954d6aa4a00570da397ba15ae9ea4d7d1f136256a9093f0a787a36cbb3520b6a3cf4d1b13b16bf399c4b0326da1382a90bd79cf92f4808c8c84eaa50a8ccf44acbde0e35b2e6b72858c8446d6a05f3ba70fb4adc70af27cea9bd1dc1ea35fb3cc236b8b9b69b614903db339b22ad5dc2ddda7ac65fd7de24e60b7dbba7aafc9d26c0f9fcb03f1bb85dfc21762f862620651db52ea703ae60aa7e07febf11caa95c4245a4b37eb9c233e1ab1604fb85849e7f49cb9f7c681c4d91b7c320eb4b89b9c6bcb636ceadda59f6ed47aa5b1ea0a946ea98f6ad2614e79e0d4ef96d6d65903adb0479709e03008bbdf3355dd87df7d68965fdf1ad5c68b6dc2761b96b10f8eb4c0329a646bf38cf4702002e61565231b4ac7a9cde63d23f7b24c9d42797b3c434558d71ed8bf9fcab2c2aee3e8b38c19f9edc3ad3dfe9ebba7387ce4764f97ed1c1a83552dff8315546761479a6f929c39bcca0891d4a967d1b77fa80feed6ae74ac82ed5fb7be225c3f2b0ebdc652afc2255c47bc318ac645bbf19c0819ff527ff6708a78e19c8ca3dc8087035e10d5ac976e84b71148586c8a5a7b26ed11b5b401ce7bb2ac532207eaa24d2f53aaa8024607da764d807c91489e82fcad04e6b8992a507119367f576ee5ffe6807d5723d60234d4c3f94adce0acfed9dba535ca375446a4e9b500b74ad2a66e1c6b0fc38933f282d3a4a877bceceeca52b46e731ca51a9534224a883c4a45587f973f73a22069a4154b1da03d307d8575c821bef0eef87165b9a1bbf902ecfca82ddd805d10fbb7147b496f6772f01e9bf542b00288f3a6efab32590c1f34535ece03a0587ca187d27a98d4c9aa7c044794baa43a81abbe307f51d0bda6e7b4cf62c4be553b176321777e7fd483d6cec16df137293aaf3ad53608e1c7831368675bb9608db04d5c859e7714edab3d2389837fa071f0795adfabc51507b1adbadc7f83e80bd4e4eb9ed1a89c9e0a6dc16f38d55181d5666b02150651961aab34faef97d80fa4e1960864dfec3b687fd4eadf7aa6c709cb4698ae86ae112f386f33731d996b9d41926a2e820c6ba483a61674a4bae03af37e872ffdc0a9a8a034327af17e13e9e7ac619c9188c2a5c12a6ebf887721455c0e2822e67a621ed49f1f50dfc38b71c29d0224954e84ced086c80de552cca3a14adbe43035901225bafc3db3b672c780e4fa12b59221f93690527efc16a28e7c63d1a99fc881f023b03a157076a7e999a715ed37521adb483e2477d75ba5a55d4abad22b024c5317334b6544f15971591c774d896229e4e668fc1c7958fbd76fa0b152a6f14c95692083badd066b6621367fd73d88ba8d860566e6d55b871d80c68296b80ae8847d'), packet.to_bytes()) for i, privkey in enumerate(payment_path_privkeys): processed_packet = process_onion_packet(packet, associated_data, privkey) - self.assertEqual(hops_data[i].per_hop.to_bytes(), processed_packet.hop_data.per_hop.to_bytes()) + self.assertEqual(hops_data[i].to_bytes(), processed_packet.hop_data.to_bytes()) packet = processed_packet.next_packet @needs_test_with_all_chacha20_implementations diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py @@ -8,7 +8,7 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see make_htlc_tx_inputs, secret_to_pubkey, derive_blinded_pubkey, derive_privkey, derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret, get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, - ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc) + ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures) from electrum.util import bh2u, bfh, MyEncoder from electrum.transaction import Transaction, PartialTransaction @@ -755,3 +755,53 @@ class TestLNUtil(ElectrumTestCase): with self.assertRaises(ConnStringFormatError): extract_nodeid("00" * 33 + "@") self.assertEqual(extract_nodeid("00" * 33 + "@localhost"), (b"\x00" * 33, "localhost")) + + def test_ln_features_validate_transitive_dependecies(self): + features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ + self.assertTrue(features.validate_transitive_dependecies()) + features = LnFeatures.PAYMENT_SECRET_OPT + self.assertFalse(features.validate_transitive_dependecies()) + features = LnFeatures.PAYMENT_SECRET_REQ + self.assertFalse(features.validate_transitive_dependecies()) + features = LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ + self.assertTrue(features.validate_transitive_dependecies()) + features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ + self.assertFalse(features.validate_transitive_dependecies()) + features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT + self.assertTrue(features.validate_transitive_dependecies()) + features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ + self.assertTrue(features.validate_transitive_dependecies()) + + def test_ln_features_for_init_message(self): + features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ + self.assertEqual(features, features.for_init_message()) + features = LnFeatures.PAYMENT_SECRET_OPT + self.assertEqual(features, features.for_init_message()) + features = LnFeatures.PAYMENT_SECRET_REQ + self.assertEqual(features, features.for_init_message()) + features = LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ + self.assertEqual(features, features.for_init_message()) + features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ + self.assertEqual(features, features.for_init_message()) + features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT + self.assertEqual(features, features.for_init_message()) + features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ + self.assertEqual(features, features.for_init_message()) + + def test_ln_features_for_invoice(self): + features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ + self.assertEqual(LnFeatures(0), features.for_invoice()) + features = LnFeatures.PAYMENT_SECRET_OPT + self.assertEqual(features, features.for_invoice()) + features = LnFeatures.PAYMENT_SECRET_REQ + self.assertEqual(features, features.for_invoice()) + features = LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ + self.assertEqual(features, features.for_invoice()) + features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ + self.assertEqual(LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ, + features.for_invoice()) + features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ + self.assertEqual(LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT, + features.for_invoice()) + features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ + self.assertEqual(features, features.for_invoice())