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