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 . import constants
from .util import bh2u, profiler, get_headers_dir, bfh, is_ip_address, list_enabled_bits from .util import bh2u, profiler, get_headers_dir, bfh, is_ip_address, list_enabled_bits
from .logging import Logger 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 .lnverifier import LNChannelVerifier, verify_sig_for_channel_update
from .lnmsg import decode_msg from .lnmsg import decode_msg
@ -47,15 +48,6 @@ if TYPE_CHECKING:
from .lnchannel import Channel 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_DISABLE = 1 << 1
FLAG_DIRECTION = 1 << 0 FLAG_DIRECTION = 1 << 0
@ -102,14 +94,14 @@ class Policy(NamedTuple):
def from_msg(payload: dict) -> 'Policy': def from_msg(payload: dict) -> 'Policy':
return Policy( return Policy(
key = payload['short_channel_id'] + payload['start_node'], key = payload['short_channel_id'] + payload['start_node'],
cltv_expiry_delta = int.from_bytes(payload['cltv_expiry_delta'], "big"), cltv_expiry_delta = payload['cltv_expiry_delta'],
htlc_minimum_msat = int.from_bytes(payload['htlc_minimum_msat'], "big"), htlc_minimum_msat = payload['htlc_minimum_msat'],
htlc_maximum_msat = int.from_bytes(payload['htlc_maximum_msat'], "big") if 'htlc_maximum_msat' in payload else None, htlc_maximum_msat = payload.get('htlc_maximum_msat', None),
fee_base_msat = int.from_bytes(payload['fee_base_msat'], "big"), fee_base_msat = payload['fee_base_msat'],
fee_proportional_millionths = int.from_bytes(payload['fee_proportional_millionths'], "big"), fee_proportional_millionths = payload['fee_proportional_millionths'],
message_flags = int.from_bytes(payload['message_flags'], "big"), message_flags = int.from_bytes(payload['message_flags'], "big"),
channel_flags = int.from_bytes(payload['channel_flags'], "big"), channel_flags = int.from_bytes(payload['channel_flags'], "big"),
timestamp = int.from_bytes(payload['timestamp'], "big") timestamp = payload['timestamp'],
) )
@staticmethod @staticmethod
@ -154,7 +146,7 @@ class NodeInfo(NamedTuple):
alias = alias.decode('utf8') alias = alias.decode('utf8')
except: except:
alias = '' alias = ''
timestamp = int.from_bytes(payload['timestamp'], "big") timestamp = payload['timestamp']
node_info = NodeInfo(node_id=node_id, features=features, timestamp=timestamp, alias=alias) node_info = NodeInfo(node_id=node_id, features=features, timestamp=timestamp, alias=alias)
return node_info, peer_addrs return node_info, peer_addrs
@ -321,11 +313,12 @@ class ChannelDB(SqlDB):
return ret return ret
# note: currently channel announcements are trusted by default (trusted=True); # 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. # even slower; especially as servers will start throttling us.
# It would probably put significant strain on servers if all clients # It would probably put significant strain on servers if all clients
# verified the complete gossip. # verified the complete gossip.
def add_channel_announcement(self, msg_payloads, *, trusted=True): def add_channel_announcement(self, msg_payloads, *, trusted=True):
# note: signatures have already been verified.
if type(msg_payloads) is dict: if type(msg_payloads) is dict:
msg_payloads = [msg_payloads] msg_payloads = [msg_payloads]
added = 0 added = 0
@ -338,8 +331,8 @@ class ChannelDB(SqlDB):
continue continue
try: try:
channel_info = ChannelInfo.from_msg(msg) channel_info = ChannelInfo.from_msg(msg)
except UnknownEvenFeatureBits: except IncompatibleOrInsaneFeatures as e:
self.logger.info("unknown feature bits") self.logger.info(f"unknown or insane feature bits: {e!r}")
continue continue
if trusted: if trusted:
added += 1 added += 1
@ -353,7 +346,7 @@ class ChannelDB(SqlDB):
def add_verified_channel_info(self, msg: dict, *, capacity_sat: int = None) -> None: def add_verified_channel_info(self, msg: dict, *, capacity_sat: int = None) -> None:
try: try:
channel_info = ChannelInfo.from_msg(msg) channel_info = ChannelInfo.from_msg(msg)
except UnknownEvenFeatureBits: except IncompatibleOrInsaneFeatures:
return return
channel_info = channel_info._replace(capacity_sat=capacity_sat) channel_info = channel_info._replace(capacity_sat=capacity_sat)
with self.lock: with self.lock:
@ -392,7 +385,7 @@ class ChannelDB(SqlDB):
now = int(time.time()) now = int(time.time())
for payload in payloads: for payload in payloads:
short_channel_id = ShortChannelID(payload['short_channel_id']) 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: if max_age and now - timestamp > max_age:
expired.append(payload) expired.append(payload)
continue continue
@ -407,7 +400,7 @@ class ChannelDB(SqlDB):
known.append(payload) known.append(payload)
# compare updates to existing database entries # compare updates to existing database entries
for payload in known: for payload in known:
timestamp = int.from_bytes(payload['timestamp'], "big") timestamp = payload['timestamp']
start_node = payload['start_node'] start_node = payload['start_node']
short_channel_id = ShortChannelID(payload['short_channel_id']) short_channel_id = ShortChannelID(payload['short_channel_id'])
key = (start_node, 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}') raise Exception(f'failed verifying channel update for {short_channel_id}')
def add_node_announcement(self, msg_payloads): def add_node_announcement(self, msg_payloads):
# note: signatures have already been verified.
if type(msg_payloads) is dict: if type(msg_payloads) is dict:
msg_payloads = [msg_payloads] msg_payloads = [msg_payloads]
new_nodes = {} new_nodes = {}
for msg_payload in msg_payloads: for msg_payload in msg_payloads:
try: try:
node_info, node_addresses = NodeInfo.from_msg(msg_payload) node_info, node_addresses = NodeInfo.from_msg(msg_payload)
except UnknownEvenFeatureBits: except IncompatibleOrInsaneFeatures:
continue continue
node_id = node_info.node_id node_id = node_info.node_id
# Ignore node if it has no associated channel (DoS protection) # 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] self._recent_peers = sorted_node_ids[:self.NUM_MAX_RECENT_PEERS]
c.execute("""SELECT * FROM channel_info""") c.execute("""SELECT * FROM channel_info""")
for short_channel_id, msg in c: 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 self._channels[ShortChannelID.normalize(short_channel_id)] = ci
c.execute("""SELECT * FROM node_info""") c.execute("""SELECT * FROM node_info""")
for node_id, msg in c: 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 # don't load node_addresses because they dont have timestamps
self._nodes[node_id] = node_info self._nodes[node_id] = node_info
c.execute("""SELECT * FROM policy""") c.execute("""SELECT * FROM policy""")
@ -671,7 +671,7 @@ class ChannelDB(SqlDB):
return return
now = int(time.time()) now = int(time.time())
remote_update_decoded = decode_msg(remote_update_raw)[1] 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 remote_update_decoded['start_node'] = node_id
return Policy.from_msg(remote_update_decoded) return Policy.from_msg(remote_update_decoded)
elif node_id == chan.get_local_pubkey(): # outgoing direction (from us) 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): def tagged_bytes(char, l):
return tagged(char, bitstring.BitArray(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. # Discard trailing bits, convert to bytes.
def trim_to_bytes(barr): def trim_to_bytes(barr):
# Adds a byte if necessary. # Adds a byte if necessary.
@ -155,7 +170,7 @@ def pull_tagged(stream):
length = stream.read(5).uint * 32 + stream.read(5).uint length = stream.read(5).uint * 32 + stream.read(5).uint
return (CHARSET[tag], stream.read(length * 5), stream) return (CHARSET[tag], stream.read(length * 5), stream)
def lnencode(addr, privkey): def lnencode(addr: 'LnAddr', privkey) -> str:
if addr.amount: if addr.amount:
amount = Decimal(str(addr.amount)) amount = Decimal(str(addr.amount))
# We can only send down to millisatoshi. # We can only send down to millisatoshi.
@ -172,16 +187,22 @@ def lnencode(addr, privkey):
# Start with the timestamp # Start with the timestamp
data = bitstring.pack('uint:35', addr.date) data = bitstring.pack('uint:35', addr.date)
tags_set = set()
# Payment hash # Payment hash
data += tagged_bytes('p', addr.paymenthash) 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: for k, v in addr.tags:
# BOLT #11: # BOLT #11:
# #
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields, # 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: if k in tags_set:
raise ValueError("Duplicate '{}' tag".format(k)) raise ValueError("Duplicate '{}' tag".format(k))
@ -196,23 +217,23 @@ def lnencode(addr, privkey):
elif k == 'd': elif k == 'd':
data += tagged_bytes('d', v.encode()) data += tagged_bytes('d', v.encode())
elif k == 'x': elif k == 'x':
# Get minimal length by trimming leading 5 bits at a time. expirybits = bitstring.pack('intbe:64', v)
expirybits = bitstring.pack('intbe:64', v)[4:64] expirybits = trim_to_min_length(expirybits)
while expirybits.startswith('0b00000'):
if len(expirybits) == 5:
break # v == 0
expirybits = expirybits[5:]
data += tagged('x', expirybits) data += tagged('x', expirybits)
elif k == 'h': elif k == 'h':
data += tagged_bytes('h', sha256(v.encode('utf-8')).digest()) data += tagged_bytes('h', sha256(v.encode('utf-8')).digest())
elif k == 'n': elif k == 'n':
data += tagged_bytes('n', v) data += tagged_bytes('n', v)
elif k == 'c': elif k == 'c':
# Get minimal length by trimming leading 5 bits at a time. finalcltvbits = bitstring.pack('intbe:64', v)
finalcltvbits = bitstring.pack('intbe:64', v)[4:64] finalcltvbits = trim_to_min_length(finalcltvbits)
while finalcltvbits.startswith('0b00000'):
finalcltvbits = finalcltvbits[5:]
data += tagged('c', 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: else:
# FIXME: Support unknown tags? # FIXME: Support unknown tags?
raise ValueError("Unknown tag {}".format(k)) raise ValueError("Unknown tag {}".format(k))
@ -239,15 +260,17 @@ def lnencode(addr, privkey):
return bech32_encode(hrp, bitarray_to_u5(data)) return bech32_encode(hrp, bitarray_to_u5(data))
class LnAddr(object): 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.date = int(time.time()) if not date else int(date)
self.tags = [] if not tags else tags self.tags = [] if not tags else tags
self.unknown_tags = [] self.unknown_tags = []
self.paymenthash = paymenthash self.paymenthash = paymenthash
self.payment_secret = payment_secret
self.signature = None self.signature = None
self.pubkey = None self.pubkey = None
self.currency = constants.net.SEGWIT_HRP if currency is None else currency 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 self._min_final_cltv_expiry = 9
def __str__(self): def __str__(self):
@ -383,14 +406,28 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
continue continue
addr.paymenthash = trim_to_bytes(tagdata) 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': elif tag == 'n':
if data_length != 53: if data_length != 53:
addr.unknown_tags.append((tag, tagdata)) addr.unknown_tags.append((tag, tagdata))
continue continue
pubkeybytes = trim_to_bytes(tagdata) pubkeybytes = trim_to_bytes(tagdata)
addr.pubkey = pubkeybytes addr.pubkey = pubkeybytes
elif tag == 'c': elif tag == 'c':
addr._min_final_cltv_expiry = tagdata.int 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: else:
addr.unknown_tags.append((tag, tagdata)) addr.unknown_tags.append((tag, tagdata))

17
electrum/lnchannel.py

@ -218,13 +218,13 @@ class Channel(Logger):
short_channel_id=self.short_channel_id, short_channel_id=self.short_channel_id,
channel_flags=channel_flags, channel_flags=channel_flags,
message_flags=b'\x01', message_flags=b'\x01',
cltv_expiry_delta=lnutil.NBLOCK_OUR_CLTV_EXPIRY_DELTA.to_bytes(2, byteorder="big"), cltv_expiry_delta=lnutil.NBLOCK_OUR_CLTV_EXPIRY_DELTA,
htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat.to_bytes(8, byteorder="big"), htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat,
htlc_maximum_msat=htlc_maximum_msat.to_bytes(8, byteorder="big"), htlc_maximum_msat=htlc_maximum_msat,
fee_base_msat=lnutil.OUR_FEE_BASE_MSAT.to_bytes(4, byteorder="big"), fee_base_msat=lnutil.OUR_FEE_BASE_MSAT,
fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS.to_bytes(4, byteorder="big"), fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS,
chain_hash=constants.net.rev_genesis_bytes(), chain_hash=constants.net.rev_genesis_bytes(),
timestamp=now.to_bytes(4, byteorder="big"), timestamp=now,
) )
sighash = sha256d(chan_upd[2 + 64:]) sighash = sha256d(chan_upd[2 + 64:])
sig = ecc.ECPrivkey(self.lnworker.node_keypair.privkey).sign(sighash, ecc.sig_string_from_r_and_s) 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 node_ids = sorted_node_ids
bitcoin_keys.reverse() bitcoin_keys.reverse()
chan_ann = encode_msg("channel_announcement", chan_ann = encode_msg(
"channel_announcement",
len=0, len=0,
features=b'', features=b'',
chain_hash=constants.net.rev_genesis_bytes(), chain_hash=constants.net.rev_genesis_bytes(),
@ -257,7 +258,7 @@ class Channel(Logger):
node_id_1=node_ids[0], node_id_1=node_ids[0],
node_id_2=node_ids[1], node_id_2=node_ids[1],
bitcoin_key_1=bitcoin_keys[0], 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 self._chan_ann_without_sigs = chan_ann

604
electrum/lnmsg.py

@ -1,153 +1,513 @@
import json
import os 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 from collections import OrderedDict
def _eval_length_term(x, ma: dict) -> int: from .lnutil import OnionFailureCodeMetaFlag
"""
Evaluate a term of the simple language used
to specify lightning message field lengths.
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 class MalformedMsg(Exception): pass
assumed big-endian bytes and decoded. 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
""" def _num_remaining_bytes_to_read(fd: io.BytesIO) -> int:
try: cur_pos = fd.tell()
x = int(x) end_pos = fd.seek(0, io.SEEK_END)
except ValueError: fd.seek(cur_pos)
x = ma[x] 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: try:
x = int(x) first = fd.read(1)[0]
except ValueError: except IndexError:
x = int.from_bytes(x, byteorder='big') return None # end of file
return x 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 # 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?
exp = str(exp) def _read_field(*, fd: io.BytesIO, field_type: str, count: Union[int, str]) -> Union[bytes, int]:
if "*" in exp: if not fd: raise Exception()
assert "+" not in exp if isinstance(count, int):
result = 1 assert count >= 0, f"{count!r} must be non-neg int"
for term in exp.split("*"): elif count == "...":
result *= _eval_length_term(term, ctx) pass
return result else:
return sum(_eval_length_term(x, ctx) for x in exp.split("+")) raise Exception(f"unexpected field count: {count!r}")
if count == 0:
def _make_handler(msg_name: str, v: dict) -> Callable[[bytes], Tuple[str, dict]]: return b""
""" type_len = None
Generate a message handler function (taking bytes) if field_type == 'byte':
for message type `msg_name` with specification `v` 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', # TODO: maybe for "value" we could accept a list with len "count" of appropriate items
and `v` could be 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]: if field_count_str == "":
ma = {} # map of field name -> field data; after parsing msg field_count = 1
pos = 0 elif field_count_str == "...":
for fieldname in v["payload"]: if not allow_any:
poslenMap = v["payload"][fieldname] raise Exception("field count is '...' but allow_any is False")
if "feature" in poslenMap and pos == len(data): return field_count_str
continue else:
#assert pos == _eval_exp_with_ctx(poslenMap["position"], ma) # this assert is expensive... try:
length = poslenMap["length"] field_count = int(field_count_str)
length = _eval_exp_with_ctx(length, ma) except ValueError:
ma[fieldname] = data[pos:pos+length] field_count = vars_dict[field_count_str]
pos += length if isinstance(field_count, (bytes, bytearray)):
# BOLT-01: "MUST ignore any additional data within a message beyond the length that it expects for that type." field_count = int.from_bytes(field_count, byteorder="big")
assert pos <= len(data), (msg_name, pos, len(data)) assert isinstance(field_count, int)
return msg_name, ma return field_count
return handler
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: class LNSerializer:
def __init__(self):
message_types = {} def __init__(self, *, for_onion_wire: bool = False):
path = os.path.join(os.path.dirname(__file__), 'lightning.json') # TODO msg_type could be 'int' everywhere...
with open(path) as f: self.msg_scheme_from_type = {} # type: Dict[bytes, List[Sequence[str]]]
structured = json.loads(f.read(), object_pairs_hook=OrderedDict) self.msg_type_from_name = {} # type: Dict[str, bytes]
for msg_name in structured: self.in_tlv_stream_get_tlv_record_scheme_from_type = {} # type: Dict[str, Dict[int, List[Sequence[str]]]]
v = structured[msg_name] self.in_tlv_stream_get_record_type_from_name = {} # type: Dict[str, Dict[str, int]]
# these message types are skipped since their types collide self.in_tlv_stream_get_record_name_from_type = {} # type: Dict[str, Dict[int, str]]
# (for example with pong, which also uses type=19)
# we don't need them yet if for_onion_wire:
if msg_name in ["final_incorrect_cltv_expiry", "final_incorrect_htlc_amount"]: path = os.path.join(os.path.dirname(__file__), "lnwire", "onion_wire.csv")
continue else:
if len(v["payload"]) == 0: 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 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: try:
num = int(v["type"]) scheme = scheme_map[tlv_record_type]
except ValueError: except KeyError:
#print("skipping", k) if tlv_record_type % 2 == 0:
continue # unknown "even" type: hard fail
byts = num.to_bytes(2, 'big') raise UnknownMandatoryTLVRecordType(f"{tlv_stream_name}/{tlv_record_type}") from None
assert byts not in message_types, (byts, message_types[byts].__name__, msg_name) else:
names = [x.__name__ for x in message_types.values()] # unknown "odd" type: skip it
assert msg_name + "_handler" not in names, (msg_name, names) continue
message_types[byts] = _make_handler(msg_name, v) tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type]
message_types[byts].__name__ = msg_name + "_handler" parsed[tlv_record_name] = {}
with io.BytesIO(tlv_record_val) as tlv_record_fd:
assert message_types[b"\x00\x10"].__name__ == "init_handler" for row in scheme:
self.structured = structured #print(f"row: {row!r}")
self.message_types = message_types if row[0] == "tlvtype":
pass
def encode_msg(self, msg_type : str, **kwargs) -> bytes: 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) Encode kwargs into a Lightning message (bytes)
of the type given in the msg_type string of the type given in the msg_type string
""" """
typ = self.structured[msg_type] #print(f">>> encode_msg. msg_type={msg_type}, payload={kwargs!r}")
data = int(typ["type"]).to_bytes(2, 'big') msg_type_bytes = self.msg_type_from_name[msg_type]
lengths = {} scheme = self.msg_scheme_from_type[msg_type_bytes]
for k in typ["payload"]: with io.BytesIO() as fd:
poslenMap = typ["payload"][k] fd.write(msg_type_bytes)
if k not in kwargs and "feature" in poslenMap: for row in scheme:
continue if row[0] == "msgtype":
param = kwargs.get(k, 0) pass
leng = _eval_exp_with_ctx(poslenMap["length"], lengths) elif row[0] == "msgdata":
try: # msgdata,<msgname>,<fieldname>,<typename>,[<count>][,<option>]
clone = dict(lengths) field_name = row[2]
clone.update(kwargs) field_type = row[3]
leng = _eval_exp_with_ctx(poslenMap["length"], clone) field_count_str = row[4]
except KeyError: #print(f">>> encode_msg. msgdata. field_name={field_name!r}. field_type={field_type!r}. field_count_str={field_count_str!r}")
pass field_count = _resolve_field_count(field_count_str, vars_dict=kwargs)
try: if field_name == "tlvs":
if not isinstance(param, bytes): tlv_stream_name = field_type
assert isinstance(param, int), "field {} is neither bytes or int".format(k) if tlv_stream_name in kwargs:
param = param.to_bytes(leng, 'big') self.write_tlv_stream(fd=fd, tlv_stream_name=tlv_stream_name, **(kwargs[tlv_stream_name]))
except ValueError: continue
raise Exception("{} does not fit in {} bytes".format(k, leng)) try:
lengths[k] = len(param) field_value = kwargs[field_name]
if lengths[k] != leng: except KeyError:
raise Exception("field {} is {} bytes long, should be {} bytes long".format(k, lengths[k], leng)) if len(row) > 5:
data += param break # optional feature field not present
return data else:
field_value = 0 # default mandatory fields to zero
def decode_msg(self, data : bytes) -> Tuple[str, dict]: #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 Decode Lightning message by reading the first
two bytes to determine message type. two bytes to determine message type.
Returns message type string and parsed message contents dict Returns message type string and parsed message contents dict
""" """
typ = data[:2] #print(f"decode_msg >>> {data.hex()}")
k, parsed = self.message_types[typ](data[2:]) assert len(data) >= 2
return k, parsed 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() _inst = LNSerializer()
encode_msg = _inst.encode_msg encode_msg = _inst.encode_msg
decode_msg = _inst.decode_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 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
import io
import hashlib import hashlib
from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING
from enum import IntEnum, IntFlag from enum import IntEnum, IntFlag
@ -31,15 +32,16 @@ from . import ecc
from .crypto import sha256, hmac_oneshot, chacha20_encrypt from .crypto import sha256, hmac_oneshot, chacha20_encrypt
from .util import bh2u, profiler, xor_bytes, bfh from .util import bh2u, profiler, xor_bytes, bfh
from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH, 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: if TYPE_CHECKING:
from .lnrouter import LNPaymentRoute from .lnrouter import LNPaymentRoute
HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04 HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04
PER_HOP_FULL_SIZE = 65 # HOPS_DATA_SIZE / 20 LEGACY_PER_HOP_FULL_SIZE = 65
NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE NUM_STREAM_BYTES = 2 * HOPS_DATA_SIZE
PER_HOP_HMAC_SIZE = 32 PER_HOP_HMAC_SIZE = 32
@ -48,64 +50,127 @@ class InvalidOnionMac(Exception): pass
class InvalidOnionPubkey(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.short_channel_id = ShortChannelID(short_channel_id)
self.amt_to_forward = amt_to_forward self.amt_to_forward = amt_to_forward
self.outgoing_cltv_value = outgoing_cltv_value self.outgoing_cltv_value = outgoing_cltv_value
def to_bytes(self) -> bytes: def to_bytes(self) -> bytes:
ret = self.short_channel_id ret = self.short_channel_id
ret += self.amt_to_forward ret += int.to_bytes(self.amt_to_forward, length=8, byteorder="big", signed=False)
ret += self.outgoing_cltv_value ret += int.to_bytes(self.outgoing_cltv_value, length=4, byteorder="big", signed=False)
ret += bytes(12) # padding ret += bytes(12) # padding
if len(ret) != 32: if len(ret) != 32:
raise Exception('unexpected length {}'.format(len(ret))) raise Exception('unexpected length {}'.format(len(ret)))
return 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 @classmethod
def from_bytes(cls, b: bytes): def from_bytes(cls, b: bytes) -> 'LegacyHopDataPayload':
if len(b) != 32: if len(b) != 32:
raise Exception('unexpected length {}'.format(len(b))) raise Exception('unexpected length {}'.format(len(b)))
return OnionPerHop( return LegacyHopDataPayload(
short_channel_id=b[:8], short_channel_id=b[:8],
amt_to_forward=b[8:16], amt_to_forward=int.from_bytes(b[8:16], byteorder="big", signed=False),
outgoing_cltv_value=b[16:20] 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 class OnionHopsDataSingle: # called HopData in lnd
def __init__(self, per_hop: OnionPerHop = None): def __init__(self, *, is_tlv_payload: bool, payload: dict = None):
self.realm = 0 self.is_tlv_payload = is_tlv_payload
self.per_hop = per_hop if payload is None:
payload = {}
self.payload = payload
self.hmac = None self.hmac = None
self._raw_bytes_payload = None # used in unit tests
def to_bytes(self) -> bytes: def to_bytes(self) -> bytes:
ret = bytes([self.realm]) hmac_ = self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE)
ret += self.per_hop.to_bytes() if self._raw_bytes_payload is not None:
ret += self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE) ret = write_bigsize_int(len(self._raw_bytes_payload))
if len(ret) != PER_HOP_FULL_SIZE: ret += self._raw_bytes_payload
raise Exception('unexpected length {}'.format(len(ret))) ret += hmac_
return ret 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 @classmethod
def from_bytes(cls, b: bytes): def from_fd(cls, fd: io.BytesIO) -> 'OnionHopsDataSingle':
if len(b) != PER_HOP_FULL_SIZE: first_byte = fd.read(1)
raise Exception('unexpected length {}'.format(len(b))) if len(first_byte) == 0:
ret = OnionHopsDataSingle() raise Exception(f"unexpected EOF")
ret.realm = b[0] fd.seek(-1, io.SEEK_CUR) # undo read
if ret.realm != 0: if first_byte == b'\x00':
raise Exception('only realm 0 is supported') # legacy hop data format
ret.per_hop = OnionPerHop.from_bytes(b[1:33]) b = fd.read(LEGACY_PER_HOP_FULL_SIZE)
ret.hmac = b[33:] if len(b) != LEGACY_PER_HOP_FULL_SIZE:
return ret 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: class OnionPacket:
def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes): 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.version = 0
self.public_key = public_key self.public_key = public_key
self.hops_data = hops_data # also called RoutingInfo in bolt-04 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, def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket: hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket:
num_hops = len(payment_path_pubkeys) 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) 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) next_hmac = bytes(PER_HOP_HMAC_SIZE)
# Our starting packet needs to be filled out with random bytes, we # 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) pad_key = get_bolt04_onion_key(b'pad', session_key)
mix_header = generate_cipher_stream(pad_key, HOPS_DATA_SIZE) 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]) rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])
mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i]) mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i])
hops_data[i].hmac = next_hmac hops_data[i].hmac = next_hmac
stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) stream_bytes = generate_cipher_stream(rho_key, HOPS_DATA_SIZE)
mix_header = mix_header[:-PER_HOP_FULL_SIZE] hop_data_bytes = hops_data[i].to_bytes()
mix_header = hops_data[i].to_bytes() + mix_header mix_header = mix_header[:-len(hop_data_bytes)]
mix_header = hop_data_bytes + mix_header
mix_header = xor_bytes(mix_header, stream_bytes) mix_header = xor_bytes(mix_header, stream_bytes)
if i == num_hops - 1 and len(filler) != 0: if i == num_hops - 1 and len(filler) != 0:
mix_header = mix_header[:-len(filler)] + filler 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) 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]: -> Tuple[List[OnionHopsDataSingle], int, int]:
"""Returns the hops_data to be used for constructing an onion packet, """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. 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: if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
raise PaymentFailure(f"too long route ({len(route)} edges)") raise PaymentFailure(f"too long route ({len(route)} edges)")
# payload that will be seen by the last hop:
amt = amount_msat amt = amount_msat
cltv = final_cltv cltv = final_cltv
hops_data = [OnionHopsDataSingle(OnionPerHop(b"\x00" * 8, hop_payload = {
amt.to_bytes(8, "big"), "amt_to_forward": {"amt_to_forward": amt},
cltv.to_bytes(4, "big")))] "outgoing_cltv_value": {"outgoing_cltv_value": cltv},
for route_edge in reversed(route[1:]): }
hops_data += [OnionHopsDataSingle(OnionPerHop(route_edge.short_channel_id, if payment_secret is not None:
amt.to_bytes(8, "big"), hop_payload["payment_data"] = {"payment_secret": payment_secret, "total_msat": amt}
cltv.to_bytes(4, "big")))] 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) amt += route_edge.fee_for_edge(amt)
cltv += route_edge.cltv_expiry_delta cltv += route_edge.cltv_expiry_delta
hops_data.reverse() hops_data.reverse()
return hops_data, amt, cltv return hops_data, amt, cltv
def generate_filler(key_type: bytes, num_hops: int, hop_size: int, def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],
shared_secrets: Sequence[bytes]) -> bytes: shared_secrets: Sequence[bytes]) -> bytes:
filler_size = (NUM_MAX_HOPS_IN_PAYMENT_PATH + 1) * hop_size 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) filler = bytearray(filler_size)
for i in range(0, num_hops-1): # -1, as last hop does not obfuscate for i in range(0, num_hops-1): # -1, as last hop does not obfuscate
filler = filler[hop_size:] # Sum up how many frames were used by prior hops.
filler += bytearray(hop_size) 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_key = get_bolt04_onion_key(key_type, shared_secrets[i])
stream_bytes = generate_cipher_stream(stream_key, filler_size) stream_bytes = generate_cipher_stream(stream_key, NUM_STREAM_BYTES)
filler = xor_bytes(filler, 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: 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 # peel an onion layer off
rho_key = get_bolt04_onion_key(b'rho', shared_secret) rho_key = get_bolt04_onion_key(b'rho', shared_secret)
stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) 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 = xor_bytes(padded_header, stream_bytes)
next_hops_data_fd = io.BytesIO(next_hops_data)
# calc next ephemeral key # calc next ephemeral key
blinding_factor = sha256(onion_packet.public_key + shared_secret) 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_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int
next_public_key = next_public_key_int.get_public_key_bytes() 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( next_onion_packet = OnionPacket(
public_key=next_public_key, 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 hmac=hop_data.hmac
) )
if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE): 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) return OnionRoutingFailureMessage(failure_code, failure_data)
class OnionFailureCodeMetaFlag(IntFlag): # TODO maybe we should rm this and just use OnionWireSerializer and onion_wire.csv
BADONION = 0x8000
PERM = 0x4000
NODE = 0x2000
UPDATE = 0x1000
BADONION = OnionFailureCodeMetaFlag.BADONION BADONION = OnionFailureCodeMetaFlag.BADONION
PERM = OnionFailureCodeMetaFlag.PERM PERM = OnionFailureCodeMetaFlag.PERM
NODE = OnionFailureCodeMetaFlag.NODE NODE = OnionFailureCodeMetaFlag.NODE
@ -398,6 +487,7 @@ class OnionFailureCode(IntEnum):
FINAL_INCORRECT_HTLC_AMOUNT = 19 FINAL_INCORRECT_HTLC_AMOUNT = 19
CHANNEL_DISABLED = UPDATE | 20 CHANNEL_DISABLED = UPDATE | 20
EXPIRY_TOO_FAR = 21 EXPIRY_TOO_FAR = 21
INVALID_ONION_PAYLOAD = PERM | 22
# don't use these elsewhere, the names are ambiguous without context # 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, from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore, RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
funding_output_script, get_per_commitment_secret_from_seed, 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, LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily,
ln_compare_features, privkey_to_pubkey, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_ACCEPTED, ln_compare_features, privkey_to_pubkey, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_ACCEPTED,
LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate, LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate,
MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED, MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED,
MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY, MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY,
NBLOCK_OUR_CLTV_EXPIRY_DELTA, format_short_channel_id, ShortChannelID, NBLOCK_OUR_CLTV_EXPIRY_DELTA, format_short_channel_id, ShortChannelID,
IncompatibleLightningFeatures) IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage)
from .lnutil import FeeUpdate from .lnutil import FeeUpdate
from .lntransport import LNTransport, LNTransportBase from .lntransport import LNTransport, LNTransportBase
from .lnmsg import encode_msg, decode_msg from .lnmsg import encode_msg, decode_msg
@ -77,7 +77,7 @@ class Peer(Logger):
self.pubkey = pubkey # remote pubkey self.pubkey = pubkey # remote pubkey
self.lnworker = lnworker self.lnworker = lnworker
self.privkey = lnworker.node_keypair.privkey # local privkey 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.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]
self.network = lnworker.network self.network = lnworker.network
self.channel_db = lnworker.network.channel_db self.channel_db = lnworker.network.channel_db
@ -131,7 +131,12 @@ class Peer(Logger):
async def initialize(self): async def initialize(self):
if isinstance(self.transport, LNTransport): if isinstance(self.transport, LNTransport):
await self.transport.handshake() 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._sent_init = True
self.maybe_set_initialized() self.maybe_set_initialized()
@ -180,7 +185,7 @@ class Peer(Logger):
self.ordered_message_queues[chan_id].put_nowait((None, {'error':payload['data']})) self.ordered_message_queues[chan_id].put_nowait((None, {'error':payload['data']}))
def on_ping(self, payload): 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) self.send_message('pong', byteslen=l)
def on_pong(self, payload): def on_pong(self, payload):
@ -199,14 +204,25 @@ class Peer(Logger):
if self._received_init: if self._received_init:
self.logger.info("ALREADY INITIALIZED BUT RECEIVED INIT") self.logger.info("ALREADY INITIALIZED BUT RECEIVED INIT")
return return
# if they required some even flag we don't have, they will close themselves their_features = LnFeatures(int.from_bytes(payload['features'], byteorder="big"))
# but if we require an even flag they don't have, we close their_globalfeatures = int.from_bytes(payload['globalfeatures'], byteorder="big")
their_localfeatures = int.from_bytes(payload['localfeatures'], 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: try:
self.localfeatures = ln_compare_features(self.localfeatures, their_localfeatures) self.features = ln_compare_features(self.features, their_features)
except IncompatibleLightningFeatures as e: except IncompatibleLightningFeatures as e:
self.initialized.set_exception(e) self.initialized.set_exception(e)
raise GracefulDisconnect(f"{str(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): if isinstance(self.transport, LNTransport):
self.channel_db.add_recent_peer(self.transport.peer_addr) self.channel_db.add_recent_peer(self.transport.peer_addr)
for chan in self.channels.values(): for chan in self.channels.values():
@ -417,8 +433,8 @@ class Peer(Logger):
return ids return ids
def on_reply_channel_range(self, payload): def on_reply_channel_range(self, payload):
first = int.from_bytes(payload['first_blocknum'], 'big') first = payload['first_blocknum']
num = int.from_bytes(payload['number_of_blocks'], 'big') num = payload['number_of_blocks']
complete = bool(int.from_bytes(payload['complete'], 'big')) complete = bool(int.from_bytes(payload['complete'], 'big'))
encoded = payload['encoded_short_ids'] encoded = payload['encoded_short_ids']
ids = self.decode_short_ids(encoded) ids = self.decode_short_ids(encoded)
@ -465,7 +481,7 @@ class Peer(Logger):
self.lnworker.peer_closed(self) self.lnworker.peer_closed(self)
def is_static_remotekey(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: def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> LocalConfig:
# key derivation # key derivation
@ -541,27 +557,27 @@ class Peer(Logger):
) )
payload = await self.wait_for_message('accept_channel', temp_channel_id) payload = await self.wait_for_message('accept_channel', temp_channel_id)
remote_per_commitment_point = payload['first_per_commitment_point'] 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: if funding_txn_minimum_depth <= 0:
raise Exception(f"minimum depth too low, {funding_txn_minimum_depth}") raise Exception(f"minimum depth too low, {funding_txn_minimum_depth}")
if funding_txn_minimum_depth > 30: if funding_txn_minimum_depth > 30:
raise Exception(f"minimum depth too high, {funding_txn_minimum_depth}") 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) 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: 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.") 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: if htlc_min > MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED:
raise Exception(f"Remote Lightning peer reports htlc_minimum_msat={htlc_min} mSAT," + 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).") 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: 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" + 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).") 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: if max_accepted_htlcs > 483:
raise Exception("Remote Lightning peer reports max_accepted_htlcs > 483, which is a BOLT-02 protocol violation.") 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: 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}," + 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})") f" which is above Electrums required maximum ({MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED})")
@ -647,9 +663,9 @@ class Peer(Logger):
# payload['channel_flags'] # payload['channel_flags']
if payload['chain_hash'] != constants.net.rev_genesis_bytes(): if payload['chain_hash'] != constants.net.rev_genesis_bytes():
raise Exception('wrong chain_hash') raise Exception('wrong chain_hash')
funding_sat = int.from_bytes(payload['funding_satoshis'], 'big') funding_sat = payload['funding_satoshis']
push_msat = int.from_bytes(payload['push_msat'], 'big') push_msat = payload['push_msat']
feerate = int.from_bytes(payload['feerate_per_kw'], 'big') feerate = payload['feerate_per_kw']
temp_chan_id = payload['temporary_channel_id'] temp_chan_id = payload['temporary_channel_id']
local_config = self.make_local_config(funding_sat, push_msat, REMOTE) local_config = self.make_local_config(funding_sat, push_msat, REMOTE)
# for the first commitment transaction # for the first commitment transaction
@ -674,11 +690,11 @@ class Peer(Logger):
first_per_commitment_point=per_commitment_point_first, first_per_commitment_point=per_commitment_point_first,
) )
funding_created = await self.wait_for_message('funding_created', temp_chan_id) 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]) funding_txid = bh2u(funding_created['funding_txid'][::-1])
channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx) channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx)
remote_balance_sat = funding_sat * 1000 - push_msat 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_reserve_sat = self.validate_remote_reserve(payload['channel_reserve_satoshis'], remote_dust_limit_sat, funding_sat)
remote_config = RemoteConfig( remote_config = RemoteConfig(
payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']), payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
@ -686,13 +702,13 @@ class Peer(Logger):
htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']), htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']),
delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']), delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']),
revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_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, 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_htlc_value_in_flight_msat=payload['max_htlc_value_in_flight_msat'], # TODO validate
max_accepted_htlcs=int.from_bytes(payload['max_accepted_htlcs'], 'big'), # TODO validate max_accepted_htlcs=payload['max_accepted_htlcs'], # TODO validate
initial_msat=remote_balance_sat, initial_msat=remote_balance_sat,
reserve_sat = remote_reserve_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'], next_per_commitment_point=payload['first_per_commitment_point'],
current_per_commitment_point=None, current_per_commitment_point=None,
) )
@ -718,8 +734,7 @@ class Peer(Logger):
chan.set_state(channel_states.OPENING) chan.set_state(channel_states.OPENING)
self.lnworker.add_new_channel(chan) self.lnworker.add_new_channel(chan)
def validate_remote_reserve(self, payload_field: bytes, dust_limit: int, funding_sat: int) -> int: def validate_remote_reserve(self, remote_reserve_sat: int, dust_limit: int, funding_sat: int) -> int:
remote_reserve_sat = int.from_bytes(payload_field, 'big')
if remote_reserve_sat < dust_limit: if remote_reserve_sat < dust_limit:
raise Exception('protocol violation: reserve < dust_limit') raise Exception('protocol violation: reserve < dust_limit')
if remote_reserve_sat > funding_sat/100: if remote_reserve_sat > funding_sat/100:
@ -745,7 +760,7 @@ class Peer(Logger):
oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE) oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)
latest_remote_ctn = chan.get_latest_ctn(REMOTE) latest_remote_ctn = chan.get_latest_ctn(REMOTE)
next_remote_ctn = chan.get_next_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 # send message
srk_enabled = chan.is_static_remotekey_enabled() srk_enabled = chan.is_static_remotekey_enabled()
if srk_enabled: if srk_enabled:
@ -760,16 +775,16 @@ class Peer(Logger):
self.send_message( self.send_message(
"channel_reestablish", "channel_reestablish",
channel_id=chan_id, channel_id=chan_id,
next_local_commitment_number=next_local_ctn, next_commitment_number=next_local_ctn,
next_remote_revocation_number=oldest_unrevoked_remote_ctn, next_revocation_number=oldest_unrevoked_remote_ctn,
your_last_per_commitment_secret=last_rev_secret, your_last_per_commitment_secret=last_rev_secret,
my_current_per_commitment_point=latest_point) my_current_per_commitment_point=latest_point)
self.logger.info(f'channel_reestablish ({chan.get_id_for_log()}): sent channel_reestablish with ' self.logger.info(f'channel_reestablish ({chan.get_id_for_log()}): sent channel_reestablish with '
f'(next_local_ctn={next_local_ctn}, ' f'(next_local_ctn={next_local_ctn}, '
f'oldest_unrevoked_remote_ctn={oldest_unrevoked_remote_ctn})') f'oldest_unrevoked_remote_ctn={oldest_unrevoked_remote_ctn})')
msg = await self.wait_for_message('channel_reestablish', chan_id) msg = await self.wait_for_message('channel_reestablish', chan_id)
their_next_local_ctn = int.from_bytes(msg["next_local_commitment_number"], 'big') their_next_local_ctn = msg["next_commitment_number"]
their_oldest_unrevoked_remote_ctn = int.from_bytes(msg["next_remote_revocation_number"], 'big') their_oldest_unrevoked_remote_ctn = msg["next_revocation_number"]
their_local_pcp = msg.get("my_current_per_commitment_point") 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") 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 ' 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 != their_oldest_unrevoked_remote_ctn:
if oldest_unrevoked_local_ctn - 1 == their_oldest_unrevoked_remote_ctn: if oldest_unrevoked_local_ctn - 1 == their_oldest_unrevoked_remote_ctn:
# A node: # 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: # the receiving node sent, AND the receiving node hasn't already received a closing_signed:
# MUST re-send the revoke_and_ack. # MUST re-send the revoke_and_ack.
last_secret, last_point = chan.get_secret_and_point(LOCAL, oldest_unrevoked_local_ctn - 1) 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 return msg_hash, node_signature, bitcoin_signature
def on_update_fail_htlc(self, chan: Channel, payload): def on_update_fail_htlc(self, chan: Channel, payload):
htlc_id = int.from_bytes(payload["id"], "big") htlc_id = payload["id"]
reason = payload["reason"] reason = payload["reason"]
self.logger.info(f"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}") 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) 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() 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)) 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, def pay(self, *, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
payment_hash: bytes, min_final_cltv_expiry: int) -> UpdateAddHtlc: payment_hash: bytes, min_final_cltv_expiry: int, payment_secret: bytes = None) -> UpdateAddHtlc:
assert amount_msat > 0, "amount_msat is not greater zero" assert amount_msat > 0, "amount_msat is not greater zero"
assert len(route) > 0
if not chan.can_send_update_add_htlc(): if not chan.can_send_update_add_htlc():
raise PaymentFailure("Channel cannot 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() local_height = self.network.get_local_height()
# create onion packet # create onion packet
final_cltv = local_height + min_final_cltv_expiry 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) assert final_cltv <= cltv, (final_cltv, cltv)
secret_key = os.urandom(32) secret_key = os.urandom(32)
onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash) 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 = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time()))
htlc = chan.add_htlc(htlc) htlc = chan.add_htlc(htlc)
chan.set_onion_key(htlc.htlc_id, secret_key) 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( self.send_message(
"update_add_htlc", "update_add_htlc",
channel_id=chan.channel_id, channel_id=chan.channel_id,
@ -1083,7 +1103,7 @@ class Peer(Logger):
def on_update_fulfill_htlc(self, chan: Channel, payload): def on_update_fulfill_htlc(self, chan: Channel, payload):
preimage = payload["payment_preimage"] preimage = payload["payment_preimage"]
payment_hash = sha256(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}") 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) 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) self.lnworker.save_preimage(payment_hash, preimage)
@ -1103,10 +1123,10 @@ class Peer(Logger):
def on_update_add_htlc(self, chan: Channel, payload): def on_update_add_htlc(self, chan: Channel, payload):
payment_hash = payload["payment_hash"] 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}") 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') cltv_expiry = payload["cltv_expiry"]
amount_msat_htlc = int.from_bytes(payload["amount_msat"], 'big') amount_msat_htlc = payload["amount_msat"]
onion_packet = payload["onion_routing_packet"] onion_packet = payload["onion_routing_packet"]
if chan.get_state() != channel_states.OPEN: if chan.get_state() != channel_states.OPEN:
raise RemoteMisbehaving(f"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()}") 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: if not forwarding_enabled:
self.logger.info(f"forwarding is disabled. failing htlc.") self.logger.info(f"forwarding is disabled. failing htlc.")
return OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') return OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
dph = processed_onion.hop_data.per_hop try:
next_chan = self.lnworker.get_channel_by_short_id(dph.short_channel_id) next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"]
next_chan_scid = dph.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() local_height = self.network.get_local_height()
if next_chan is None: if next_chan is None:
self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}") 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}") f"chan state {next_chan.get_state()}, peer state: {next_chan.peer_state}")
data = outgoing_chan_upd_len + outgoing_chan_upd data = outgoing_chan_upd_len + outgoing_chan_upd
return OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data) 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: 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 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) 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) 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: 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'') 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( forwarding_fees = fee_for_edge_msat(
forwarded_amount_msat=next_amount_msat_htlc, forwarded_amount_msat=next_amount_msat_htlc,
fee_base_msat=lnutil.OUR_FEE_BASE_MSAT, fee_base_msat=lnutil.OUR_FEE_BASE_MSAT,
@ -1175,8 +1203,8 @@ class Peer(Logger):
"update_add_htlc", "update_add_htlc",
channel_id=next_chan.channel_id, channel_id=next_chan.channel_id,
id=next_htlc.htlc_id, id=next_htlc.htlc_id,
cltv_expiry=dph.outgoing_cltv_value, cltv_expiry=next_cltv_expiry,
amount_msat=dph.amt_to_forward, amount_msat=next_amount_msat_htlc,
payment_hash=next_htlc.payment_hash, payment_hash=next_htlc.payment_hash,
onion_routing_packet=processed_onion.next_packet.to_bytes() onion_routing_packet=processed_onion.next_packet.to_bytes()
) )
@ -1194,6 +1222,14 @@ class Peer(Logger):
except UnknownPaymentHash: except UnknownPaymentHash:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
return False, reason 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 expected_received_msat = int(info.amount * 1000) if info.amount is not None else None
if expected_received_msat is not None and \ if expected_received_msat is not None and \
not (expected_received_msat <= htlc.amount_msat <= 2 * expected_received_msat): 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: if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'') reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'')
return False, reason 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: if cltv_from_onion != htlc.cltv_expiry:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY, reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
data=htlc.cltv_expiry.to_bytes(4, byteorder="big")) data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
return False, reason 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: if amount_from_onion > htlc.amount_msat:
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT, reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
data=htlc.amount_msat.to_bytes(8, byteorder="big")) data=htlc.amount_msat.to_bytes(8, byteorder="big"))
@ -1258,7 +1306,7 @@ class Peer(Logger):
self.maybe_send_commitment(chan) self.maybe_send_commitment(chan)
def on_update_fee(self, chan: Channel, payload): 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) chan.update_fee(feerate, False)
async def maybe_update_fee(self, chan: Channel): async def maybe_update_fee(self, chan: Channel):
@ -1378,7 +1426,7 @@ class Peer(Logger):
while True: while True:
# FIXME: the remote SHOULD send closing_signed, but some don't. # FIXME: the remote SHOULD send closing_signed, but some don't.
cs_payload = await self.wait_for_message('closing_signed', chan.channel_id) 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: 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}') 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'] 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)) error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_KEY, data=sha256(onion_packet_bytes))
except InvalidOnionMac: except InvalidOnionMac:
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_HMAC, data=sha256(onion_packet_bytes)) 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: else:
if processed_onion.are_we_final: if processed_onion.are_we_final:
preimage, error_reason = self.maybe_fulfill_htlc( preimage, error_reason = self.maybe_fulfill_htlc(

48
electrum/lnrouter.py

@ -27,11 +27,13 @@ import queue
from collections import defaultdict from collections import defaultdict
from typing import Sequence, List, Tuple, Optional, Dict, NamedTuple, TYPE_CHECKING, Set from typing import Sequence, List, Tuple, Optional, Dict, NamedTuple, TYPE_CHECKING, Set
import attr
from .util import bh2u, profiler from .util import bh2u, profiler
from .logging import Logger from .logging import Logger
from .lnutil import NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures,
from .channel_db import ChannelDB, Policy NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE)
from .lnutil import NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE from .channel_db import ChannelDB, Policy, NodeInfo
if TYPE_CHECKING: if TYPE_CHECKING:
from .lnchannel import Channel 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) + (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""" """if you travel through short_channel_id, you will reach node_id"""
node_id: bytes node_id = attr.ib(type=bytes, kw_only=True)
short_channel_id: ShortChannelID short_channel_id = attr.ib(type=ShortChannelID, kw_only=True)
fee_base_msat: int fee_base_msat = attr.ib(type=int, kw_only=True)
fee_proportional_millionths: int fee_proportional_millionths = attr.ib(type=int, kw_only=True)
cltv_expiry_delta: int 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: def fee_for_edge(self, amount_msat: int) -> int:
return fee_for_edge_msat(forwarded_amount_msat=amount_msat, return fee_for_edge_msat(forwarded_amount_msat=amount_msat,
@ -63,14 +67,16 @@ class RouteEdge(NamedTuple):
@classmethod @classmethod
def from_channel_policy(cls, channel_policy: 'Policy', 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 isinstance(short_channel_id, bytes)
assert type(end_node) is bytes assert type(end_node) is bytes
return RouteEdge(end_node, return RouteEdge(node_id=end_node,
ShortChannelID.normalize(short_channel_id), short_channel_id=ShortChannelID.normalize(short_channel_id),
channel_policy.fee_base_msat, fee_base_msat=channel_policy.fee_base_msat,
channel_policy.fee_proportional_millionths, fee_proportional_millionths=channel_policy.fee_proportional_millionths,
channel_policy.cltv_expiry_delta) 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: def is_sane_to_use(self, amount_msat: int) -> bool:
# TODO revise ad-hoc heuristics # TODO revise ad-hoc heuristics
@ -82,6 +88,10 @@ class RouteEdge(NamedTuple):
return False return False
return True 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] LNPaymentRoute = Sequence[RouteEdge]
@ -154,7 +164,9 @@ class LNPathFinder(Logger):
if channel_policy.htlc_maximum_msat is not None and \ if channel_policy.htlc_maximum_msat is not None and \
payment_amt_msat > channel_policy.htlc_maximum_msat: payment_amt_msat > channel_policy.htlc_maximum_msat:
return float('inf'), 0 # payment amount too large 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): if not route_edge.is_sane_to_use(payment_amt_msat):
return float('inf'), 0 # thanks but no thanks return float('inf'), 0 # thanks but no thanks
@ -268,6 +280,8 @@ class LNPathFinder(Logger):
my_channels=my_channels) my_channels=my_channels)
if channel_policy is None: if channel_policy is None:
raise NoChannelPolicy(short_channel_id) 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 prev_node_id = node_id
return route return route

189
electrum/lnutil.py

@ -3,12 +3,13 @@
# file LICENCE or http://www.opensource.org/licenses/mit-license.php # file LICENCE or http://www.opensource.org/licenses/mit-license.php
from enum import IntFlag, IntEnum from enum import IntFlag, IntEnum
import enum
import json 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 from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
import re import re
import attr
import attr
from aiorpcx import NetAddress from aiorpcx import NetAddress
from .util import bfh, bh2u, inv_dict, UserFacingException 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()) 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_REQ = 1 << 0
OPTION_DATA_LOSS_PROTECT_OPT = 1 << 1 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 INITIAL_ROUTING_SYNC = 1 << 3
_ln_feature_contexts[INITIAL_ROUTING_SYNC] = LNFC.INIT
OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ = 1 << 4 OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ = 1 << 4
OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT = 1 << 5 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_REQ = 1 << 6
GOSSIP_QUERIES_OPT = 1 << 7 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_REQ = 1 << 12
OPTION_STATIC_REMOTEKEY_OPT = 1 << 13 OPTION_STATIC_REMOTEKEY_OPT = 1 << 13
_ln_feature_contexts[OPTION_STATIC_REMOTEKEY_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
# note that these are powers of two, not the bits themselves _ln_feature_contexts[OPTION_STATIC_REMOTEKEY_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
LN_LOCAL_FEATURES_KNOWN_SET = set(LnLocalFeatures)
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: 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 return flag_bit - 1
class LnGlobalFeatures(IntFlag):
pass
# note that these are powers of two, not the bits themselves class IncompatibleOrInsaneFeatures(Exception): pass
LN_GLOBAL_FEATURES_KNOWN_SET = set(LnGlobalFeatures) class UnknownEvenFeatureBits(IncompatibleOrInsaneFeatures): pass
class IncompatibleLightningFeatures(IncompatibleOrInsaneFeatures): pass
class IncompatibleLightningFeatures(ValueError): pass
def ln_compare_features(our_features, their_features) -> int: def ln_compare_features(our_features: 'LnFeatures', their_features: int) -> 'LnFeatures':
"""raises IncompatibleLightningFeatures if incompatible""" """Returns negotiated features.
Raises IncompatibleLightningFeatures if incompatible.
"""
our_flags = set(list_enabled_bits(our_features)) our_flags = set(list_enabled_bits(our_features))
their_flags = set(list_enabled_bits(their_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: for flag in our_flags:
if flag not in their_flags and get_ln_flag_pair_of_bit(flag) not in their_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 :( # they don't have this feature we wanted :(
if flag % 2 == 0: # even flags are compulsory 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 our_features ^= 1 << flag # disable flag
else: else:
# They too have this flag. # They too have this flag.
@ -759,9 +879,42 @@ def ln_compare_features(our_features, their_features) -> int:
# set the corresponding odd flag now. # set the corresponding odd flag now.
if flag % 2 == 0 and our_features & (1 << flag): if flag % 2 == 0 and our_features & (1 << flag):
our_features |= 1 << get_ln_flag_pair_of_bit(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 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: class LNPeerAddr:
def __init__(self, host: str, port: int, pubkey: bytes): def __init__(self, host: str, port: int, pubkey: bytes):
@ -955,3 +1108,11 @@ class UpdateAddHtlc:
def to_tuple(self): def to_tuple(self):
return (self.amount_msat, self.payment_hash, self.cltv_expiry, self.htlc_id, self.timestamp) 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, generate_keypair, LnKeyFamily, LOCAL, REMOTE,
UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE,
NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner, NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
UpdateAddHtlc, Direction, LnLocalFeatures, UpdateAddHtlc, Direction, LnFeatures,
ShortChannelID, PaymentAttemptLog, PaymentAttemptFailureDetails, ShortChannelID, PaymentAttemptLog, PaymentAttemptFailureDetails,
BarePaymentAttemptLog) BarePaymentAttemptLog, derive_payment_secret_from_payment_preimage)
from .lnutil import ln_dummy_address, ln_compare_features from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures
from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket
from .lnmsg import decode_msg from .lnmsg import decode_msg
@ -147,9 +147,11 @@ class LNWorker(Logger):
self.taskgroup = SilentTaskGroup() self.taskgroup = SilentTaskGroup()
# set some feature flags as baseline for both LNWallet and LNGossip # 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 # note that e.g. DATA_LOSS_PROTECT is needed for LNGossip as many peers require it
self.localfeatures = LnLocalFeatures(0) self.features = LnFeatures(0)
self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
self.localfeatures |= LnLocalFeatures.OPTION_STATIC_REMOTEKEY_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): def channels_for_peer(self, node_id):
return {} return {}
@ -248,8 +250,8 @@ class LNWorker(Logger):
if not node: if not node:
return False return False
try: try:
ln_compare_features(self.localfeatures, node.features) ln_compare_features(self.features, node.features)
except ValueError: except IncompatibleLightningFeatures:
return False return False
#self.logger.info(f'is_good {peer.host}') #self.logger.info(f'is_good {peer.host}')
return True return True
@ -366,8 +368,8 @@ class LNGossip(LNWorker):
node = BIP32Node.from_rootseed(seed, xtype='standard') node = BIP32Node.from_rootseed(seed, xtype='standard')
xprv = node.to_xprv() xprv = node.to_xprv()
super().__init__(xprv) super().__init__(xprv)
self.localfeatures |= LnLocalFeatures.GOSSIP_QUERIES_OPT self.features |= LnFeatures.GOSSIP_QUERIES_OPT
self.localfeatures |= LnLocalFeatures.GOSSIP_QUERIES_REQ self.features |= LnFeatures.GOSSIP_QUERIES_REQ
self.unknown_ids = set() self.unknown_ids = set()
def start_network(self, network: 'Network'): def start_network(self, network: 'Network'):
@ -419,8 +421,8 @@ class LNWallet(LNWorker):
self.db = wallet.db self.db = wallet.db
self.config = wallet.config self.config = wallet.config
LNWorker.__init__(self, xprv) LNWorker.__init__(self, xprv)
self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
self.localfeatures |= LnLocalFeatures.OPTION_STATIC_REMOTEKEY_REQ self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid
self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
self.sweep_address = wallet.get_receiving_address() self.sweep_address = wallet.get_receiving_address()
@ -952,7 +954,12 @@ class LNWallet(LNWorker):
if not peer: if not peer:
raise Exception('Dropped peer') raise Exception('Dropped peer')
await peer.initialized 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) self.network.trigger_callback('htlc_added', htlc, lnaddr, SENT)
payment_attempt = await self.await_payment(lnaddr.paymenthash) payment_attempt = await self.await_payment(lnaddr.paymenthash)
if payment_attempt.success: if payment_attempt.success:
@ -1047,7 +1054,7 @@ class LNWallet(LNWorker):
return addr return addr
@profiler @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) amount_msat = int(decoded_invoice.amount * COIN * 1000)
invoice_pubkey = decoded_invoice.pubkey.serialize() invoice_pubkey = decoded_invoice.pubkey.serialize()
# use 'r' field from invoice # use 'r' field from invoice
@ -1091,8 +1098,13 @@ class LNWallet(LNWorker):
fee_base_msat = channel_policy.fee_base_msat fee_base_msat = channel_policy.fee_base_msat
fee_proportional_millionths = channel_policy.fee_proportional_millionths fee_proportional_millionths = channel_policy.fee_proportional_millionths
cltv_expiry_delta = channel_policy.cltv_expiry_delta cltv_expiry_delta = channel_policy.cltv_expiry_delta
route.append(RouteEdge(node_pubkey, short_channel_id, fee_base_msat, fee_proportional_millionths, node_info = self.channel_db.get_node_info_for_node_id(node_id=node_pubkey)
cltv_expiry_delta)) 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 prev_node_id = node_pubkey
# test sanity # test sanity
if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()): 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()): 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}") self.logger.info(f"rejecting insane route {route}")
raise NoPathFound() 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 return route
def add_request(self, amount_sat, message, expiry): 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.") "Other clients will likely not be able to send to us.")
payment_preimage = os.urandom(32) payment_preimage = os.urandom(32)
payment_hash = sha256(payment_preimage) payment_hash = sha256(payment_preimage)
info = PaymentInfo(payment_hash, amount_sat, RECEIVED, PR_UNPAID) info = PaymentInfo(payment_hash, amount_sat, RECEIVED, PR_UNPAID)
amount_btc = amount_sat/Decimal(COIN) if amount_sat else None amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
if expiry == 0: if expiry == 0:
@ -1138,12 +1156,15 @@ class LNWallet(LNWorker):
# Our higher level invoices code however uses 0 for "never". # Our higher level invoices code however uses 0 for "never".
# Hence set some high expiration here # Hence set some high expiration here
expiry = 100 * 365 * 24 * 60 * 60 # 100 years 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), tags=[('d', message),
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE), ('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
('x', expiry)] ('x', expiry),
('9', self.features.for_invoice())]
+ routing_hints, + routing_hints,
date = timestamp) date=timestamp,
payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage))
invoice = lnencode(lnaddr, self.node_keypair.privkey) invoice = lnencode(lnaddr, self.node_keypair.privkey)
key = bh2u(lnaddr.paymenthash) key = bh2u(lnaddr.paymenthash)
req = { 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.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.segwit_addr import bech32_encode, bech32_decode
from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage
from . import ElectrumTestCase from . import ElectrumTestCase
@ -61,16 +62,28 @@ class TestBolt11(ElectrumTestCase):
tests = [ tests = [
LnAddr(RHASH, tags=[('d', '')]), LnAddr(paymenthash=RHASH, tags=[('d', '')]),
LnAddr(RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]), LnAddr(paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]),
LnAddr(RHASH, amount=Decimal('1'), tags=[('h', longdescription)]), LnAddr(paymenthash=RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),
LnAddr(RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]), LnAddr(paymenthash=RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),
LnAddr(RHASH, amount=24, tags=[ 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)]), ('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3),
LnAddr(RHASH, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription)]), (unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]),
LnAddr(RHASH, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription)]), ('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'),
LnAddr(RHASH, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription)]), ('h', longdescription)]),
LnAddr(RHASH, amount=24, tags=[('n', PUBKEY), ('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 # Roundtrip
@ -81,14 +94,14 @@ class TestBolt11(ElectrumTestCase):
def test_n_decoding(self): def test_n_decoding(self):
# We flip the signature recovery bit, which would normally give a different # We flip the signature recovery bit, which would normally give a different
# pubkey. # 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 = u5_to_bitarray(data)
databits.invert(-1) databits.invert(-1)
lnaddr = lndecode(bech32_encode(hrp, bitarray_to_u5(databits)), verbose=True) lnaddr = lndecode(bech32_encode(hrp, bitarray_to_u5(databits)), verbose=True)
assert lnaddr.pubkey.serialize() != PUBKEY assert lnaddr.pubkey.serialize() != PUBKEY
# But not if we supply expliciy `n` specifier! # 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', ''), tags=[('d', ''),
('n', PUBKEY)]), ('n', PUBKEY)]),
PRIVKEY), True) PRIVKEY), True)
@ -98,9 +111,28 @@ class TestBolt11(ElectrumTestCase):
assert lnaddr.pubkey.serialize() == PUBKEY assert lnaddr.pubkey.serialize() == PUBKEY
def test_min_final_cltv_expiry_decoding(self): 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): 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) invoice = lnencode(lnaddr, PRIVKEY)
self.assertEqual(150, lndecode(invoice).get_min_final_cltv_expiry()) 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.lnpeer import Peer
from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey
from electrum.lnutil import LightningPeerConnectionClosed, RemoteMisbehaving 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.lnchannel import channel_states, peer_states, Channel
from electrum.lnrouter import LNPathFinder from electrum.lnrouter import LNPathFinder
from electrum.channel_db import ChannelDB from electrum.channel_db import ChannelDB
@ -95,8 +95,8 @@ class MockLNWallet(Logger):
self.payments = {} self.payments = {}
self.logs = defaultdict(list) self.logs = defaultdict(list)
self.wallet = MockWallet() self.wallet = MockWallet()
self.localfeatures = LnLocalFeatures(0) self.features = LnFeatures(0)
self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
self.pending_payments = defaultdict(asyncio.Future) self.pending_payments = defaultdict(asyncio.Future)
chan.lnworker = self chan.lnworker = self
chan.node_id = remote_keypair.pubkey chan.node_id = remote_keypair.pubkey
@ -235,8 +235,8 @@ class TestPeer(ElectrumTestCase):
w2.save_preimage(RHASH, payment_preimage) w2.save_preimage(RHASH, payment_preimage)
w2.save_payment_info(info) w2.save_payment_info(info)
lnaddr = LnAddr( lnaddr = LnAddr(
RHASH, paymenthash=RHASH,
amount_btc, amount=amount_btc,
tags=[('c', lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE), tags=[('c', lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
('d', 'coffee') ('d', 'coffee')
]) ])
@ -317,8 +317,9 @@ class TestPeer(ElectrumTestCase):
alice_init_balance_msat = alice_channel.balance(HTLCOwner.LOCAL) alice_init_balance_msat = alice_channel.balance(HTLCOwner.LOCAL)
bob_init_balance_msat = bob_channel.balance(HTLCOwner.LOCAL) bob_init_balance_msat = bob_channel.balance(HTLCOwner.LOCAL)
num_payments = 50 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_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) max_htlcs_in_flight = asyncio.Semaphore(5)
async def single_payment(pay_req): async def single_payment(pay_req):
async with max_htlcs_in_flight: async with max_htlcs_in_flight:
@ -333,10 +334,10 @@ class TestPeer(ElectrumTestCase):
await gath await gath
with self.assertRaises(concurrent.futures.CancelledError): with self.assertRaises(concurrent.futures.CancelledError):
run(f()) run(f())
self.assertEqual(alice_init_balance_msat - num_payments * 1000, alice_channel.balance(HTLCOwner.LOCAL)) 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 * 1000, bob_channel.balance(HTLCOwner.REMOTE)) 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 * 1000, bob_channel.balance(HTLCOwner.LOCAL)) 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 * 1000, alice_channel.balance(HTLCOwner.REMOTE)) self.assertEqual(bob_init_balance_msat + num_payments * payment_value_sat * 1000, alice_channel.balance(HTLCOwner.REMOTE))
@needs_test_with_all_chacha20_implementations @needs_test_with_all_chacha20_implementations
def test_close(self): def test_close(self):
@ -354,7 +355,12 @@ class TestPeer(ElectrumTestCase):
await asyncio.wait_for(p2.initialized, 1) await asyncio.wait_for(p2.initialized, 1)
# alice sends htlc # alice sends htlc
route = w1._create_route_from_invoice(decoded_invoice=lnaddr) 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 # alice closes
await p1.close_channel(alice_channel.channel_id) await p1.close_channel(alice_channel.channel_id)
gath.cancel() gath.cancel()

216
electrum/tests/test_lnrouter.py

@ -4,9 +4,9 @@ import shutil
import asyncio import asyncio
from electrum.util import bh2u, bfh, create_and_start_event_loop 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, process_onion_packet, _decode_onion_error, decode_onion_error,
OnionFailureCode) OnionFailureCode, OnionPacket)
from electrum import bitcoin, lnrouter from electrum import bitcoin, lnrouter
from electrum.constants import BitcoinTestnet from electrum.constants import BitcoinTestnet
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
@ -57,46 +57,45 @@ class Test_LNRouter(TestCaseForTestnet):
'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02cccccccccccccccccccccccccccccccc', 'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02cccccccccccccccccccccccccccccccc',
'short_channel_id': bfh('0000000000000001'), 'short_channel_id': bfh('0000000000000001'),
'chain_hash': BitcoinTestnet.rev_genesis_bytes(), '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) self.assertEqual(cdb.num_channels, 1)
cdb.add_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', cdb.add_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'short_channel_id': bfh('0000000000000002'), 'short_channel_id': bfh('0000000000000002'),
'chain_hash': BitcoinTestnet.rev_genesis_bytes(), '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', cdb.add_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'short_channel_id': bfh('0000000000000003'), 'short_channel_id': bfh('0000000000000003'),
'chain_hash': BitcoinTestnet.rev_genesis_bytes(), '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', cdb.add_channel_announcement({'node_id_1': b'\x02cccccccccccccccccccccccccccccccc', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
'bitcoin_key_1': b'\x02cccccccccccccccccccccccccccccccc', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd', 'bitcoin_key_1': b'\x02cccccccccccccccccccccccccccccccc', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd',
'short_channel_id': bfh('0000000000000004'), 'short_channel_id': bfh('0000000000000004'),
'chain_hash': BitcoinTestnet.rev_genesis_bytes(), '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', cdb.add_channel_announcement({'node_id_1': b'\x02dddddddddddddddddddddddddddddddd', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'bitcoin_key_1': b'\x02dddddddddddddddddddddddddddddddd', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 'bitcoin_key_1': b'\x02dddddddddddddddddddddddddddddddd', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'short_channel_id': bfh('0000000000000005'), 'short_channel_id': bfh('0000000000000005'),
'chain_hash': BitcoinTestnet.rev_genesis_bytes(), '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', cdb.add_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd', 'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd',
'short_channel_id': bfh('0000000000000006'), 'short_channel_id': bfh('0000000000000006'),
'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
'len': b'\x00\x00', 'features': b''}, trusted=True) 'len': 0, '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': 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'\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': 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': 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': 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'\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': 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'\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': 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': 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': 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': 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': 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': 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': 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': 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': 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': 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': 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('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': 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'\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': 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('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'})
path = path_finder.find_path_for_payment(b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 100000) 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'), self.assertEqual([(b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', b'\x00\x00\x00\x00\x00\x00\x00\x03'),
(b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', b'\x00\x00\x00\x00\x00\x00\x00\x02'), (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) cdb.sql_thread.join(timeout=1)
@needs_test_with_all_chacha20_implementations @needs_test_with_all_chacha20_implementations
def test_new_onion_packet(self): def test_new_onion_packet_legacy(self):
# test vector from bolt-04 # test vector from bolt-04
payment_path_pubkeys = [ payment_path_pubkeys = [
bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'), bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
@ -124,28 +123,127 @@ class Test_LNRouter(TestCaseForTestnet):
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
hops_data = [ hops_data = [
OnionHopsDataSingle(OnionPerHop( OnionHopsDataSingle(is_tlv_payload=False, payload={
bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000') "amt_to_forward": {"amt_to_forward": 0},
)), "outgoing_cltv_value": {"outgoing_cltv_value": 0},
OnionHopsDataSingle(OnionPerHop( "short_channel_id": {"short_channel_id": bfh('0000000000000000')},
bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001') }),
)), OnionHopsDataSingle(is_tlv_payload=False, payload={
OnionHopsDataSingle(OnionPerHop( "amt_to_forward": {"amt_to_forward": 1},
bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002') "outgoing_cltv_value": {"outgoing_cltv_value": 1},
)), "short_channel_id": {"short_channel_id": bfh('0101010101010101')},
OnionHopsDataSingle(OnionPerHop( }),
bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003') OnionHopsDataSingle(is_tlv_payload=False, payload={
)), "amt_to_forward": {"amt_to_forward": 2},
OnionHopsDataSingle(OnionPerHop( "outgoing_cltv_value": {"outgoing_cltv_value": 2},
bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004') "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) packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data)
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71e87f9aab8f6378c6ff744c1f34b393ad28d065b535c1a8668d85d3b34a1b3befd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a1f9e7abc789266cc861cabd95818c0fc8efbdfdc14e3f7c2bc7eb8d6a79ef75ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d014698cf05d742557763d9cb743faeae65dcc79dddaecf27fe5942be5380d15e9a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040a2a2fba158a0d8085926dc2e44f0c88bf487da56e13ef2d5e676a8589881b4869ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565a9f99728426ce2380a9580e2a9442481ceae7679906c30b1a0e21a10f26150e0645ab6edfdab1ce8f8bea7b1dee511c5fd38ac0e702c1c15bb86b52bca1b71e15b96982d262a442024c33ceb7dd8f949063c2e5e613e873250e2f8708bd4e1924abd45f65c2fa5617bfb10ee9e4a42d6b5811acc8029c16274f937dac9e8817c7e579fdb767ffe277f26d413ced06b620ede8362081da21cf67c2ca9d6f15fe5bc05f82f5bb93f8916bad3d63338ca824f3bbc11b57ce94a5fa1bc239533679903d6fec92a8c792fd86e2960188c14f21e399cfd72a50c620e10aefc6249360b463df9a89bf6836f4f26359207b765578e5ed76ae9f31b1cc48324be576e3d8e44d217445dba466f9b6293fdf05448584eb64f61e02903f834518622b7d4732471c6e0e22e22d1f45e31f0509eab39cdea5980a492a1da2aaac55a98a01216cd4bfe7abaa682af0fbff2dfed030ba28f1285df750e4d3477190dd193f8643b61d8ac1c427d590badb1f61a05d480908fbdc7c6f0502dd0c4abb51d725e92f95da2a8facb79881a844e2026911adcc659d1fb20a2fce63787c8bb0d9f6789c4b231c76da81c3f0718eb7156565a081d2be6b4170c0e0bcebddd459f53db2590c974bca0d705c055dee8c629bf854a5d58edc85228499ec6dde80cce4c8910b81b1e9e8b0f43bd39c8d69c3a80672729b7dc952dd9448688b6bd06afc2d2819cda80b66c57b52ccf7ac1a86601410d18d0c732f69de792e0894a9541684ef174de766fd4ce55efea8f53812867be6a391ac865802dbc26d93959df327ec2667c7256aa5a1d3c45a69a6158f285d6c97c3b8eedb09527848500517995a9eae4cd911df531544c77f5a9a2f22313e3eb72ca7a07dba243476bc926992e0d1e58b4a2fc8c7b01e0cad726237933ea319bad7537d39f3ed635d1e6c1d29e97b3d2160a09e30ee2b65ac5bce00996a73c008bcf351cecb97b6833b6d121dcf4644260b2946ea204732ac9954b228f0beaa15071930fd9583dfc466d12b5f0eeeba6dcf23d5ce8ae62ee5796359d97a4a15955c778d868d0ef9991d9f2833b5bb66119c5f8b396fd108baed7906cbb3cc376d13551caed97fece6f42a4c908ee279f1127fda1dd3ee77d8de0a6f3c135fa3f1cffe38591b6738dc97b55f0acc52be9753ce53e64d7e497bb00ca6123758df3b68fad99e35c04389f7514a8e36039f541598a417275e77869989782325a15b5342ac5011ff07af698584b476b35d941a4981eac590a07a092bb50342da5d3341f901aa07964a8d02b623c7b106dd0ae50bfa007a22d46c8772fa55558176602946cb1d11ea5460db7586fb89c6d3bcd3ab6dd20df4a4db63d2e7d52380800ad812b8640887e027e946df96488b47fbc4a4fadaa8beda4abe446fafea5403fae2ef'), self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71e87f9aab8f6378c6ff744c1f34b393ad28d065b535c1a8668d85d3b34a1b3befd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a1f9e7abc789266cc861cabd95818c0fc8efbdfdc14e3f7c2bc7eb8d6a79ef75ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d014698cf05d742557763d9cb743faeae65dcc79dddaecf27fe5942be5380d15e9a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040a2a2fba158a0d8085926dc2e44f0c88bf487da56e13ef2d5e676a8589881b4869ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565a9f99728426ce2380a9580e2a9442481ceae7679906c30b1a0e21a10f26150e0645ab6edfdab1ce8f8bea7b1dee511c5fd38ac0e702c1c15bb86b52bca1b71e15b96982d262a442024c33ceb7dd8f949063c2e5e613e873250e2f8708bd4e1924abd45f65c2fa5617bfb10ee9e4a42d6b5811acc8029c16274f937dac9e8817c7e579fdb767ffe277f26d413ced06b620ede8362081da21cf67c2ca9d6f15fe5bc05f82f5bb93f8916bad3d63338ca824f3bbc11b57ce94a5fa1bc239533679903d6fec92a8c792fd86e2960188c14f21e399cfd72a50c620e10aefc6249360b463df9a89bf6836f4f26359207b765578e5ed76ae9f31b1cc48324be576e3d8e44d217445dba466f9b6293fdf05448584eb64f61e02903f834518622b7d4732471c6e0e22e22d1f45e31f0509eab39cdea5980a492a1da2aaac55a98a01216cd4bfe7abaa682af0fbff2dfed030ba28f1285df750e4d3477190dd193f8643b61d8ac1c427d590badb1f61a05d480908fbdc7c6f0502dd0c4abb51d725e92f95da2a8facb79881a844e2026911adcc659d1fb20a2fce63787c8bb0d9f6789c4b231c76da81c3f0718eb7156565a081d2be6b4170c0e0bcebddd459f53db2590c974bca0d705c055dee8c629bf854a5d58edc85228499ec6dde80cce4c8910b81b1e9e8b0f43bd39c8d69c3a80672729b7dc952dd9448688b6bd06afc2d2819cda80b66c57b52ccf7ac1a86601410d18d0c732f69de792e0894a9541684ef174de766fd4ce55efea8f53812867be6a391ac865802dbc26d93959df327ec2667c7256aa5a1d3c45a69a6158f285d6c97c3b8eedb09527848500517995a9eae4cd911df531544c77f5a9a2f22313e3eb72ca7a07dba243476bc926992e0d1e58b4a2fc8c7b01e0cad726237933ea319bad7537d39f3ed635d1e6c1d29e97b3d2160a09e30ee2b65ac5bce00996a73c008bcf351cecb97b6833b6d121dcf4644260b2946ea204732ac9954b228f0beaa15071930fd9583dfc466d12b5f0eeeba6dcf23d5ce8ae62ee5796359d97a4a15955c778d868d0ef9991d9f2833b5bb66119c5f8b396fd108baed7906cbb3cc376d13551caed97fece6f42a4c908ee279f1127fda1dd3ee77d8de0a6f3c135fa3f1cffe38591b6738dc97b55f0acc52be9753ce53e64d7e497bb00ca6123758df3b68fad99e35c04389f7514a8e36039f541598a417275e77869989782325a15b5342ac5011ff07af698584b476b35d941a4981eac590a07a092bb50342da5d3341f901aa07964a8d02b623c7b106dd0ae50bfa007a22d46c8772fa55558176602946cb1d11ea5460db7586fb89c6d3bcd3ab6dd20df4a4db63d2e7d52380800ad812b8640887e027e946df96488b47fbc4a4fadaa8beda4abe446fafea5403fae2ef'),
packet.to_bytes()) packet.to_bytes())
@needs_test_with_all_chacha20_implementations @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; # this test is not from bolt-04, but is based on the one there;
# except here we have the privkeys for these pubkeys # except here we have the privkeys for these pubkeys
payment_path_pubkeys = [ payment_path_pubkeys = [
@ -165,28 +263,38 @@ class Test_LNRouter(TestCaseForTestnet):
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141') session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242') associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
hops_data = [ hops_data = [
OnionHopsDataSingle(OnionPerHop( OnionHopsDataSingle(is_tlv_payload=False, payload={
bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000') "amt_to_forward": {"amt_to_forward": 0},
)), "outgoing_cltv_value": {"outgoing_cltv_value": 0},
OnionHopsDataSingle(OnionPerHop( "short_channel_id": {"short_channel_id": bfh('0000000000000000')},
bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001') }),
)), OnionHopsDataSingle(is_tlv_payload=False, payload={
OnionHopsDataSingle(OnionPerHop( "amt_to_forward": {"amt_to_forward": 1},
bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002') "outgoing_cltv_value": {"outgoing_cltv_value": 1},
)), "short_channel_id": {"short_channel_id": bfh('0101010101010101')},
OnionHopsDataSingle(OnionPerHop( }),
bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003') OnionHopsDataSingle(is_tlv_payload=False, payload={
)), "amt_to_forward": {"amt_to_forward": 2},
OnionHopsDataSingle(OnionPerHop( "outgoing_cltv_value": {"outgoing_cltv_value": 2},
bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004') "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) packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data)
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661954176cd9869da33d713aa219fcef1e5c806fef11e696bcc66844de8271c27974a049d041ffc5be934b8575c6ff4371f2f88d4edfd73e445534d3f6ae15b64b0d8308390bebf8d149002e31bdc283056477ba27c8054c248ad7306de31663a7c99ec65b251704041f7c4cc40a0016ba172fbf805ec59132a65a4c7eb1f41337931c5df0f840704535729262d30c6132d1b390f073edec8fa057176c6268b6ad06a82ff0229c3be444ee50b40686bc1306838b93c65771de1b6ca05dace1ff9814a6e58b2dd71e8244c83e28b2ed5a3b09e9e7df5c8c747e5765ba366a4f7407a6c6b0a32fb5521cce7cd668f7434c909c1be027d8595d85893e5f612c49a93eeeed80a78bab9c4a621ce0f6f5df7d64a9c8d435db19de192d9db522c7f7b4e201fc1b61a9bd3efd062ae24455d463818b01e2756c7d0691bc3ac4c017be34c9a8b2913bb1b94056bf7a21730afc3f254ffa41ca140a5d87ff470f536d08619e8004d50de2fe5954d6aa4a00570da397ba15ae9ea4d7d1f136256a9093f0a787a36cbb3520b6a3cf4d1b13b16bf399c4b0326da1382a90bd79cf92f4808c8c84eaa50a8ccf44acbde0e35b2e6b72858c8446d6a05f3ba70fb4adc70af27cea9bd1dc1ea35fb3cc236b8b9b69b614903db339b22ad5dc2ddda7ac65fd7de24e60b7dbba7aafc9d26c0f9fcb03f1bb85dfc21762f862620651db52ea703ae60aa7e07febf11caa95c4245a4b37eb9c233e1ab1604fb85849e7f49cb9f7c681c4d91b7c320eb4b89b9c6bcb636ceadda59f6ed47aa5b1ea0a946ea98f6ad2614e79e0d4ef96d6d65903adb0479709e03008bbdf3355dd87df7d68965fdf1ad5c68b6dc2761b96b10f8eb4c0329a646bf38cf4702002e61565231b4ac7a9cde63d23f7b24c9d42797b3c434558d71ed8bf9fcab2c2aee3e8b38c19f9edc3ad3dfe9ebba7387ce4764f97ed1c1a83552dff8315546761479a6f929c39bcca0891d4a967d1b77fa80feed6ae74ac82ed5fb7be225c3f2b0ebdc652afc2255c47bc318ac645bbf19c0819ff527ff6708a78e19c8ca3dc8087035e10d5ac976e84b71148586c8a5a7b26ed11b5b401ce7bb2ac532207eaa24d2f53aaa8024607da764d807c91489e82fcad04e6b8992a507119367f576ee5ffe6807d5723d60234d4c3f94adce0acfed9dba535ca375446a4e9b500b74ad2a66e1c6b0fc38933f282d3a4a877bceceeca52b46e731ca51a9534224a883c4a45587f973f73a22069a4154b1da03d307d8575c821bef0eef87165b9a1bbf902ecfca82ddd805d10fbb7147b496f6772f01e9bf542b00288f3a6efab32590c1f34535ece03a0587ca187d27a98d4c9aa7c044794baa43a81abbe307f51d0bda6e7b4cf62c4be553b176321777e7fd483d6cec16df137293aaf3ad53608e1c7831368675bb9608db04d5c859e7714edab3d2389837fa071f0795adfabc51507b1adbadc7f83e80bd4e4eb9ed1a89c9e0a6dc16f38d55181d5666b02150651961aab34faef97d80fa4e1960864dfec3b687fd4eadf7aa6c709cb4698ae86ae112f386f33731d996b9d41926a2e820c6ba483a61674a4bae03af37e872ffdc0a9a8a034327af17e13e9e7ac619c9188c2a5c12a6ebf887721455c0e2822e67a621ed49f1f50dfc38b71c29d0224954e84ced086c80de552cca3a14adbe43035901225bafc3db3b672c780e4fa12b59221f93690527efc16a28e7c63d1a99fc881f023b03a157076a7e999a715ed37521adb483e2477d75ba5a55d4abad22b024c5317334b6544f15971591c774d896229e4e668fc1c7958fbd76fa0b152a6f14c95692083badd066b6621367fd73d88ba8d860566e6d55b871d80c68296b80ae8847d'), self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661954176cd9869da33d713aa219fcef1e5c806fef11e696bcc66844de8271c27974a049d041ffc5be934b8575c6ff4371f2f88d4edfd73e445534d3f6ae15b64b0d8308390bebf8d149002e31bdc283056477ba27c8054c248ad7306de31663a7c99ec65b251704041f7c4cc40a0016ba172fbf805ec59132a65a4c7eb1f41337931c5df0f840704535729262d30c6132d1b390f073edec8fa057176c6268b6ad06a82ff0229c3be444ee50b40686bc1306838b93c65771de1b6ca05dace1ff9814a6e58b2dd71e8244c83e28b2ed5a3b09e9e7df5c8c747e5765ba366a4f7407a6c6b0a32fb5521cce7cd668f7434c909c1be027d8595d85893e5f612c49a93eeeed80a78bab9c4a621ce0f6f5df7d64a9c8d435db19de192d9db522c7f7b4e201fc1b61a9bd3efd062ae24455d463818b01e2756c7d0691bc3ac4c017be34c9a8b2913bb1b94056bf7a21730afc3f254ffa41ca140a5d87ff470f536d08619e8004d50de2fe5954d6aa4a00570da397ba15ae9ea4d7d1f136256a9093f0a787a36cbb3520b6a3cf4d1b13b16bf399c4b0326da1382a90bd79cf92f4808c8c84eaa50a8ccf44acbde0e35b2e6b72858c8446d6a05f3ba70fb4adc70af27cea9bd1dc1ea35fb3cc236b8b9b69b614903db339b22ad5dc2ddda7ac65fd7de24e60b7dbba7aafc9d26c0f9fcb03f1bb85dfc21762f862620651db52ea703ae60aa7e07febf11caa95c4245a4b37eb9c233e1ab1604fb85849e7f49cb9f7c681c4d91b7c320eb4b89b9c6bcb636ceadda59f6ed47aa5b1ea0a946ea98f6ad2614e79e0d4ef96d6d65903adb0479709e03008bbdf3355dd87df7d68965fdf1ad5c68b6dc2761b96b10f8eb4c0329a646bf38cf4702002e61565231b4ac7a9cde63d23f7b24c9d42797b3c434558d71ed8bf9fcab2c2aee3e8b38c19f9edc3ad3dfe9ebba7387ce4764f97ed1c1a83552dff8315546761479a6f929c39bcca0891d4a967d1b77fa80feed6ae74ac82ed5fb7be225c3f2b0ebdc652afc2255c47bc318ac645bbf19c0819ff527ff6708a78e19c8ca3dc8087035e10d5ac976e84b71148586c8a5a7b26ed11b5b401ce7bb2ac532207eaa24d2f53aaa8024607da764d807c91489e82fcad04e6b8992a507119367f576ee5ffe6807d5723d60234d4c3f94adce0acfed9dba535ca375446a4e9b500b74ad2a66e1c6b0fc38933f282d3a4a877bceceeca52b46e731ca51a9534224a883c4a45587f973f73a22069a4154b1da03d307d8575c821bef0eef87165b9a1bbf902ecfca82ddd805d10fbb7147b496f6772f01e9bf542b00288f3a6efab32590c1f34535ece03a0587ca187d27a98d4c9aa7c044794baa43a81abbe307f51d0bda6e7b4cf62c4be553b176321777e7fd483d6cec16df137293aaf3ad53608e1c7831368675bb9608db04d5c859e7714edab3d2389837fa071f0795adfabc51507b1adbadc7f83e80bd4e4eb9ed1a89c9e0a6dc16f38d55181d5666b02150651961aab34faef97d80fa4e1960864dfec3b687fd4eadf7aa6c709cb4698ae86ae112f386f33731d996b9d41926a2e820c6ba483a61674a4bae03af37e872ffdc0a9a8a034327af17e13e9e7ac619c9188c2a5c12a6ebf887721455c0e2822e67a621ed49f1f50dfc38b71c29d0224954e84ced086c80de552cca3a14adbe43035901225bafc3db3b672c780e4fa12b59221f93690527efc16a28e7c63d1a99fc881f023b03a157076a7e999a715ed37521adb483e2477d75ba5a55d4abad22b024c5317334b6544f15971591c774d896229e4e668fc1c7958fbd76fa0b152a6f14c95692083badd066b6621367fd73d88ba8d860566e6d55b871d80c68296b80ae8847d'),
packet.to_bytes()) packet.to_bytes())
for i, privkey in enumerate(payment_path_privkeys): for i, privkey in enumerate(payment_path_privkeys):
processed_packet = process_onion_packet(packet, associated_data, privkey) processed_packet = process_onion_packet(packet, associated_data, privkey)
self.assertEqual(hops_data[i].per_hop.to_bytes(), processed_packet.hop_data.per_hop.to_bytes()) self.assertEqual(hops_data[i].to_bytes(), processed_packet.hop_data.to_bytes())
packet = processed_packet.next_packet packet = processed_packet.next_packet
@needs_test_with_all_chacha20_implementations @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, make_htlc_tx_inputs, secret_to_pubkey, derive_blinded_pubkey, derive_privkey,
derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret, derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret,
get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, 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.util import bh2u, bfh, MyEncoder
from electrum.transaction import Transaction, PartialTransaction from electrum.transaction import Transaction, PartialTransaction
@ -755,3 +755,53 @@ class TestLNUtil(ElectrumTestCase):
with self.assertRaises(ConnStringFormatError): with self.assertRaises(ConnStringFormatError):
extract_nodeid("00" * 33 + "@") extract_nodeid("00" * 33 + "@")
self.assertEqual(extract_nodeid("00" * 33 + "@localhost"), (b"\x00" * 33, "localhost")) 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