Browse Source

Merge pull request #6050 from SomberNight/202003_lnmsg_rewrite

lnmsg rewrite, implement TLV, invoice features, varonion, payment secret
hard-fail-on-bad-server-string
ghost43 5 years ago
committed by GitHub
parent
commit
158854f94e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 54
      electrum/channel_db.py
  2. 903
      electrum/lightning.json
  3. 67
      electrum/lnaddr.py
  4. 17
      electrum/lnchannel.py
  5. 604
      electrum/lnmsg.py
  6. 210
      electrum/lnonion.py
  7. 161
      electrum/lnpeer.py
  8. 48
      electrum/lnrouter.py
  9. 189
      electrum/lnutil.py
  10. 5
      electrum/lnwire/README.md
  11. 53
      electrum/lnwire/onion_wire.csv
  12. 210
      electrum/lnwire/peer_wire.csv
  13. 59
      electrum/lnworker.py
  14. 60
      electrum/tests/test_bolt11.py
  15. 385
      electrum/tests/test_lnmsg.py
  16. 28
      electrum/tests/test_lnpeer.py
  17. 216
      electrum/tests/test_lnrouter.py
  18. 52
      electrum/tests/test_lnutil.py

54
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)

903
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"
}
}
}
}

67
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))

17
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

604
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)

210
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

161
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(

48
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

189
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

5
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
```

53
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
Can't render this file because it has a wrong number of fields in line 2.

210
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,
Can't render this file because it has a wrong number of fields in line 2.

59
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 = {

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

385
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")))

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

216
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

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

Loading…
Cancel
Save