mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-31 09:21:39 +00:00
Merge pull request #6050 from SomberNight/202003_lnmsg_rewrite
lnmsg rewrite, implement TLV, invoice features, varonion, payment secret
This commit is contained in:
commit
158854f94e
18 changed files with 1999 additions and 1318 deletions
|
@ -38,7 +38,8 @@ from .sql_db import SqlDB, sql
|
|||
from . import constants
|
||||
from .util import bh2u, profiler, get_headers_dir, bfh, is_ip_address, list_enabled_bits
|
||||
from .logging import Logger
|
||||
from .lnutil import LN_GLOBAL_FEATURES_KNOWN_SET, LNPeerAddr, format_short_channel_id, ShortChannelID
|
||||
from .lnutil import (LNPeerAddr, format_short_channel_id, ShortChannelID,
|
||||
validate_features, IncompatibleOrInsaneFeatures)
|
||||
from .lnverifier import LNChannelVerifier, verify_sig_for_channel_update
|
||||
from .lnmsg import decode_msg
|
||||
|
||||
|
@ -47,15 +48,6 @@ if TYPE_CHECKING:
|
|||
from .lnchannel import Channel
|
||||
|
||||
|
||||
class UnknownEvenFeatureBits(Exception): pass
|
||||
|
||||
def validate_features(features : int):
|
||||
enabled_features = list_enabled_bits(features)
|
||||
for fbit in enabled_features:
|
||||
if (1 << fbit) not in LN_GLOBAL_FEATURES_KNOWN_SET and fbit % 2 == 0:
|
||||
raise UnknownEvenFeatureBits()
|
||||
|
||||
|
||||
FLAG_DISABLE = 1 << 1
|
||||
FLAG_DIRECTION = 1 << 0
|
||||
|
||||
|
@ -102,14 +94,14 @@ class Policy(NamedTuple):
|
|||
def from_msg(payload: dict) -> 'Policy':
|
||||
return Policy(
|
||||
key = payload['short_channel_id'] + payload['start_node'],
|
||||
cltv_expiry_delta = int.from_bytes(payload['cltv_expiry_delta'], "big"),
|
||||
htlc_minimum_msat = int.from_bytes(payload['htlc_minimum_msat'], "big"),
|
||||
htlc_maximum_msat = int.from_bytes(payload['htlc_maximum_msat'], "big") if 'htlc_maximum_msat' in payload else None,
|
||||
fee_base_msat = int.from_bytes(payload['fee_base_msat'], "big"),
|
||||
fee_proportional_millionths = int.from_bytes(payload['fee_proportional_millionths'], "big"),
|
||||
cltv_expiry_delta = payload['cltv_expiry_delta'],
|
||||
htlc_minimum_msat = payload['htlc_minimum_msat'],
|
||||
htlc_maximum_msat = payload.get('htlc_maximum_msat', None),
|
||||
fee_base_msat = payload['fee_base_msat'],
|
||||
fee_proportional_millionths = payload['fee_proportional_millionths'],
|
||||
message_flags = int.from_bytes(payload['message_flags'], "big"),
|
||||
channel_flags = int.from_bytes(payload['channel_flags'], "big"),
|
||||
timestamp = int.from_bytes(payload['timestamp'], "big")
|
||||
timestamp = payload['timestamp'],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@ -154,7 +146,7 @@ class NodeInfo(NamedTuple):
|
|||
alias = alias.decode('utf8')
|
||||
except:
|
||||
alias = ''
|
||||
timestamp = int.from_bytes(payload['timestamp'], "big")
|
||||
timestamp = payload['timestamp']
|
||||
node_info = NodeInfo(node_id=node_id, features=features, timestamp=timestamp, alias=alias)
|
||||
return node_info, peer_addrs
|
||||
|
||||
|
@ -321,11 +313,12 @@ class ChannelDB(SqlDB):
|
|||
return ret
|
||||
|
||||
# note: currently channel announcements are trusted by default (trusted=True);
|
||||
# they are not verified. Verifying them would make the gossip sync
|
||||
# they are not SPV-verified. Verifying them would make the gossip sync
|
||||
# even slower; especially as servers will start throttling us.
|
||||
# It would probably put significant strain on servers if all clients
|
||||
# verified the complete gossip.
|
||||
def add_channel_announcement(self, msg_payloads, *, trusted=True):
|
||||
# note: signatures have already been verified.
|
||||
if type(msg_payloads) is dict:
|
||||
msg_payloads = [msg_payloads]
|
||||
added = 0
|
||||
|
@ -338,8 +331,8 @@ class ChannelDB(SqlDB):
|
|||
continue
|
||||
try:
|
||||
channel_info = ChannelInfo.from_msg(msg)
|
||||
except UnknownEvenFeatureBits:
|
||||
self.logger.info("unknown feature bits")
|
||||
except IncompatibleOrInsaneFeatures as e:
|
||||
self.logger.info(f"unknown or insane feature bits: {e!r}")
|
||||
continue
|
||||
if trusted:
|
||||
added += 1
|
||||
|
@ -353,7 +346,7 @@ class ChannelDB(SqlDB):
|
|||
def add_verified_channel_info(self, msg: dict, *, capacity_sat: int = None) -> None:
|
||||
try:
|
||||
channel_info = ChannelInfo.from_msg(msg)
|
||||
except UnknownEvenFeatureBits:
|
||||
except IncompatibleOrInsaneFeatures:
|
||||
return
|
||||
channel_info = channel_info._replace(capacity_sat=capacity_sat)
|
||||
with self.lock:
|
||||
|
@ -392,7 +385,7 @@ class ChannelDB(SqlDB):
|
|||
now = int(time.time())
|
||||
for payload in payloads:
|
||||
short_channel_id = ShortChannelID(payload['short_channel_id'])
|
||||
timestamp = int.from_bytes(payload['timestamp'], "big")
|
||||
timestamp = payload['timestamp']
|
||||
if max_age and now - timestamp > max_age:
|
||||
expired.append(payload)
|
||||
continue
|
||||
|
@ -407,7 +400,7 @@ class ChannelDB(SqlDB):
|
|||
known.append(payload)
|
||||
# compare updates to existing database entries
|
||||
for payload in known:
|
||||
timestamp = int.from_bytes(payload['timestamp'], "big")
|
||||
timestamp = payload['timestamp']
|
||||
start_node = payload['start_node']
|
||||
short_channel_id = ShortChannelID(payload['short_channel_id'])
|
||||
key = (start_node, short_channel_id)
|
||||
|
@ -499,13 +492,14 @@ class ChannelDB(SqlDB):
|
|||
raise Exception(f'failed verifying channel update for {short_channel_id}')
|
||||
|
||||
def add_node_announcement(self, msg_payloads):
|
||||
# note: signatures have already been verified.
|
||||
if type(msg_payloads) is dict:
|
||||
msg_payloads = [msg_payloads]
|
||||
new_nodes = {}
|
||||
for msg_payload in msg_payloads:
|
||||
try:
|
||||
node_info, node_addresses = NodeInfo.from_msg(msg_payload)
|
||||
except UnknownEvenFeatureBits:
|
||||
except IncompatibleOrInsaneFeatures:
|
||||
continue
|
||||
node_id = node_info.node_id
|
||||
# Ignore node if it has no associated channel (DoS protection)
|
||||
|
@ -599,11 +593,17 @@ class ChannelDB(SqlDB):
|
|||
self._recent_peers = sorted_node_ids[:self.NUM_MAX_RECENT_PEERS]
|
||||
c.execute("""SELECT * FROM channel_info""")
|
||||
for short_channel_id, msg in c:
|
||||
ci = ChannelInfo.from_raw_msg(msg)
|
||||
try:
|
||||
ci = ChannelInfo.from_raw_msg(msg)
|
||||
except IncompatibleOrInsaneFeatures:
|
||||
continue
|
||||
self._channels[ShortChannelID.normalize(short_channel_id)] = ci
|
||||
c.execute("""SELECT * FROM node_info""")
|
||||
for node_id, msg in c:
|
||||
node_info, node_addresses = NodeInfo.from_raw_msg(msg)
|
||||
try:
|
||||
node_info, node_addresses = NodeInfo.from_raw_msg(msg)
|
||||
except IncompatibleOrInsaneFeatures:
|
||||
continue
|
||||
# don't load node_addresses because they dont have timestamps
|
||||
self._nodes[node_id] = node_info
|
||||
c.execute("""SELECT * FROM policy""")
|
||||
|
@ -671,7 +671,7 @@ class ChannelDB(SqlDB):
|
|||
return
|
||||
now = int(time.time())
|
||||
remote_update_decoded = decode_msg(remote_update_raw)[1]
|
||||
remote_update_decoded['timestamp'] = now.to_bytes(4, byteorder="big")
|
||||
remote_update_decoded['timestamp'] = now
|
||||
remote_update_decoded['start_node'] = node_id
|
||||
return Policy.from_msg(remote_update_decoded)
|
||||
elif node_id == chan.get_local_pubkey(): # outgoing direction (from us)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -141,6 +141,21 @@ def tagged(char, l):
|
|||
def tagged_bytes(char, l):
|
||||
return tagged(char, bitstring.BitArray(l))
|
||||
|
||||
def trim_to_min_length(bits):
|
||||
"""Ensures 'bits' have min number of leading zeroes.
|
||||
Assumes 'bits' is big-endian, and that it needs to be encoded in 5 bit blocks.
|
||||
"""
|
||||
bits = bits[:] # copy
|
||||
# make sure we can be split into 5 bit blocks
|
||||
while bits.len % 5 != 0:
|
||||
bits.prepend('0b0')
|
||||
# Get minimal length by trimming leading 5 bits at a time.
|
||||
while bits.startswith('0b00000'):
|
||||
if len(bits) == 5:
|
||||
break # v == 0
|
||||
bits = bits[5:]
|
||||
return bits
|
||||
|
||||
# Discard trailing bits, convert to bytes.
|
||||
def trim_to_bytes(barr):
|
||||
# Adds a byte if necessary.
|
||||
|
@ -155,7 +170,7 @@ def pull_tagged(stream):
|
|||
length = stream.read(5).uint * 32 + stream.read(5).uint
|
||||
return (CHARSET[tag], stream.read(length * 5), stream)
|
||||
|
||||
def lnencode(addr, privkey):
|
||||
def lnencode(addr: 'LnAddr', privkey) -> str:
|
||||
if addr.amount:
|
||||
amount = Decimal(str(addr.amount))
|
||||
# We can only send down to millisatoshi.
|
||||
|
@ -172,16 +187,22 @@ def lnencode(addr, privkey):
|
|||
# Start with the timestamp
|
||||
data = bitstring.pack('uint:35', addr.date)
|
||||
|
||||
tags_set = set()
|
||||
|
||||
# Payment hash
|
||||
data += tagged_bytes('p', addr.paymenthash)
|
||||
tags_set = set()
|
||||
tags_set.add('p')
|
||||
|
||||
if addr.payment_secret is not None:
|
||||
data += tagged_bytes('s', addr.payment_secret)
|
||||
tags_set.add('s')
|
||||
|
||||
for k, v in addr.tags:
|
||||
|
||||
# BOLT #11:
|
||||
#
|
||||
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
|
||||
if k in ('d', 'h', 'n', 'x'):
|
||||
if k in ('d', 'h', 'n', 'x', 'p', 's'):
|
||||
if k in tags_set:
|
||||
raise ValueError("Duplicate '{}' tag".format(k))
|
||||
|
||||
|
@ -196,23 +217,23 @@ def lnencode(addr, privkey):
|
|||
elif k == 'd':
|
||||
data += tagged_bytes('d', v.encode())
|
||||
elif k == 'x':
|
||||
# Get minimal length by trimming leading 5 bits at a time.
|
||||
expirybits = bitstring.pack('intbe:64', v)[4:64]
|
||||
while expirybits.startswith('0b00000'):
|
||||
if len(expirybits) == 5:
|
||||
break # v == 0
|
||||
expirybits = expirybits[5:]
|
||||
expirybits = bitstring.pack('intbe:64', v)
|
||||
expirybits = trim_to_min_length(expirybits)
|
||||
data += tagged('x', expirybits)
|
||||
elif k == 'h':
|
||||
data += tagged_bytes('h', sha256(v.encode('utf-8')).digest())
|
||||
elif k == 'n':
|
||||
data += tagged_bytes('n', v)
|
||||
elif k == 'c':
|
||||
# Get minimal length by trimming leading 5 bits at a time.
|
||||
finalcltvbits = bitstring.pack('intbe:64', v)[4:64]
|
||||
while finalcltvbits.startswith('0b00000'):
|
||||
finalcltvbits = finalcltvbits[5:]
|
||||
finalcltvbits = bitstring.pack('intbe:64', v)
|
||||
finalcltvbits = trim_to_min_length(finalcltvbits)
|
||||
data += tagged('c', finalcltvbits)
|
||||
elif k == '9':
|
||||
if v == 0:
|
||||
continue
|
||||
feature_bits = bitstring.BitArray(uint=v, length=v.bit_length())
|
||||
feature_bits = trim_to_min_length(feature_bits)
|
||||
data += tagged('9', feature_bits)
|
||||
else:
|
||||
# FIXME: Support unknown tags?
|
||||
raise ValueError("Unknown tag {}".format(k))
|
||||
|
@ -239,15 +260,17 @@ def lnencode(addr, privkey):
|
|||
return bech32_encode(hrp, bitarray_to_u5(data))
|
||||
|
||||
class LnAddr(object):
|
||||
def __init__(self, paymenthash: bytes = None, amount=None, currency=None, tags=None, date=None):
|
||||
def __init__(self, *, paymenthash: bytes = None, amount=None, currency=None, tags=None, date=None,
|
||||
payment_secret: bytes = None):
|
||||
self.date = int(time.time()) if not date else int(date)
|
||||
self.tags = [] if not tags else tags
|
||||
self.unknown_tags = []
|
||||
self.paymenthash = paymenthash
|
||||
self.payment_secret = payment_secret
|
||||
self.signature = None
|
||||
self.pubkey = None
|
||||
self.currency = constants.net.SEGWIT_HRP if currency is None else currency
|
||||
self.amount = amount
|
||||
self.amount = amount # in bitcoins
|
||||
self._min_final_cltv_expiry = 9
|
||||
|
||||
def __str__(self):
|
||||
|
@ -383,14 +406,28 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
|
|||
continue
|
||||
addr.paymenthash = trim_to_bytes(tagdata)
|
||||
|
||||
elif tag == 's':
|
||||
if data_length != 52:
|
||||
addr.unknown_tags.append((tag, tagdata))
|
||||
continue
|
||||
addr.payment_secret = trim_to_bytes(tagdata)
|
||||
|
||||
elif tag == 'n':
|
||||
if data_length != 53:
|
||||
addr.unknown_tags.append((tag, tagdata))
|
||||
continue
|
||||
pubkeybytes = trim_to_bytes(tagdata)
|
||||
addr.pubkey = pubkeybytes
|
||||
|
||||
elif tag == 'c':
|
||||
addr._min_final_cltv_expiry = tagdata.int
|
||||
|
||||
elif tag == '9':
|
||||
features = tagdata.uint
|
||||
addr.tags.append(('9', features))
|
||||
from .lnutil import validate_features
|
||||
validate_features(features)
|
||||
|
||||
else:
|
||||
addr.unknown_tags.append((tag, tagdata))
|
||||
|
||||
|
|
|
@ -218,13 +218,13 @@ class Channel(Logger):
|
|||
short_channel_id=self.short_channel_id,
|
||||
channel_flags=channel_flags,
|
||||
message_flags=b'\x01',
|
||||
cltv_expiry_delta=lnutil.NBLOCK_OUR_CLTV_EXPIRY_DELTA.to_bytes(2, byteorder="big"),
|
||||
htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat.to_bytes(8, byteorder="big"),
|
||||
htlc_maximum_msat=htlc_maximum_msat.to_bytes(8, byteorder="big"),
|
||||
fee_base_msat=lnutil.OUR_FEE_BASE_MSAT.to_bytes(4, byteorder="big"),
|
||||
fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS.to_bytes(4, byteorder="big"),
|
||||
cltv_expiry_delta=lnutil.NBLOCK_OUR_CLTV_EXPIRY_DELTA,
|
||||
htlc_minimum_msat=self.config[REMOTE].htlc_minimum_msat,
|
||||
htlc_maximum_msat=htlc_maximum_msat,
|
||||
fee_base_msat=lnutil.OUR_FEE_BASE_MSAT,
|
||||
fee_proportional_millionths=lnutil.OUR_FEE_PROPORTIONAL_MILLIONTHS,
|
||||
chain_hash=constants.net.rev_genesis_bytes(),
|
||||
timestamp=now.to_bytes(4, byteorder="big"),
|
||||
timestamp=now,
|
||||
)
|
||||
sighash = sha256d(chan_upd[2 + 64:])
|
||||
sig = ecc.ECPrivkey(self.lnworker.node_keypair.privkey).sign(sighash, ecc.sig_string_from_r_and_s)
|
||||
|
@ -249,7 +249,8 @@ class Channel(Logger):
|
|||
node_ids = sorted_node_ids
|
||||
bitcoin_keys.reverse()
|
||||
|
||||
chan_ann = encode_msg("channel_announcement",
|
||||
chan_ann = encode_msg(
|
||||
"channel_announcement",
|
||||
len=0,
|
||||
features=b'',
|
||||
chain_hash=constants.net.rev_genesis_bytes(),
|
||||
|
@ -257,7 +258,7 @@ class Channel(Logger):
|
|||
node_id_1=node_ids[0],
|
||||
node_id_2=node_ids[1],
|
||||
bitcoin_key_1=bitcoin_keys[0],
|
||||
bitcoin_key_2=bitcoin_keys[1]
|
||||
bitcoin_key_2=bitcoin_keys[1],
|
||||
)
|
||||
|
||||
self._chan_ann_without_sigs = chan_ann
|
||||
|
|
|
@ -1,153 +1,513 @@
|
|||
import json
|
||||
import os
|
||||
from typing import Callable, Tuple
|
||||
import csv
|
||||
import io
|
||||
from typing import Callable, Tuple, Any, Dict, List, Sequence, Union, Optional
|
||||
from collections import OrderedDict
|
||||
|
||||
def _eval_length_term(x, ma: dict) -> int:
|
||||
"""
|
||||
Evaluate a term of the simple language used
|
||||
to specify lightning message field lengths.
|
||||
from .lnutil import OnionFailureCodeMetaFlag
|
||||
|
||||
If `x` is an integer, it is returned as is,
|
||||
otherwise it is treated as a variable and
|
||||
looked up in `ma`.
|
||||
|
||||
If the value in `ma` was no integer, it is
|
||||
assumed big-endian bytes and decoded.
|
||||
class MalformedMsg(Exception): pass
|
||||
class UnknownMsgFieldType(MalformedMsg): pass
|
||||
class UnexpectedEndOfStream(MalformedMsg): pass
|
||||
class FieldEncodingNotMinimal(MalformedMsg): pass
|
||||
class UnknownMandatoryTLVRecordType(MalformedMsg): pass
|
||||
class MsgTrailingGarbage(MalformedMsg): pass
|
||||
class MsgInvalidFieldOrder(MalformedMsg): pass
|
||||
class UnexpectedFieldSizeForEncoder(MalformedMsg): pass
|
||||
|
||||
Returns evaluated result as int
|
||||
"""
|
||||
|
||||
def _num_remaining_bytes_to_read(fd: io.BytesIO) -> int:
|
||||
cur_pos = fd.tell()
|
||||
end_pos = fd.seek(0, io.SEEK_END)
|
||||
fd.seek(cur_pos)
|
||||
return end_pos - cur_pos
|
||||
|
||||
|
||||
def _assert_can_read_at_least_n_bytes(fd: io.BytesIO, n: int) -> None:
|
||||
# note: it's faster to read n bytes and then check if we read n, than
|
||||
# to assert we can read at least n and then read n bytes.
|
||||
nremaining = _num_remaining_bytes_to_read(fd)
|
||||
if nremaining < n:
|
||||
raise UnexpectedEndOfStream(f"wants to read {n} bytes but only {nremaining} bytes left")
|
||||
|
||||
|
||||
def write_bigsize_int(i: int) -> bytes:
|
||||
assert i >= 0, i
|
||||
if i < 0xfd:
|
||||
return int.to_bytes(i, length=1, byteorder="big", signed=False)
|
||||
elif i < 0x1_0000:
|
||||
return b"\xfd" + int.to_bytes(i, length=2, byteorder="big", signed=False)
|
||||
elif i < 0x1_0000_0000:
|
||||
return b"\xfe" + int.to_bytes(i, length=4, byteorder="big", signed=False)
|
||||
else:
|
||||
return b"\xff" + int.to_bytes(i, length=8, byteorder="big", signed=False)
|
||||
|
||||
|
||||
def read_bigsize_int(fd: io.BytesIO) -> Optional[int]:
|
||||
try:
|
||||
x = int(x)
|
||||
except ValueError:
|
||||
x = ma[x]
|
||||
try:
|
||||
x = int(x)
|
||||
except ValueError:
|
||||
x = int.from_bytes(x, byteorder='big')
|
||||
return x
|
||||
first = fd.read(1)[0]
|
||||
except IndexError:
|
||||
return None # end of file
|
||||
if first < 0xfd:
|
||||
return first
|
||||
elif first == 0xfd:
|
||||
buf = fd.read(2)
|
||||
if len(buf) != 2:
|
||||
raise UnexpectedEndOfStream()
|
||||
val = int.from_bytes(buf, byteorder="big", signed=False)
|
||||
if not (0xfd <= val < 0x1_0000):
|
||||
raise FieldEncodingNotMinimal()
|
||||
return val
|
||||
elif first == 0xfe:
|
||||
buf = fd.read(4)
|
||||
if len(buf) != 4:
|
||||
raise UnexpectedEndOfStream()
|
||||
val = int.from_bytes(buf, byteorder="big", signed=False)
|
||||
if not (0x1_0000 <= val < 0x1_0000_0000):
|
||||
raise FieldEncodingNotMinimal()
|
||||
return val
|
||||
elif first == 0xff:
|
||||
buf = fd.read(8)
|
||||
if len(buf) != 8:
|
||||
raise UnexpectedEndOfStream()
|
||||
val = int.from_bytes(buf, byteorder="big", signed=False)
|
||||
if not (0x1_0000_0000 <= val):
|
||||
raise FieldEncodingNotMinimal()
|
||||
return val
|
||||
raise Exception()
|
||||
|
||||
def _eval_exp_with_ctx(exp, ctx: dict) -> int:
|
||||
|
||||
# TODO: maybe if field_type is not "byte", we could return a list of type_len sized chunks?
|
||||
# if field_type is a numeric, we could return a list of ints?
|
||||
def _read_field(*, fd: io.BytesIO, field_type: str, count: Union[int, str]) -> Union[bytes, int]:
|
||||
if not fd: raise Exception()
|
||||
if isinstance(count, int):
|
||||
assert count >= 0, f"{count!r} must be non-neg int"
|
||||
elif count == "...":
|
||||
pass
|
||||
else:
|
||||
raise Exception(f"unexpected field count: {count!r}")
|
||||
if count == 0:
|
||||
return b""
|
||||
type_len = None
|
||||
if field_type == 'byte':
|
||||
type_len = 1
|
||||
elif field_type in ('u8', 'u16', 'u32', 'u64'):
|
||||
if field_type == 'u8':
|
||||
type_len = 1
|
||||
elif field_type == 'u16':
|
||||
type_len = 2
|
||||
elif field_type == 'u32':
|
||||
type_len = 4
|
||||
else:
|
||||
assert field_type == 'u64'
|
||||
type_len = 8
|
||||
assert count == 1, count
|
||||
buf = fd.read(type_len)
|
||||
if len(buf) != type_len:
|
||||
raise UnexpectedEndOfStream()
|
||||
return int.from_bytes(buf, byteorder="big", signed=False)
|
||||
elif field_type in ('tu16', 'tu32', 'tu64'):
|
||||
if field_type == 'tu16':
|
||||
type_len = 2
|
||||
elif field_type == 'tu32':
|
||||
type_len = 4
|
||||
else:
|
||||
assert field_type == 'tu64'
|
||||
type_len = 8
|
||||
assert count == 1, count
|
||||
raw = fd.read(type_len)
|
||||
if len(raw) > 0 and raw[0] == 0x00:
|
||||
raise FieldEncodingNotMinimal()
|
||||
return int.from_bytes(raw, byteorder="big", signed=False)
|
||||
elif field_type == 'varint':
|
||||
assert count == 1, count
|
||||
val = read_bigsize_int(fd)
|
||||
if val is None:
|
||||
raise UnexpectedEndOfStream()
|
||||
return val
|
||||
elif field_type == 'chain_hash':
|
||||
type_len = 32
|
||||
elif field_type == 'channel_id':
|
||||
type_len = 32
|
||||
elif field_type == 'sha256':
|
||||
type_len = 32
|
||||
elif field_type == 'signature':
|
||||
type_len = 64
|
||||
elif field_type == 'point':
|
||||
type_len = 33
|
||||
elif field_type == 'short_channel_id':
|
||||
type_len = 8
|
||||
|
||||
if count == "...":
|
||||
total_len = -1 # read all
|
||||
else:
|
||||
if type_len is None:
|
||||
raise UnknownMsgFieldType(f"unknown field type: {field_type!r}")
|
||||
total_len = count * type_len
|
||||
|
||||
buf = fd.read(total_len)
|
||||
if total_len >= 0 and len(buf) != total_len:
|
||||
raise UnexpectedEndOfStream()
|
||||
return buf
|
||||
|
||||
|
||||
# TODO: maybe for "value" we could accept a list with len "count" of appropriate items
|
||||
def _write_field(*, fd: io.BytesIO, field_type: str, count: Union[int, str],
|
||||
value: Union[bytes, int]) -> None:
|
||||
if not fd: raise Exception()
|
||||
if isinstance(count, int):
|
||||
assert count >= 0, f"{count!r} must be non-neg int"
|
||||
elif count == "...":
|
||||
pass
|
||||
else:
|
||||
raise Exception(f"unexpected field count: {count!r}")
|
||||
if count == 0:
|
||||
return
|
||||
type_len = None
|
||||
if field_type == 'byte':
|
||||
type_len = 1
|
||||
elif field_type == 'u8':
|
||||
type_len = 1
|
||||
elif field_type == 'u16':
|
||||
type_len = 2
|
||||
elif field_type == 'u32':
|
||||
type_len = 4
|
||||
elif field_type == 'u64':
|
||||
type_len = 8
|
||||
elif field_type in ('tu16', 'tu32', 'tu64'):
|
||||
if field_type == 'tu16':
|
||||
type_len = 2
|
||||
elif field_type == 'tu32':
|
||||
type_len = 4
|
||||
else:
|
||||
assert field_type == 'tu64'
|
||||
type_len = 8
|
||||
assert count == 1, count
|
||||
if isinstance(value, int):
|
||||
value = int.to_bytes(value, length=type_len, byteorder="big", signed=False)
|
||||
if not isinstance(value, (bytes, bytearray)):
|
||||
raise Exception(f"can only write bytes into fd. got: {value!r}")
|
||||
while len(value) > 0 and value[0] == 0x00:
|
||||
value = value[1:]
|
||||
nbytes_written = fd.write(value)
|
||||
if nbytes_written != len(value):
|
||||
raise Exception(f"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?")
|
||||
return
|
||||
elif field_type == 'varint':
|
||||
assert count == 1, count
|
||||
if isinstance(value, int):
|
||||
value = write_bigsize_int(value)
|
||||
if not isinstance(value, (bytes, bytearray)):
|
||||
raise Exception(f"can only write bytes into fd. got: {value!r}")
|
||||
nbytes_written = fd.write(value)
|
||||
if nbytes_written != len(value):
|
||||
raise Exception(f"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?")
|
||||
return
|
||||
elif field_type == 'chain_hash':
|
||||
type_len = 32
|
||||
elif field_type == 'channel_id':
|
||||
type_len = 32
|
||||
elif field_type == 'sha256':
|
||||
type_len = 32
|
||||
elif field_type == 'signature':
|
||||
type_len = 64
|
||||
elif field_type == 'point':
|
||||
type_len = 33
|
||||
elif field_type == 'short_channel_id':
|
||||
type_len = 8
|
||||
total_len = -1
|
||||
if count != "...":
|
||||
if type_len is None:
|
||||
raise UnknownMsgFieldType(f"unknown field type: {field_type!r}")
|
||||
total_len = count * type_len
|
||||
if isinstance(value, int) and (count == 1 or field_type == 'byte'):
|
||||
value = int.to_bytes(value, length=total_len, byteorder="big", signed=False)
|
||||
if not isinstance(value, (bytes, bytearray)):
|
||||
raise Exception(f"can only write bytes into fd. got: {value!r}")
|
||||
if count != "..." and total_len != len(value):
|
||||
raise UnexpectedFieldSizeForEncoder(f"expected: {total_len}, got {len(value)}")
|
||||
nbytes_written = fd.write(value)
|
||||
if nbytes_written != len(value):
|
||||
raise Exception(f"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?")
|
||||
|
||||
|
||||
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=="...".
|
||||
"""
|
||||
Evaluate simple mathematical expression given
|
||||
in `exp` with context (variables assigned)
|
||||
from the dict `ctx`.
|
||||
if field_count_str == "":
|
||||
field_count = 1
|
||||
elif field_count_str == "...":
|
||||
if not allow_any:
|
||||
raise Exception("field count is '...' but allow_any is False")
|
||||
return field_count_str
|
||||
else:
|
||||
try:
|
||||
field_count = int(field_count_str)
|
||||
except ValueError:
|
||||
field_count = vars_dict[field_count_str]
|
||||
if isinstance(field_count, (bytes, bytearray)):
|
||||
field_count = int.from_bytes(field_count, byteorder="big")
|
||||
assert isinstance(field_count, int)
|
||||
return field_count
|
||||
|
||||
Returns evaluated result as int
|
||||
"""
|
||||
exp = str(exp)
|
||||
if "*" in exp:
|
||||
assert "+" not in exp
|
||||
result = 1
|
||||
for term in exp.split("*"):
|
||||
result *= _eval_length_term(term, ctx)
|
||||
return result
|
||||
return sum(_eval_length_term(x, ctx) for x in exp.split("+"))
|
||||
|
||||
def _make_handler(msg_name: str, v: dict) -> Callable[[bytes], Tuple[str, dict]]:
|
||||
"""
|
||||
Generate a message handler function (taking bytes)
|
||||
for message type `msg_name` with specification `v`
|
||||
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
|
||||
|
||||
Check lib/lightning.json, `msg_name` could be 'init',
|
||||
and `v` could be
|
||||
|
||||
{ type: 16, payload: { 'gflen': ..., ... }, ... }
|
||||
|
||||
Returns function taking bytes
|
||||
"""
|
||||
def handler(data: bytes) -> Tuple[str, dict]:
|
||||
ma = {} # map of field name -> field data; after parsing msg
|
||||
pos = 0
|
||||
for fieldname in v["payload"]:
|
||||
poslenMap = v["payload"][fieldname]
|
||||
if "feature" in poslenMap and pos == len(data):
|
||||
continue
|
||||
#assert pos == _eval_exp_with_ctx(poslenMap["position"], ma) # this assert is expensive...
|
||||
length = poslenMap["length"]
|
||||
length = _eval_exp_with_ctx(length, ma)
|
||||
ma[fieldname] = data[pos:pos+length]
|
||||
pos += length
|
||||
# BOLT-01: "MUST ignore any additional data within a message beyond the length that it expects for that type."
|
||||
assert pos <= len(data), (msg_name, pos, len(data))
|
||||
return msg_name, ma
|
||||
return handler
|
||||
|
||||
class LNSerializer:
|
||||
def __init__(self):
|
||||
message_types = {}
|
||||
path = os.path.join(os.path.dirname(__file__), 'lightning.json')
|
||||
with open(path) as f:
|
||||
structured = json.loads(f.read(), object_pairs_hook=OrderedDict)
|
||||
|
||||
for msg_name in structured:
|
||||
v = structured[msg_name]
|
||||
# these message types are skipped since their types collide
|
||||
# (for example with pong, which also uses type=19)
|
||||
# we don't need them yet
|
||||
if msg_name in ["final_incorrect_cltv_expiry", "final_incorrect_htlc_amount"]:
|
||||
continue
|
||||
if len(v["payload"]) == 0:
|
||||
def __init__(self, *, for_onion_wire: bool = False):
|
||||
# TODO msg_type could be 'int' everywhere...
|
||||
self.msg_scheme_from_type = {} # type: Dict[bytes, List[Sequence[str]]]
|
||||
self.msg_type_from_name = {} # type: Dict[str, bytes]
|
||||
|
||||
self.in_tlv_stream_get_tlv_record_scheme_from_type = {} # type: Dict[str, Dict[int, List[Sequence[str]]]]
|
||||
self.in_tlv_stream_get_record_type_from_name = {} # type: Dict[str, Dict[str, int]]
|
||||
self.in_tlv_stream_get_record_name_from_type = {} # type: Dict[str, Dict[int, str]]
|
||||
|
||||
if for_onion_wire:
|
||||
path = os.path.join(os.path.dirname(__file__), "lnwire", "onion_wire.csv")
|
||||
else:
|
||||
path = os.path.join(os.path.dirname(__file__), "lnwire", "peer_wire.csv")
|
||||
with open(path, newline='') as f:
|
||||
csvreader = csv.reader(f)
|
||||
for row in csvreader:
|
||||
#print(f">>> {row!r}")
|
||||
if row[0] == "msgtype":
|
||||
# msgtype,<msgname>,<value>[,<option>]
|
||||
msg_type_name = row[1]
|
||||
if for_onion_wire:
|
||||
msg_type_int = _parse_msgtype_intvalue_for_onion_wire(str(row[2]))
|
||||
else:
|
||||
msg_type_int = int(row[2])
|
||||
msg_type_bytes = msg_type_int.to_bytes(2, 'big')
|
||||
assert msg_type_bytes not in self.msg_scheme_from_type, f"type collision? for {msg_type_name}"
|
||||
assert msg_type_name not in self.msg_type_from_name, f"type collision? for {msg_type_name}"
|
||||
row[2] = msg_type_int
|
||||
self.msg_scheme_from_type[msg_type_bytes] = [tuple(row)]
|
||||
self.msg_type_from_name[msg_type_name] = msg_type_bytes
|
||||
elif row[0] == "msgdata":
|
||||
# msgdata,<msgname>,<fieldname>,<typename>,[<count>][,<option>]
|
||||
assert msg_type_name == row[1]
|
||||
self.msg_scheme_from_type[msg_type_bytes].append(tuple(row))
|
||||
elif row[0] == "tlvtype":
|
||||
# tlvtype,<tlvstreamname>,<tlvname>,<value>[,<option>]
|
||||
tlv_stream_name = row[1]
|
||||
tlv_record_name = row[2]
|
||||
tlv_record_type = int(row[3])
|
||||
row[3] = tlv_record_type
|
||||
if tlv_stream_name not in self.in_tlv_stream_get_tlv_record_scheme_from_type:
|
||||
self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name] = OrderedDict()
|
||||
self.in_tlv_stream_get_record_type_from_name[tlv_stream_name] = {}
|
||||
self.in_tlv_stream_get_record_name_from_type[tlv_stream_name] = {}
|
||||
assert tlv_record_type not in self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name], f"type collision? for {tlv_stream_name}/{tlv_record_name}"
|
||||
assert tlv_record_name not in self.in_tlv_stream_get_record_type_from_name[tlv_stream_name], f"type collision? for {tlv_stream_name}/{tlv_record_name}"
|
||||
assert tlv_record_type not in self.in_tlv_stream_get_record_type_from_name[tlv_stream_name], f"type collision? for {tlv_stream_name}/{tlv_record_name}"
|
||||
self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name][tlv_record_type] = [tuple(row)]
|
||||
self.in_tlv_stream_get_record_type_from_name[tlv_stream_name][tlv_record_name] = tlv_record_type
|
||||
self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type] = tlv_record_name
|
||||
if max(self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name].keys()) > tlv_record_type:
|
||||
raise Exception(f"tlv record types must be listed in monotonically increasing order for stream. "
|
||||
f"stream={tlv_stream_name}")
|
||||
elif row[0] == "tlvdata":
|
||||
# tlvdata,<tlvstreamname>,<tlvname>,<fieldname>,<typename>,[<count>][,<option>]
|
||||
assert tlv_stream_name == row[1]
|
||||
assert tlv_record_name == row[2]
|
||||
self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name][tlv_record_type].append(tuple(row))
|
||||
else:
|
||||
pass # TODO
|
||||
|
||||
def write_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str, **kwargs) -> None:
|
||||
scheme_map = self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name]
|
||||
for tlv_record_type, scheme in scheme_map.items(): # note: tlv_record_type is monotonically increasing
|
||||
tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type]
|
||||
if tlv_record_name not in kwargs:
|
||||
continue
|
||||
with io.BytesIO() as tlv_record_fd:
|
||||
for row in scheme:
|
||||
if row[0] == "tlvtype":
|
||||
pass
|
||||
elif row[0] == "tlvdata":
|
||||
# tlvdata,<tlvstreamname>,<tlvname>,<fieldname>,<typename>,[<count>][,<option>]
|
||||
assert tlv_stream_name == row[1]
|
||||
assert tlv_record_name == row[2]
|
||||
field_name = row[3]
|
||||
field_type = row[4]
|
||||
field_count_str = row[5]
|
||||
field_count = _resolve_field_count(field_count_str,
|
||||
vars_dict=kwargs[tlv_record_name],
|
||||
allow_any=True)
|
||||
field_value = kwargs[tlv_record_name][field_name]
|
||||
_write_field(fd=tlv_record_fd,
|
||||
field_type=field_type,
|
||||
count=field_count,
|
||||
value=field_value)
|
||||
else:
|
||||
raise Exception(f"unexpected row in scheme: {row!r}")
|
||||
_write_tlv_record(fd=fd, tlv_type=tlv_record_type, tlv_val=tlv_record_fd.getvalue())
|
||||
|
||||
def read_tlv_stream(self, *, fd: io.BytesIO, tlv_stream_name: str) -> Dict[str, Dict[str, Any]]:
|
||||
parsed = {} # type: Dict[str, Dict[str, Any]]
|
||||
scheme_map = self.in_tlv_stream_get_tlv_record_scheme_from_type[tlv_stream_name]
|
||||
last_seen_tlv_record_type = -1 # type: int
|
||||
while _num_remaining_bytes_to_read(fd) > 0:
|
||||
tlv_record_type, tlv_record_val = _read_tlv_record(fd=fd)
|
||||
if not (tlv_record_type > last_seen_tlv_record_type):
|
||||
raise MsgInvalidFieldOrder(f"TLV records must be monotonically increasing by type. "
|
||||
f"cur: {tlv_record_type}. prev: {last_seen_tlv_record_type}")
|
||||
last_seen_tlv_record_type = tlv_record_type
|
||||
try:
|
||||
num = int(v["type"])
|
||||
except ValueError:
|
||||
#print("skipping", k)
|
||||
continue
|
||||
byts = num.to_bytes(2, 'big')
|
||||
assert byts not in message_types, (byts, message_types[byts].__name__, msg_name)
|
||||
names = [x.__name__ for x in message_types.values()]
|
||||
assert msg_name + "_handler" not in names, (msg_name, names)
|
||||
message_types[byts] = _make_handler(msg_name, v)
|
||||
message_types[byts].__name__ = msg_name + "_handler"
|
||||
scheme = scheme_map[tlv_record_type]
|
||||
except KeyError:
|
||||
if tlv_record_type % 2 == 0:
|
||||
# unknown "even" type: hard fail
|
||||
raise UnknownMandatoryTLVRecordType(f"{tlv_stream_name}/{tlv_record_type}") from None
|
||||
else:
|
||||
# unknown "odd" type: skip it
|
||||
continue
|
||||
tlv_record_name = self.in_tlv_stream_get_record_name_from_type[tlv_stream_name][tlv_record_type]
|
||||
parsed[tlv_record_name] = {}
|
||||
with io.BytesIO(tlv_record_val) as tlv_record_fd:
|
||||
for row in scheme:
|
||||
#print(f"row: {row!r}")
|
||||
if row[0] == "tlvtype":
|
||||
pass
|
||||
elif row[0] == "tlvdata":
|
||||
# tlvdata,<tlvstreamname>,<tlvname>,<fieldname>,<typename>,[<count>][,<option>]
|
||||
assert tlv_stream_name == row[1]
|
||||
assert tlv_record_name == row[2]
|
||||
field_name = row[3]
|
||||
field_type = row[4]
|
||||
field_count_str = row[5]
|
||||
field_count = _resolve_field_count(field_count_str,
|
||||
vars_dict=parsed[tlv_record_name],
|
||||
allow_any=True)
|
||||
#print(f">> count={field_count}. parsed={parsed}")
|
||||
parsed[tlv_record_name][field_name] = _read_field(fd=tlv_record_fd,
|
||||
field_type=field_type,
|
||||
count=field_count)
|
||||
else:
|
||||
raise Exception(f"unexpected row in scheme: {row!r}")
|
||||
if _num_remaining_bytes_to_read(tlv_record_fd) > 0:
|
||||
raise MsgTrailingGarbage(f"TLV record ({tlv_stream_name}/{tlv_record_name}) has extra trailing garbage")
|
||||
return parsed
|
||||
|
||||
assert message_types[b"\x00\x10"].__name__ == "init_handler"
|
||||
self.structured = structured
|
||||
self.message_types = message_types
|
||||
|
||||
def encode_msg(self, msg_type : str, **kwargs) -> bytes:
|
||||
def encode_msg(self, msg_type: str, **kwargs) -> bytes:
|
||||
"""
|
||||
Encode kwargs into a Lightning message (bytes)
|
||||
of the type given in the msg_type string
|
||||
"""
|
||||
typ = self.structured[msg_type]
|
||||
data = int(typ["type"]).to_bytes(2, 'big')
|
||||
lengths = {}
|
||||
for k in typ["payload"]:
|
||||
poslenMap = typ["payload"][k]
|
||||
if k not in kwargs and "feature" in poslenMap:
|
||||
continue
|
||||
param = kwargs.get(k, 0)
|
||||
leng = _eval_exp_with_ctx(poslenMap["length"], lengths)
|
||||
try:
|
||||
clone = dict(lengths)
|
||||
clone.update(kwargs)
|
||||
leng = _eval_exp_with_ctx(poslenMap["length"], clone)
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
if not isinstance(param, bytes):
|
||||
assert isinstance(param, int), "field {} is neither bytes or int".format(k)
|
||||
param = param.to_bytes(leng, 'big')
|
||||
except ValueError:
|
||||
raise Exception("{} does not fit in {} bytes".format(k, leng))
|
||||
lengths[k] = len(param)
|
||||
if lengths[k] != leng:
|
||||
raise Exception("field {} is {} bytes long, should be {} bytes long".format(k, lengths[k], leng))
|
||||
data += param
|
||||
return data
|
||||
#print(f">>> encode_msg. msg_type={msg_type}, payload={kwargs!r}")
|
||||
msg_type_bytes = self.msg_type_from_name[msg_type]
|
||||
scheme = self.msg_scheme_from_type[msg_type_bytes]
|
||||
with io.BytesIO() as fd:
|
||||
fd.write(msg_type_bytes)
|
||||
for row in scheme:
|
||||
if row[0] == "msgtype":
|
||||
pass
|
||||
elif row[0] == "msgdata":
|
||||
# msgdata,<msgname>,<fieldname>,<typename>,[<count>][,<option>]
|
||||
field_name = row[2]
|
||||
field_type = row[3]
|
||||
field_count_str = row[4]
|
||||
#print(f">>> encode_msg. msgdata. field_name={field_name!r}. field_type={field_type!r}. field_count_str={field_count_str!r}")
|
||||
field_count = _resolve_field_count(field_count_str, vars_dict=kwargs)
|
||||
if field_name == "tlvs":
|
||||
tlv_stream_name = field_type
|
||||
if tlv_stream_name in kwargs:
|
||||
self.write_tlv_stream(fd=fd, tlv_stream_name=tlv_stream_name, **(kwargs[tlv_stream_name]))
|
||||
continue
|
||||
try:
|
||||
field_value = kwargs[field_name]
|
||||
except KeyError:
|
||||
if len(row) > 5:
|
||||
break # optional feature field not present
|
||||
else:
|
||||
field_value = 0 # default mandatory fields to zero
|
||||
#print(f">>> encode_msg. writing field: {field_name}. value={field_value!r}. field_type={field_type!r}. count={field_count!r}")
|
||||
_write_field(fd=fd,
|
||||
field_type=field_type,
|
||||
count=field_count,
|
||||
value=field_value)
|
||||
#print(f">>> encode_msg. so far: {fd.getvalue().hex()}")
|
||||
else:
|
||||
raise Exception(f"unexpected row in scheme: {row!r}")
|
||||
return fd.getvalue()
|
||||
|
||||
def decode_msg(self, data : bytes) -> Tuple[str, dict]:
|
||||
def decode_msg(self, data: bytes) -> Tuple[str, dict]:
|
||||
"""
|
||||
Decode Lightning message by reading the first
|
||||
two bytes to determine message type.
|
||||
|
||||
Returns message type string and parsed message contents dict
|
||||
"""
|
||||
typ = data[:2]
|
||||
k, parsed = self.message_types[typ](data[2:])
|
||||
return k, parsed
|
||||
#print(f"decode_msg >>> {data.hex()}")
|
||||
assert len(data) >= 2
|
||||
msg_type_bytes = data[:2]
|
||||
msg_type_int = int.from_bytes(msg_type_bytes, byteorder="big", signed=False)
|
||||
scheme = self.msg_scheme_from_type[msg_type_bytes]
|
||||
assert scheme[0][2] == msg_type_int
|
||||
msg_type_name = scheme[0][1]
|
||||
parsed = {}
|
||||
with io.BytesIO(data[2:]) as fd:
|
||||
for row in scheme:
|
||||
#print(f"row: {row!r}")
|
||||
if row[0] == "msgtype":
|
||||
pass
|
||||
elif row[0] == "msgdata":
|
||||
field_name = row[2]
|
||||
field_type = row[3]
|
||||
field_count_str = row[4]
|
||||
field_count = _resolve_field_count(field_count_str, vars_dict=parsed)
|
||||
if field_name == "tlvs":
|
||||
tlv_stream_name = field_type
|
||||
d = self.read_tlv_stream(fd=fd, tlv_stream_name=tlv_stream_name)
|
||||
parsed[tlv_stream_name] = d
|
||||
continue
|
||||
#print(f">> count={field_count}. parsed={parsed}")
|
||||
try:
|
||||
parsed[field_name] = _read_field(fd=fd,
|
||||
field_type=field_type,
|
||||
count=field_count)
|
||||
except UnexpectedEndOfStream as e:
|
||||
if len(row) > 5:
|
||||
break # optional feature field not present
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
raise Exception(f"unexpected row in scheme: {row!r}")
|
||||
return msg_type_name, parsed
|
||||
|
||||
|
||||
_inst = LNSerializer()
|
||||
encode_msg = _inst.encode_msg
|
||||
decode_msg = _inst.decode_msg
|
||||
|
||||
|
||||
OnionWireSerializer = LNSerializer(for_onion_wire=True)
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import io
|
||||
import hashlib
|
||||
from typing import Sequence, List, Tuple, NamedTuple, TYPE_CHECKING
|
||||
from enum import IntEnum, IntFlag
|
||||
|
@ -31,15 +32,16 @@ from . import ecc
|
|||
from .crypto import sha256, hmac_oneshot, chacha20_encrypt
|
||||
from .util import bh2u, profiler, xor_bytes, bfh
|
||||
from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH,
|
||||
NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID)
|
||||
NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, OnionFailureCodeMetaFlag)
|
||||
from .lnmsg import OnionWireSerializer, read_bigsize_int, write_bigsize_int
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .lnrouter import LNPaymentRoute
|
||||
|
||||
|
||||
HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04
|
||||
PER_HOP_FULL_SIZE = 65 # HOPS_DATA_SIZE / 20
|
||||
NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE
|
||||
LEGACY_PER_HOP_FULL_SIZE = 65
|
||||
NUM_STREAM_BYTES = 2 * HOPS_DATA_SIZE
|
||||
PER_HOP_HMAC_SIZE = 32
|
||||
|
||||
|
||||
|
@ -48,64 +50,127 @@ class InvalidOnionMac(Exception): pass
|
|||
class InvalidOnionPubkey(Exception): pass
|
||||
|
||||
|
||||
class OnionPerHop:
|
||||
class LegacyHopDataPayload:
|
||||
|
||||
def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes):
|
||||
def __init__(self, *, short_channel_id: bytes, amt_to_forward: int, outgoing_cltv_value: int):
|
||||
self.short_channel_id = ShortChannelID(short_channel_id)
|
||||
self.amt_to_forward = amt_to_forward
|
||||
self.outgoing_cltv_value = outgoing_cltv_value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
ret = self.short_channel_id
|
||||
ret += self.amt_to_forward
|
||||
ret += self.outgoing_cltv_value
|
||||
ret += int.to_bytes(self.amt_to_forward, length=8, byteorder="big", signed=False)
|
||||
ret += int.to_bytes(self.outgoing_cltv_value, length=4, byteorder="big", signed=False)
|
||||
ret += bytes(12) # padding
|
||||
if len(ret) != 32:
|
||||
raise Exception('unexpected length {}'.format(len(ret)))
|
||||
return ret
|
||||
|
||||
def to_tlv_dict(self) -> dict:
|
||||
d = {
|
||||
"amt_to_forward": {"amt_to_forward": self.amt_to_forward},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": self.outgoing_cltv_value},
|
||||
"short_channel_id": {"short_channel_id": self.short_channel_id},
|
||||
}
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, b: bytes):
|
||||
def from_bytes(cls, b: bytes) -> 'LegacyHopDataPayload':
|
||||
if len(b) != 32:
|
||||
raise Exception('unexpected length {}'.format(len(b)))
|
||||
return OnionPerHop(
|
||||
return LegacyHopDataPayload(
|
||||
short_channel_id=b[:8],
|
||||
amt_to_forward=b[8:16],
|
||||
outgoing_cltv_value=b[16:20]
|
||||
amt_to_forward=int.from_bytes(b[8:16], byteorder="big", signed=False),
|
||||
outgoing_cltv_value=int.from_bytes(b[16:20], byteorder="big", signed=False),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_tlv_dict(cls, d: dict) -> 'LegacyHopDataPayload':
|
||||
return LegacyHopDataPayload(
|
||||
short_channel_id=d["short_channel_id"]["short_channel_id"] if "short_channel_id" in d else b"\x00" * 8,
|
||||
amt_to_forward=d["amt_to_forward"]["amt_to_forward"],
|
||||
outgoing_cltv_value=d["outgoing_cltv_value"]["outgoing_cltv_value"],
|
||||
)
|
||||
|
||||
|
||||
class OnionHopsDataSingle: # called HopData in lnd
|
||||
|
||||
def __init__(self, per_hop: OnionPerHop = None):
|
||||
self.realm = 0
|
||||
self.per_hop = per_hop
|
||||
def __init__(self, *, is_tlv_payload: bool, payload: dict = None):
|
||||
self.is_tlv_payload = is_tlv_payload
|
||||
if payload is None:
|
||||
payload = {}
|
||||
self.payload = payload
|
||||
self.hmac = None
|
||||
self._raw_bytes_payload = None # used in unit tests
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
ret = bytes([self.realm])
|
||||
ret += self.per_hop.to_bytes()
|
||||
ret += self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE)
|
||||
if len(ret) != PER_HOP_FULL_SIZE:
|
||||
raise Exception('unexpected length {}'.format(len(ret)))
|
||||
return ret
|
||||
hmac_ = self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE)
|
||||
if self._raw_bytes_payload is not None:
|
||||
ret = write_bigsize_int(len(self._raw_bytes_payload))
|
||||
ret += self._raw_bytes_payload
|
||||
ret += hmac_
|
||||
return ret
|
||||
if not self.is_tlv_payload:
|
||||
ret = b"\x00" # realm==0
|
||||
legacy_payload = LegacyHopDataPayload.from_tlv_dict(self.payload)
|
||||
ret += legacy_payload.to_bytes()
|
||||
ret += hmac_
|
||||
if len(ret) != LEGACY_PER_HOP_FULL_SIZE:
|
||||
raise Exception('unexpected length {}'.format(len(ret)))
|
||||
return ret
|
||||
else: # tlv
|
||||
payload_fd = io.BytesIO()
|
||||
OnionWireSerializer.write_tlv_stream(fd=payload_fd,
|
||||
tlv_stream_name="tlv_payload",
|
||||
**self.payload)
|
||||
payload_bytes = payload_fd.getvalue()
|
||||
with io.BytesIO() as fd:
|
||||
fd.write(write_bigsize_int(len(payload_bytes)))
|
||||
fd.write(payload_bytes)
|
||||
fd.write(hmac_)
|
||||
return fd.getvalue()
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, b: bytes):
|
||||
if len(b) != PER_HOP_FULL_SIZE:
|
||||
raise Exception('unexpected length {}'.format(len(b)))
|
||||
ret = OnionHopsDataSingle()
|
||||
ret.realm = b[0]
|
||||
if ret.realm != 0:
|
||||
raise Exception('only realm 0 is supported')
|
||||
ret.per_hop = OnionPerHop.from_bytes(b[1:33])
|
||||
ret.hmac = b[33:]
|
||||
return ret
|
||||
def from_fd(cls, fd: io.BytesIO) -> 'OnionHopsDataSingle':
|
||||
first_byte = fd.read(1)
|
||||
if len(first_byte) == 0:
|
||||
raise Exception(f"unexpected EOF")
|
||||
fd.seek(-1, io.SEEK_CUR) # undo read
|
||||
if first_byte == b'\x00':
|
||||
# legacy hop data format
|
||||
b = fd.read(LEGACY_PER_HOP_FULL_SIZE)
|
||||
if len(b) != LEGACY_PER_HOP_FULL_SIZE:
|
||||
raise Exception(f'unexpected length {len(b)}')
|
||||
ret = OnionHopsDataSingle(is_tlv_payload=False)
|
||||
legacy_payload = LegacyHopDataPayload.from_bytes(b[1:33])
|
||||
ret.payload = legacy_payload.to_tlv_dict()
|
||||
ret.hmac = b[33:]
|
||||
return ret
|
||||
elif first_byte == b'\x01':
|
||||
# reserved for future use
|
||||
raise Exception("unsupported hop payload: length==1")
|
||||
else:
|
||||
hop_payload_length = read_bigsize_int(fd)
|
||||
hop_payload = fd.read(hop_payload_length)
|
||||
if hop_payload_length != len(hop_payload):
|
||||
raise Exception(f"unexpected EOF")
|
||||
ret = OnionHopsDataSingle(is_tlv_payload=True)
|
||||
ret.payload = OnionWireSerializer.read_tlv_stream(fd=io.BytesIO(hop_payload),
|
||||
tlv_stream_name="tlv_payload")
|
||||
ret.hmac = fd.read(PER_HOP_HMAC_SIZE)
|
||||
assert len(ret.hmac) == PER_HOP_HMAC_SIZE
|
||||
return ret
|
||||
|
||||
def __repr__(self):
|
||||
return f"<OnionHopsDataSingle. is_tlv_payload={self.is_tlv_payload}. payload={self.payload}. hmac={self.hmac}>"
|
||||
|
||||
|
||||
class OnionPacket:
|
||||
|
||||
def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes):
|
||||
assert len(public_key) == 33
|
||||
assert len(hops_data) == HOPS_DATA_SIZE
|
||||
assert len(hmac) == PER_HOP_HMAC_SIZE
|
||||
self.version = 0
|
||||
self.public_key = public_key
|
||||
self.hops_data = hops_data # also called RoutingInfo in bolt-04
|
||||
|
@ -163,13 +228,14 @@ def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes],
|
|||
def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
|
||||
hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket:
|
||||
num_hops = len(payment_path_pubkeys)
|
||||
assert num_hops == len(hops_data)
|
||||
hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
|
||||
|
||||
filler = generate_filler(b'rho', num_hops, PER_HOP_FULL_SIZE, hop_shared_secrets)
|
||||
filler = _generate_filler(b'rho', hops_data, hop_shared_secrets)
|
||||
next_hmac = bytes(PER_HOP_HMAC_SIZE)
|
||||
|
||||
# Our starting packet needs to be filled out with random bytes, we
|
||||
# generate some determinstically using the session private key.
|
||||
# generate some deterministically using the session private key.
|
||||
pad_key = get_bolt04_onion_key(b'pad', session_key)
|
||||
mix_header = generate_cipher_stream(pad_key, HOPS_DATA_SIZE)
|
||||
|
||||
|
@ -178,9 +244,10 @@ def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
|
|||
rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])
|
||||
mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i])
|
||||
hops_data[i].hmac = next_hmac
|
||||
stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES)
|
||||
mix_header = mix_header[:-PER_HOP_FULL_SIZE]
|
||||
mix_header = hops_data[i].to_bytes() + mix_header
|
||||
stream_bytes = generate_cipher_stream(rho_key, HOPS_DATA_SIZE)
|
||||
hop_data_bytes = hops_data[i].to_bytes()
|
||||
mix_header = mix_header[:-len(hop_data_bytes)]
|
||||
mix_header = hop_data_bytes + mix_header
|
||||
mix_header = xor_bytes(mix_header, stream_bytes)
|
||||
if i == num_hops - 1 and len(filler) != 0:
|
||||
mix_header = mix_header[:-len(filler)] + filler
|
||||
|
@ -193,7 +260,8 @@ def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
|
|||
hmac=next_hmac)
|
||||
|
||||
|
||||
def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int, final_cltv: int) \
|
||||
def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int,
|
||||
final_cltv: int, *, payment_secret: bytes = None) \
|
||||
-> Tuple[List[OnionHopsDataSingle], int, int]:
|
||||
"""Returns the hops_data to be used for constructing an onion packet,
|
||||
and the amount_msat and cltv to be used on our immediate channel.
|
||||
|
@ -201,34 +269,59 @@ def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int, final_
|
|||
if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
|
||||
raise PaymentFailure(f"too long route ({len(route)} edges)")
|
||||
|
||||
# payload that will be seen by the last hop:
|
||||
amt = amount_msat
|
||||
cltv = final_cltv
|
||||
hops_data = [OnionHopsDataSingle(OnionPerHop(b"\x00" * 8,
|
||||
amt.to_bytes(8, "big"),
|
||||
cltv.to_bytes(4, "big")))]
|
||||
for route_edge in reversed(route[1:]):
|
||||
hops_data += [OnionHopsDataSingle(OnionPerHop(route_edge.short_channel_id,
|
||||
amt.to_bytes(8, "big"),
|
||||
cltv.to_bytes(4, "big")))]
|
||||
hop_payload = {
|
||||
"amt_to_forward": {"amt_to_forward": amt},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": cltv},
|
||||
}
|
||||
if payment_secret is not None:
|
||||
hop_payload["payment_data"] = {"payment_secret": payment_secret, "total_msat": amt}
|
||||
hops_data = [OnionHopsDataSingle(is_tlv_payload=route[-1].has_feature_varonion(),
|
||||
payload=hop_payload)]
|
||||
# payloads, backwards from last hop (but excluding the first edge):
|
||||
for edge_index in range(len(route) - 1, 0, -1):
|
||||
route_edge = route[edge_index]
|
||||
hop_payload = {
|
||||
"amt_to_forward": {"amt_to_forward": amt},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": cltv},
|
||||
"short_channel_id": {"short_channel_id": route_edge.short_channel_id},
|
||||
}
|
||||
hops_data += [OnionHopsDataSingle(is_tlv_payload=route[edge_index-1].has_feature_varonion(),
|
||||
payload=hop_payload)]
|
||||
amt += route_edge.fee_for_edge(amt)
|
||||
cltv += route_edge.cltv_expiry_delta
|
||||
hops_data.reverse()
|
||||
return hops_data, amt, cltv
|
||||
|
||||
|
||||
def generate_filler(key_type: bytes, num_hops: int, hop_size: int,
|
||||
shared_secrets: Sequence[bytes]) -> bytes:
|
||||
filler_size = (NUM_MAX_HOPS_IN_PAYMENT_PATH + 1) * hop_size
|
||||
def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],
|
||||
shared_secrets: Sequence[bytes]) -> bytes:
|
||||
num_hops = len(hops_data)
|
||||
|
||||
# generate filler that matches all but the last hop (no HMAC for last hop)
|
||||
filler_size = 0
|
||||
for hop_data in hops_data[:-1]:
|
||||
filler_size += len(hop_data.to_bytes())
|
||||
filler = bytearray(filler_size)
|
||||
|
||||
for i in range(0, num_hops-1): # -1, as last hop does not obfuscate
|
||||
filler = filler[hop_size:]
|
||||
filler += bytearray(hop_size)
|
||||
stream_key = get_bolt04_onion_key(key_type, shared_secrets[i])
|
||||
stream_bytes = generate_cipher_stream(stream_key, filler_size)
|
||||
filler = xor_bytes(filler, stream_bytes)
|
||||
# Sum up how many frames were used by prior hops.
|
||||
filler_start = HOPS_DATA_SIZE
|
||||
for hop_data in hops_data[:i]:
|
||||
filler_start -= len(hop_data.to_bytes())
|
||||
# The filler is the part dangling off of the end of the
|
||||
# routingInfo, so offset it from there, and use the current
|
||||
# hop's frame count as its size.
|
||||
filler_end = HOPS_DATA_SIZE + len(hops_data[i].to_bytes())
|
||||
|
||||
return filler[(NUM_MAX_HOPS_IN_PAYMENT_PATH-num_hops+2)*hop_size:]
|
||||
stream_key = get_bolt04_onion_key(key_type, shared_secrets[i])
|
||||
stream_bytes = generate_cipher_stream(stream_key, NUM_STREAM_BYTES)
|
||||
filler = xor_bytes(filler, stream_bytes[filler_start:filler_end])
|
||||
filler += bytes(filler_size - len(filler)) # right pad with zeroes
|
||||
|
||||
return filler
|
||||
|
||||
|
||||
def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes:
|
||||
|
@ -260,8 +353,9 @@ def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
|
|||
# peel an onion layer off
|
||||
rho_key = get_bolt04_onion_key(b'rho', shared_secret)
|
||||
stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES)
|
||||
padded_header = onion_packet.hops_data + bytes(PER_HOP_FULL_SIZE)
|
||||
padded_header = onion_packet.hops_data + bytes(HOPS_DATA_SIZE)
|
||||
next_hops_data = xor_bytes(padded_header, stream_bytes)
|
||||
next_hops_data_fd = io.BytesIO(next_hops_data)
|
||||
|
||||
# calc next ephemeral key
|
||||
blinding_factor = sha256(onion_packet.public_key + shared_secret)
|
||||
|
@ -269,10 +363,10 @@ def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
|
|||
next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int
|
||||
next_public_key = next_public_key_int.get_public_key_bytes()
|
||||
|
||||
hop_data = OnionHopsDataSingle.from_bytes(next_hops_data[:PER_HOP_FULL_SIZE])
|
||||
hop_data = OnionHopsDataSingle.from_fd(next_hops_data_fd)
|
||||
next_onion_packet = OnionPacket(
|
||||
public_key=next_public_key,
|
||||
hops_data=next_hops_data[PER_HOP_FULL_SIZE:],
|
||||
hops_data=next_hops_data_fd.read(HOPS_DATA_SIZE),
|
||||
hmac=hop_data.hmac
|
||||
)
|
||||
if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):
|
||||
|
@ -365,12 +459,7 @@ def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRout
|
|||
return OnionRoutingFailureMessage(failure_code, failure_data)
|
||||
|
||||
|
||||
class OnionFailureCodeMetaFlag(IntFlag):
|
||||
BADONION = 0x8000
|
||||
PERM = 0x4000
|
||||
NODE = 0x2000
|
||||
UPDATE = 0x1000
|
||||
|
||||
# TODO maybe we should rm this and just use OnionWireSerializer and onion_wire.csv
|
||||
BADONION = OnionFailureCodeMetaFlag.BADONION
|
||||
PERM = OnionFailureCodeMetaFlag.PERM
|
||||
NODE = OnionFailureCodeMetaFlag.NODE
|
||||
|
@ -398,6 +487,7 @@ class OnionFailureCode(IntEnum):
|
|||
FINAL_INCORRECT_HTLC_AMOUNT = 19
|
||||
CHANNEL_DISABLED = UPDATE | 20
|
||||
EXPIRY_TOO_FAR = 21
|
||||
INVALID_ONION_PAYLOAD = PERM | 22
|
||||
|
||||
|
||||
# don't use these elsewhere, the names are ambiguous without context
|
||||
|
|
|
@ -37,14 +37,14 @@ from . import lnutil
|
|||
from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
|
||||
RemoteConfig, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore,
|
||||
funding_output_script, get_per_commitment_secret_from_seed,
|
||||
secret_to_pubkey, PaymentFailure, LnLocalFeatures,
|
||||
secret_to_pubkey, PaymentFailure, LnFeatures,
|
||||
LOCAL, REMOTE, HTLCOwner, generate_keypair, LnKeyFamily,
|
||||
ln_compare_features, privkey_to_pubkey, UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_ACCEPTED,
|
||||
LightningPeerConnectionClosed, HandshakeFailed, NotFoundChanAnnouncementForUpdate,
|
||||
MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED, MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED,
|
||||
MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED, RemoteMisbehaving, DEFAULT_TO_SELF_DELAY,
|
||||
NBLOCK_OUR_CLTV_EXPIRY_DELTA, format_short_channel_id, ShortChannelID,
|
||||
IncompatibleLightningFeatures)
|
||||
IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage)
|
||||
from .lnutil import FeeUpdate
|
||||
from .lntransport import LNTransport, LNTransportBase
|
||||
from .lnmsg import encode_msg, decode_msg
|
||||
|
@ -77,7 +77,7 @@ class Peer(Logger):
|
|||
self.pubkey = pubkey # remote pubkey
|
||||
self.lnworker = lnworker
|
||||
self.privkey = lnworker.node_keypair.privkey # local privkey
|
||||
self.localfeatures = self.lnworker.localfeatures
|
||||
self.features = self.lnworker.features
|
||||
self.node_ids = [self.pubkey, privkey_to_pubkey(self.privkey)]
|
||||
self.network = lnworker.network
|
||||
self.channel_db = lnworker.network.channel_db
|
||||
|
@ -131,7 +131,12 @@ class Peer(Logger):
|
|||
async def initialize(self):
|
||||
if isinstance(self.transport, LNTransport):
|
||||
await self.transport.handshake()
|
||||
self.send_message("init", gflen=0, lflen=2, localfeatures=self.localfeatures)
|
||||
# FIXME: "flen" hardcoded but actually it depends on "features"...:
|
||||
self.send_message("init", gflen=0, flen=2, features=self.features.for_init_message(),
|
||||
init_tlvs={
|
||||
'networks':
|
||||
{'chains': constants.net.rev_genesis_bytes()}
|
||||
})
|
||||
self._sent_init = True
|
||||
self.maybe_set_initialized()
|
||||
|
||||
|
@ -180,7 +185,7 @@ class Peer(Logger):
|
|||
self.ordered_message_queues[chan_id].put_nowait((None, {'error':payload['data']}))
|
||||
|
||||
def on_ping(self, payload):
|
||||
l = int.from_bytes(payload['num_pong_bytes'], 'big')
|
||||
l = payload['num_pong_bytes']
|
||||
self.send_message('pong', byteslen=l)
|
||||
|
||||
def on_pong(self, payload):
|
||||
|
@ -199,14 +204,25 @@ class Peer(Logger):
|
|||
if self._received_init:
|
||||
self.logger.info("ALREADY INITIALIZED BUT RECEIVED INIT")
|
||||
return
|
||||
# if they required some even flag we don't have, they will close themselves
|
||||
# but if we require an even flag they don't have, we close
|
||||
their_localfeatures = int.from_bytes(payload['localfeatures'], byteorder="big")
|
||||
their_features = LnFeatures(int.from_bytes(payload['features'], byteorder="big"))
|
||||
their_globalfeatures = int.from_bytes(payload['globalfeatures'], byteorder="big")
|
||||
their_features |= their_globalfeatures
|
||||
# check transitive dependencies for received features
|
||||
if not their_features.validate_transitive_dependecies():
|
||||
raise GracefulDisconnect("remote did not set all dependencies for the features they sent")
|
||||
# check if features are compatible, and set self.features to what we negotiated
|
||||
try:
|
||||
self.localfeatures = ln_compare_features(self.localfeatures, their_localfeatures)
|
||||
self.features = ln_compare_features(self.features, their_features)
|
||||
except IncompatibleLightningFeatures as e:
|
||||
self.initialized.set_exception(e)
|
||||
raise GracefulDisconnect(f"{str(e)}")
|
||||
# check that they are on the same chain as us, if provided
|
||||
their_networks = payload["init_tlvs"].get("networks")
|
||||
if their_networks:
|
||||
their_chains = list(chunks(their_networks["chains"], 32))
|
||||
if constants.net.rev_genesis_bytes() not in their_chains:
|
||||
raise GracefulDisconnect(f"no common chain found with remote. (they sent: {their_chains})")
|
||||
# all checks passed
|
||||
if isinstance(self.transport, LNTransport):
|
||||
self.channel_db.add_recent_peer(self.transport.peer_addr)
|
||||
for chan in self.channels.values():
|
||||
|
@ -417,8 +433,8 @@ class Peer(Logger):
|
|||
return ids
|
||||
|
||||
def on_reply_channel_range(self, payload):
|
||||
first = int.from_bytes(payload['first_blocknum'], 'big')
|
||||
num = int.from_bytes(payload['number_of_blocks'], 'big')
|
||||
first = payload['first_blocknum']
|
||||
num = payload['number_of_blocks']
|
||||
complete = bool(int.from_bytes(payload['complete'], 'big'))
|
||||
encoded = payload['encoded_short_ids']
|
||||
ids = self.decode_short_ids(encoded)
|
||||
|
@ -465,7 +481,7 @@ class Peer(Logger):
|
|||
self.lnworker.peer_closed(self)
|
||||
|
||||
def is_static_remotekey(self):
|
||||
return bool(self.localfeatures & LnLocalFeatures.OPTION_STATIC_REMOTEKEY_OPT)
|
||||
return bool(self.features & LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
|
||||
|
||||
def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> LocalConfig:
|
||||
# key derivation
|
||||
|
@ -541,27 +557,27 @@ class Peer(Logger):
|
|||
)
|
||||
payload = await self.wait_for_message('accept_channel', temp_channel_id)
|
||||
remote_per_commitment_point = payload['first_per_commitment_point']
|
||||
funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big')
|
||||
funding_txn_minimum_depth = payload['minimum_depth']
|
||||
if funding_txn_minimum_depth <= 0:
|
||||
raise Exception(f"minimum depth too low, {funding_txn_minimum_depth}")
|
||||
if funding_txn_minimum_depth > 30:
|
||||
raise Exception(f"minimum depth too high, {funding_txn_minimum_depth}")
|
||||
remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big')
|
||||
remote_dust_limit_sat = payload['dust_limit_satoshis']
|
||||
remote_reserve_sat = self.validate_remote_reserve(payload["channel_reserve_satoshis"], remote_dust_limit_sat, funding_sat)
|
||||
if remote_dust_limit_sat > remote_reserve_sat:
|
||||
raise Exception(f"Remote Lightning peer reports dust_limit_sat > reserve_sat which is a BOLT-02 protocol violation.")
|
||||
htlc_min = int.from_bytes(payload['htlc_minimum_msat'], 'big')
|
||||
htlc_min = payload['htlc_minimum_msat']
|
||||
if htlc_min > MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED:
|
||||
raise Exception(f"Remote Lightning peer reports htlc_minimum_msat={htlc_min} mSAT," +
|
||||
f" which is above Electrums required maximum limit of that parameter ({MAXIMUM_HTLC_MINIMUM_MSAT_ACCEPTED} mSAT).")
|
||||
remote_max = int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big')
|
||||
remote_max = payload['max_htlc_value_in_flight_msat']
|
||||
if remote_max < MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED:
|
||||
raise Exception(f"Remote Lightning peer reports max_htlc_value_in_flight_msat at only {remote_max} mSAT" +
|
||||
f" which is below Electrums required minimum ({MINIMUM_MAX_HTLC_VALUE_IN_FLIGHT_ACCEPTED} mSAT).")
|
||||
max_accepted_htlcs = int.from_bytes(payload["max_accepted_htlcs"], 'big')
|
||||
max_accepted_htlcs = payload["max_accepted_htlcs"]
|
||||
if max_accepted_htlcs > 483:
|
||||
raise Exception("Remote Lightning peer reports max_accepted_htlcs > 483, which is a BOLT-02 protocol violation.")
|
||||
remote_to_self_delay = int.from_bytes(payload['to_self_delay'], byteorder='big')
|
||||
remote_to_self_delay = payload['to_self_delay']
|
||||
if remote_to_self_delay > MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED:
|
||||
raise Exception(f"Remote Lightning peer reports to_self_delay={remote_to_self_delay}," +
|
||||
f" which is above Electrums required maximum ({MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED})")
|
||||
|
@ -647,9 +663,9 @@ class Peer(Logger):
|
|||
# payload['channel_flags']
|
||||
if payload['chain_hash'] != constants.net.rev_genesis_bytes():
|
||||
raise Exception('wrong chain_hash')
|
||||
funding_sat = int.from_bytes(payload['funding_satoshis'], 'big')
|
||||
push_msat = int.from_bytes(payload['push_msat'], 'big')
|
||||
feerate = int.from_bytes(payload['feerate_per_kw'], 'big')
|
||||
funding_sat = payload['funding_satoshis']
|
||||
push_msat = payload['push_msat']
|
||||
feerate = payload['feerate_per_kw']
|
||||
temp_chan_id = payload['temporary_channel_id']
|
||||
local_config = self.make_local_config(funding_sat, push_msat, REMOTE)
|
||||
# for the first commitment transaction
|
||||
|
@ -674,11 +690,11 @@ class Peer(Logger):
|
|||
first_per_commitment_point=per_commitment_point_first,
|
||||
)
|
||||
funding_created = await self.wait_for_message('funding_created', temp_chan_id)
|
||||
funding_idx = int.from_bytes(funding_created['funding_output_index'], 'big')
|
||||
funding_idx = funding_created['funding_output_index']
|
||||
funding_txid = bh2u(funding_created['funding_txid'][::-1])
|
||||
channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx)
|
||||
remote_balance_sat = funding_sat * 1000 - push_msat
|
||||
remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') # TODO validate
|
||||
remote_dust_limit_sat = payload['dust_limit_satoshis'] # TODO validate
|
||||
remote_reserve_sat = self.validate_remote_reserve(payload['channel_reserve_satoshis'], remote_dust_limit_sat, funding_sat)
|
||||
remote_config = RemoteConfig(
|
||||
payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
|
||||
|
@ -686,13 +702,13 @@ class Peer(Logger):
|
|||
htlc_basepoint=OnlyPubkeyKeypair(payload['htlc_basepoint']),
|
||||
delayed_basepoint=OnlyPubkeyKeypair(payload['delayed_payment_basepoint']),
|
||||
revocation_basepoint=OnlyPubkeyKeypair(payload['revocation_basepoint']),
|
||||
to_self_delay=int.from_bytes(payload['to_self_delay'], 'big'),
|
||||
to_self_delay=payload['to_self_delay'],
|
||||
dust_limit_sat=remote_dust_limit_sat,
|
||||
max_htlc_value_in_flight_msat=int.from_bytes(payload['max_htlc_value_in_flight_msat'], 'big'), # TODO validate
|
||||
max_accepted_htlcs=int.from_bytes(payload['max_accepted_htlcs'], 'big'), # TODO validate
|
||||
max_htlc_value_in_flight_msat=payload['max_htlc_value_in_flight_msat'], # TODO validate
|
||||
max_accepted_htlcs=payload['max_accepted_htlcs'], # TODO validate
|
||||
initial_msat=remote_balance_sat,
|
||||
reserve_sat = remote_reserve_sat,
|
||||
htlc_minimum_msat=int.from_bytes(payload['htlc_minimum_msat'], 'big'), # TODO validate
|
||||
htlc_minimum_msat=payload['htlc_minimum_msat'], # TODO validate
|
||||
next_per_commitment_point=payload['first_per_commitment_point'],
|
||||
current_per_commitment_point=None,
|
||||
)
|
||||
|
@ -718,8 +734,7 @@ class Peer(Logger):
|
|||
chan.set_state(channel_states.OPENING)
|
||||
self.lnworker.add_new_channel(chan)
|
||||
|
||||
def validate_remote_reserve(self, payload_field: bytes, dust_limit: int, funding_sat: int) -> int:
|
||||
remote_reserve_sat = int.from_bytes(payload_field, 'big')
|
||||
def validate_remote_reserve(self, remote_reserve_sat: int, dust_limit: int, funding_sat: int) -> int:
|
||||
if remote_reserve_sat < dust_limit:
|
||||
raise Exception('protocol violation: reserve < dust_limit')
|
||||
if remote_reserve_sat > funding_sat/100:
|
||||
|
@ -745,7 +760,7 @@ class Peer(Logger):
|
|||
oldest_unrevoked_remote_ctn = chan.get_oldest_unrevoked_ctn(REMOTE)
|
||||
latest_remote_ctn = chan.get_latest_ctn(REMOTE)
|
||||
next_remote_ctn = chan.get_next_ctn(REMOTE)
|
||||
assert self.localfeatures & LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT
|
||||
assert self.features & LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
|
||||
# send message
|
||||
srk_enabled = chan.is_static_remotekey_enabled()
|
||||
if srk_enabled:
|
||||
|
@ -760,16 +775,16 @@ class Peer(Logger):
|
|||
self.send_message(
|
||||
"channel_reestablish",
|
||||
channel_id=chan_id,
|
||||
next_local_commitment_number=next_local_ctn,
|
||||
next_remote_revocation_number=oldest_unrevoked_remote_ctn,
|
||||
next_commitment_number=next_local_ctn,
|
||||
next_revocation_number=oldest_unrevoked_remote_ctn,
|
||||
your_last_per_commitment_secret=last_rev_secret,
|
||||
my_current_per_commitment_point=latest_point)
|
||||
self.logger.info(f'channel_reestablish ({chan.get_id_for_log()}): sent channel_reestablish with '
|
||||
f'(next_local_ctn={next_local_ctn}, '
|
||||
f'oldest_unrevoked_remote_ctn={oldest_unrevoked_remote_ctn})')
|
||||
msg = await self.wait_for_message('channel_reestablish', chan_id)
|
||||
their_next_local_ctn = int.from_bytes(msg["next_local_commitment_number"], 'big')
|
||||
their_oldest_unrevoked_remote_ctn = int.from_bytes(msg["next_remote_revocation_number"], 'big')
|
||||
their_next_local_ctn = msg["next_commitment_number"]
|
||||
their_oldest_unrevoked_remote_ctn = msg["next_revocation_number"]
|
||||
their_local_pcp = msg.get("my_current_per_commitment_point")
|
||||
their_claim_of_our_last_per_commitment_secret = msg.get("your_last_per_commitment_secret")
|
||||
self.logger.info(f'channel_reestablish ({chan.get_id_for_log()}): received channel_reestablish with '
|
||||
|
@ -818,7 +833,7 @@ class Peer(Logger):
|
|||
if oldest_unrevoked_local_ctn != their_oldest_unrevoked_remote_ctn:
|
||||
if oldest_unrevoked_local_ctn - 1 == their_oldest_unrevoked_remote_ctn:
|
||||
# A node:
|
||||
# if next_remote_revocation_number is equal to the commitment number of the last revoke_and_ack
|
||||
# if next_revocation_number is equal to the commitment number of the last revoke_and_ack
|
||||
# the receiving node sent, AND the receiving node hasn't already received a closing_signed:
|
||||
# MUST re-send the revoke_and_ack.
|
||||
last_secret, last_point = chan.get_secret_and_point(LOCAL, oldest_unrevoked_local_ctn - 1)
|
||||
|
@ -1005,7 +1020,7 @@ class Peer(Logger):
|
|||
return msg_hash, node_signature, bitcoin_signature
|
||||
|
||||
def on_update_fail_htlc(self, chan: Channel, payload):
|
||||
htlc_id = int.from_bytes(payload["id"], "big")
|
||||
htlc_id = payload["id"]
|
||||
reason = payload["reason"]
|
||||
self.logger.info(f"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
|
||||
chan.receive_fail_htlc(htlc_id, error_bytes=reason) # TODO handle exc and maybe fail channel (e.g. bad htlc_id)
|
||||
|
@ -1022,15 +1037,19 @@ class Peer(Logger):
|
|||
sig_64, htlc_sigs = chan.sign_next_commitment()
|
||||
self.send_message("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs))
|
||||
|
||||
def pay(self, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
|
||||
payment_hash: bytes, min_final_cltv_expiry: int) -> UpdateAddHtlc:
|
||||
def pay(self, *, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
|
||||
payment_hash: bytes, min_final_cltv_expiry: int, payment_secret: bytes = None) -> UpdateAddHtlc:
|
||||
assert amount_msat > 0, "amount_msat is not greater zero"
|
||||
assert len(route) > 0
|
||||
if not chan.can_send_update_add_htlc():
|
||||
raise PaymentFailure("Channel cannot send update_add_htlc")
|
||||
# add features learned during "init" for direct neighbour:
|
||||
route[0].node_features |= self.features
|
||||
local_height = self.network.get_local_height()
|
||||
# create onion packet
|
||||
final_cltv = local_height + min_final_cltv_expiry
|
||||
hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv)
|
||||
hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv,
|
||||
payment_secret=payment_secret)
|
||||
assert final_cltv <= cltv, (final_cltv, cltv)
|
||||
secret_key = os.urandom(32)
|
||||
onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash)
|
||||
|
@ -1040,7 +1059,8 @@ class Peer(Logger):
|
|||
htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time()))
|
||||
htlc = chan.add_htlc(htlc)
|
||||
chan.set_onion_key(htlc.htlc_id, secret_key)
|
||||
self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. htlc: {htlc}")
|
||||
self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. "
|
||||
f"htlc: {htlc}. hops_data={hops_data!r}")
|
||||
self.send_message(
|
||||
"update_add_htlc",
|
||||
channel_id=chan.channel_id,
|
||||
|
@ -1083,7 +1103,7 @@ class Peer(Logger):
|
|||
def on_update_fulfill_htlc(self, chan: Channel, payload):
|
||||
preimage = payload["payment_preimage"]
|
||||
payment_hash = sha256(preimage)
|
||||
htlc_id = int.from_bytes(payload["id"], "big")
|
||||
htlc_id = payload["id"]
|
||||
self.logger.info(f"on_update_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
|
||||
chan.receive_htlc_settle(preimage, htlc_id) # TODO handle exc and maybe fail channel (e.g. bad htlc_id)
|
||||
self.lnworker.save_preimage(payment_hash, preimage)
|
||||
|
@ -1103,10 +1123,10 @@ class Peer(Logger):
|
|||
|
||||
def on_update_add_htlc(self, chan: Channel, payload):
|
||||
payment_hash = payload["payment_hash"]
|
||||
htlc_id = int.from_bytes(payload["id"], 'big')
|
||||
htlc_id = payload["id"]
|
||||
self.logger.info(f"on_update_add_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
|
||||
cltv_expiry = int.from_bytes(payload["cltv_expiry"], 'big')
|
||||
amount_msat_htlc = int.from_bytes(payload["amount_msat"], 'big')
|
||||
cltv_expiry = payload["cltv_expiry"]
|
||||
amount_msat_htlc = payload["amount_msat"]
|
||||
onion_packet = payload["onion_routing_packet"]
|
||||
if chan.get_state() != channel_states.OPEN:
|
||||
raise RemoteMisbehaving(f"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()}")
|
||||
|
@ -1130,9 +1150,11 @@ class Peer(Logger):
|
|||
if not forwarding_enabled:
|
||||
self.logger.info(f"forwarding is disabled. failing htlc.")
|
||||
return OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'')
|
||||
dph = processed_onion.hop_data.per_hop
|
||||
next_chan = self.lnworker.get_channel_by_short_id(dph.short_channel_id)
|
||||
next_chan_scid = dph.short_channel_id
|
||||
try:
|
||||
next_chan_scid = processed_onion.hop_data.payload["short_channel_id"]["short_channel_id"]
|
||||
except:
|
||||
return OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
next_chan = self.lnworker.get_channel_by_short_id(next_chan_scid)
|
||||
local_height = self.network.get_local_height()
|
||||
if next_chan is None:
|
||||
self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}")
|
||||
|
@ -1144,7 +1166,10 @@ class Peer(Logger):
|
|||
f"chan state {next_chan.get_state()}, peer state: {next_chan.peer_state}")
|
||||
data = outgoing_chan_upd_len + outgoing_chan_upd
|
||||
return OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
|
||||
next_cltv_expiry = int.from_bytes(dph.outgoing_cltv_value, 'big')
|
||||
try:
|
||||
next_cltv_expiry = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
|
||||
except:
|
||||
return OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
if htlc.cltv_expiry - next_cltv_expiry < NBLOCK_OUR_CLTV_EXPIRY_DELTA:
|
||||
data = htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd
|
||||
return OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=data)
|
||||
|
@ -1154,7 +1179,10 @@ class Peer(Logger):
|
|||
return OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_SOON, data=data)
|
||||
if max(htlc.cltv_expiry, next_cltv_expiry) > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
|
||||
return OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'')
|
||||
next_amount_msat_htlc = int.from_bytes(dph.amt_to_forward, 'big')
|
||||
try:
|
||||
next_amount_msat_htlc = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
|
||||
except:
|
||||
return OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
forwarding_fees = fee_for_edge_msat(
|
||||
forwarded_amount_msat=next_amount_msat_htlc,
|
||||
fee_base_msat=lnutil.OUR_FEE_BASE_MSAT,
|
||||
|
@ -1175,8 +1203,8 @@ class Peer(Logger):
|
|||
"update_add_htlc",
|
||||
channel_id=next_chan.channel_id,
|
||||
id=next_htlc.htlc_id,
|
||||
cltv_expiry=dph.outgoing_cltv_value,
|
||||
amount_msat=dph.amt_to_forward,
|
||||
cltv_expiry=next_cltv_expiry,
|
||||
amount_msat=next_amount_msat_htlc,
|
||||
payment_hash=next_htlc.payment_hash,
|
||||
onion_routing_packet=processed_onion.next_packet.to_bytes()
|
||||
)
|
||||
|
@ -1194,6 +1222,14 @@ class Peer(Logger):
|
|||
except UnknownPaymentHash:
|
||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
|
||||
return False, reason
|
||||
try:
|
||||
payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
|
||||
except:
|
||||
pass # skip
|
||||
else:
|
||||
if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage):
|
||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
|
||||
return False, reason
|
||||
expected_received_msat = int(info.amount * 1000) if info.amount is not None else None
|
||||
if expected_received_msat is not None and \
|
||||
not (expected_received_msat <= htlc.amount_msat <= 2 * expected_received_msat):
|
||||
|
@ -1203,12 +1239,24 @@ class Peer(Logger):
|
|||
if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry:
|
||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'')
|
||||
return False, reason
|
||||
cltv_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.outgoing_cltv_value, byteorder="big")
|
||||
try:
|
||||
cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
|
||||
except:
|
||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
return False, reason
|
||||
if cltv_from_onion != htlc.cltv_expiry:
|
||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
|
||||
data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
|
||||
return False, reason
|
||||
amount_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.amt_to_forward, byteorder="big")
|
||||
try:
|
||||
amount_from_onion = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
|
||||
except:
|
||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
return False, reason
|
||||
try:
|
||||
amount_from_onion = processed_onion.hop_data.payload["payment_data"]["total_msat"]
|
||||
except:
|
||||
pass # fall back to "amt_to_forward"
|
||||
if amount_from_onion > htlc.amount_msat:
|
||||
reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
|
||||
data=htlc.amount_msat.to_bytes(8, byteorder="big"))
|
||||
|
@ -1258,7 +1306,7 @@ class Peer(Logger):
|
|||
self.maybe_send_commitment(chan)
|
||||
|
||||
def on_update_fee(self, chan: Channel, payload):
|
||||
feerate = int.from_bytes(payload["feerate_per_kw"], "big")
|
||||
feerate = payload["feerate_per_kw"]
|
||||
chan.update_fee(feerate, False)
|
||||
|
||||
async def maybe_update_fee(self, chan: Channel):
|
||||
|
@ -1378,7 +1426,7 @@ class Peer(Logger):
|
|||
while True:
|
||||
# FIXME: the remote SHOULD send closing_signed, but some don't.
|
||||
cs_payload = await self.wait_for_message('closing_signed', chan.channel_id)
|
||||
their_fee = int.from_bytes(cs_payload['fee_satoshis'], 'big')
|
||||
their_fee = cs_payload['fee_satoshis']
|
||||
if their_fee > max_fee:
|
||||
raise Exception(f'the proposed fee exceeds the base fee of the latest commitment transaction {is_local, their_fee, max_fee}')
|
||||
their_sig = cs_payload['signature']
|
||||
|
@ -1445,6 +1493,9 @@ class Peer(Logger):
|
|||
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_KEY, data=sha256(onion_packet_bytes))
|
||||
except InvalidOnionMac:
|
||||
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.INVALID_ONION_HMAC, data=sha256(onion_packet_bytes))
|
||||
except Exception as e:
|
||||
self.logger.info(f"error processing onion packet: {e!r}")
|
||||
error_reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
|
||||
else:
|
||||
if processed_onion.are_we_final:
|
||||
preimage, error_reason = self.maybe_fulfill_htlc(
|
||||
|
|
|
@ -27,11 +27,13 @@ import queue
|
|||
from collections import defaultdict
|
||||
from typing import Sequence, List, Tuple, Optional, Dict, NamedTuple, TYPE_CHECKING, Set
|
||||
|
||||
import attr
|
||||
|
||||
from .util import bh2u, profiler
|
||||
from .logging import Logger
|
||||
from .lnutil import NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID
|
||||
from .channel_db import ChannelDB, Policy
|
||||
from .lnutil import NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE
|
||||
from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures,
|
||||
NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE)
|
||||
from .channel_db import ChannelDB, Policy, NodeInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .lnchannel import Channel
|
||||
|
@ -48,13 +50,15 @@ def fee_for_edge_msat(forwarded_amount_msat: int, fee_base_msat: int, fee_propor
|
|||
+ (forwarded_amount_msat * fee_proportional_millionths // 1_000_000)
|
||||
|
||||
|
||||
class RouteEdge(NamedTuple):
|
||||
@attr.s
|
||||
class RouteEdge:
|
||||
"""if you travel through short_channel_id, you will reach node_id"""
|
||||
node_id: bytes
|
||||
short_channel_id: ShortChannelID
|
||||
fee_base_msat: int
|
||||
fee_proportional_millionths: int
|
||||
cltv_expiry_delta: int
|
||||
node_id = attr.ib(type=bytes, kw_only=True)
|
||||
short_channel_id = attr.ib(type=ShortChannelID, kw_only=True)
|
||||
fee_base_msat = attr.ib(type=int, kw_only=True)
|
||||
fee_proportional_millionths = attr.ib(type=int, kw_only=True)
|
||||
cltv_expiry_delta = attr.ib(type=int, kw_only=True)
|
||||
node_features = attr.ib(type=int, kw_only=True) # note: for end node!
|
||||
|
||||
def fee_for_edge(self, amount_msat: int) -> int:
|
||||
return fee_for_edge_msat(forwarded_amount_msat=amount_msat,
|
||||
|
@ -63,14 +67,16 @@ class RouteEdge(NamedTuple):
|
|||
|
||||
@classmethod
|
||||
def from_channel_policy(cls, channel_policy: 'Policy',
|
||||
short_channel_id: bytes, end_node: bytes) -> 'RouteEdge':
|
||||
short_channel_id: bytes, end_node: bytes, *,
|
||||
node_info: Optional[NodeInfo]) -> 'RouteEdge':
|
||||
assert isinstance(short_channel_id, bytes)
|
||||
assert type(end_node) is bytes
|
||||
return RouteEdge(end_node,
|
||||
ShortChannelID.normalize(short_channel_id),
|
||||
channel_policy.fee_base_msat,
|
||||
channel_policy.fee_proportional_millionths,
|
||||
channel_policy.cltv_expiry_delta)
|
||||
return RouteEdge(node_id=end_node,
|
||||
short_channel_id=ShortChannelID.normalize(short_channel_id),
|
||||
fee_base_msat=channel_policy.fee_base_msat,
|
||||
fee_proportional_millionths=channel_policy.fee_proportional_millionths,
|
||||
cltv_expiry_delta=channel_policy.cltv_expiry_delta,
|
||||
node_features=node_info.features if node_info else 0)
|
||||
|
||||
def is_sane_to_use(self, amount_msat: int) -> bool:
|
||||
# TODO revise ad-hoc heuristics
|
||||
|
@ -82,6 +88,10 @@ class RouteEdge(NamedTuple):
|
|||
return False
|
||||
return True
|
||||
|
||||
def has_feature_varonion(self) -> bool:
|
||||
features = self.node_features
|
||||
return bool(features & LnFeatures.VAR_ONION_REQ or features & LnFeatures.VAR_ONION_OPT)
|
||||
|
||||
|
||||
LNPaymentRoute = Sequence[RouteEdge]
|
||||
|
||||
|
@ -154,7 +164,9 @@ class LNPathFinder(Logger):
|
|||
if channel_policy.htlc_maximum_msat is not None and \
|
||||
payment_amt_msat > channel_policy.htlc_maximum_msat:
|
||||
return float('inf'), 0 # payment amount too large
|
||||
route_edge = RouteEdge.from_channel_policy(channel_policy, short_channel_id, end_node)
|
||||
node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node)
|
||||
route_edge = RouteEdge.from_channel_policy(channel_policy, short_channel_id, end_node,
|
||||
node_info=node_info)
|
||||
if not route_edge.is_sane_to_use(payment_amt_msat):
|
||||
return float('inf'), 0 # thanks but no thanks
|
||||
|
||||
|
@ -268,6 +280,8 @@ class LNPathFinder(Logger):
|
|||
my_channels=my_channels)
|
||||
if channel_policy is None:
|
||||
raise NoChannelPolicy(short_channel_id)
|
||||
route.append(RouteEdge.from_channel_policy(channel_policy, short_channel_id, node_id))
|
||||
node_info = self.channel_db.get_node_info_for_node_id(node_id=node_id)
|
||||
route.append(RouteEdge.from_channel_policy(channel_policy, short_channel_id, node_id,
|
||||
node_info=node_info))
|
||||
prev_node_id = node_id
|
||||
return route
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
from enum import IntFlag, IntEnum
|
||||
import enum
|
||||
import json
|
||||
from collections import namedtuple
|
||||
from collections import namedtuple, defaultdict
|
||||
from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
|
||||
import re
|
||||
import attr
|
||||
|
||||
import attr
|
||||
from aiorpcx import NetAddress
|
||||
|
||||
from .util import bfh, bh2u, inv_dict, UserFacingException
|
||||
|
@ -708,19 +709,137 @@ def get_ecdh(priv: bytes, pub: bytes) -> bytes:
|
|||
return sha256(pt.get_public_key_bytes())
|
||||
|
||||
|
||||
class LnLocalFeatures(IntFlag):
|
||||
class LnFeatureContexts(enum.Flag):
|
||||
INIT = enum.auto()
|
||||
NODE_ANN = enum.auto()
|
||||
CHAN_ANN_AS_IS = enum.auto()
|
||||
CHAN_ANN_ALWAYS_ODD = enum.auto()
|
||||
CHAN_ANN_ALWAYS_EVEN = enum.auto()
|
||||
INVOICE = enum.auto()
|
||||
|
||||
LNFC = LnFeatureContexts
|
||||
|
||||
_ln_feature_direct_dependencies = defaultdict(set) # type: Dict[LnFeatures, Set[LnFeatures]]
|
||||
_ln_feature_contexts = {} # type: Dict[LnFeatures, LnFeatureContexts]
|
||||
|
||||
class LnFeatures(IntFlag):
|
||||
OPTION_DATA_LOSS_PROTECT_REQ = 1 << 0
|
||||
OPTION_DATA_LOSS_PROTECT_OPT = 1 << 1
|
||||
_ln_feature_contexts[OPTION_DATA_LOSS_PROTECT_OPT] = (LNFC.INIT | LnFeatureContexts.NODE_ANN)
|
||||
_ln_feature_contexts[OPTION_DATA_LOSS_PROTECT_REQ] = (LNFC.INIT | LnFeatureContexts.NODE_ANN)
|
||||
|
||||
INITIAL_ROUTING_SYNC = 1 << 3
|
||||
_ln_feature_contexts[INITIAL_ROUTING_SYNC] = LNFC.INIT
|
||||
|
||||
OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ = 1 << 4
|
||||
OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT = 1 << 5
|
||||
_ln_feature_contexts[OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
|
||||
_ln_feature_contexts[OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
|
||||
|
||||
GOSSIP_QUERIES_REQ = 1 << 6
|
||||
GOSSIP_QUERIES_OPT = 1 << 7
|
||||
_ln_feature_contexts[GOSSIP_QUERIES_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
|
||||
_ln_feature_contexts[GOSSIP_QUERIES_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
|
||||
|
||||
VAR_ONION_REQ = 1 << 8
|
||||
VAR_ONION_OPT = 1 << 9
|
||||
_ln_feature_contexts[VAR_ONION_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
|
||||
_ln_feature_contexts[VAR_ONION_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
|
||||
|
||||
GOSSIP_QUERIES_EX_REQ = 1 << 10
|
||||
GOSSIP_QUERIES_EX_OPT = 1 << 11
|
||||
_ln_feature_direct_dependencies[GOSSIP_QUERIES_EX_OPT] = {GOSSIP_QUERIES_OPT}
|
||||
_ln_feature_contexts[GOSSIP_QUERIES_EX_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
|
||||
_ln_feature_contexts[GOSSIP_QUERIES_EX_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
|
||||
|
||||
OPTION_STATIC_REMOTEKEY_REQ = 1 << 12
|
||||
OPTION_STATIC_REMOTEKEY_OPT = 1 << 13
|
||||
_ln_feature_contexts[OPTION_STATIC_REMOTEKEY_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
|
||||
_ln_feature_contexts[OPTION_STATIC_REMOTEKEY_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
|
||||
|
||||
# note that these are powers of two, not the bits themselves
|
||||
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:
|
||||
|
@ -735,23 +854,24 @@ def get_ln_flag_pair_of_bit(flag_bit: int) -> int:
|
|||
return flag_bit - 1
|
||||
|
||||
|
||||
class LnGlobalFeatures(IntFlag):
|
||||
pass
|
||||
|
||||
# note that these are powers of two, not the bits themselves
|
||||
LN_GLOBAL_FEATURES_KNOWN_SET = set(LnGlobalFeatures)
|
||||
class IncompatibleOrInsaneFeatures(Exception): pass
|
||||
class UnknownEvenFeatureBits(IncompatibleOrInsaneFeatures): pass
|
||||
class IncompatibleLightningFeatures(IncompatibleOrInsaneFeatures): pass
|
||||
|
||||
class IncompatibleLightningFeatures(ValueError): pass
|
||||
|
||||
def ln_compare_features(our_features, their_features) -> int:
|
||||
"""raises IncompatibleLightningFeatures if incompatible"""
|
||||
def ln_compare_features(our_features: 'LnFeatures', their_features: int) -> 'LnFeatures':
|
||||
"""Returns negotiated features.
|
||||
Raises IncompatibleLightningFeatures if incompatible.
|
||||
"""
|
||||
our_flags = set(list_enabled_bits(our_features))
|
||||
their_flags = set(list_enabled_bits(their_features))
|
||||
# check that they have our required features, and disable the optional features they don't have
|
||||
for flag in our_flags:
|
||||
if flag not in their_flags and get_ln_flag_pair_of_bit(flag) not in their_flags:
|
||||
# they don't have this feature we wanted :(
|
||||
if flag % 2 == 0: # even flags are compulsory
|
||||
raise IncompatibleLightningFeatures(f"remote does not support {LnLocalFeatures(1 << flag)!r}")
|
||||
raise IncompatibleLightningFeatures(f"remote does not support {LnFeatures(1 << flag)!r}")
|
||||
our_features ^= 1 << flag # disable flag
|
||||
else:
|
||||
# They too have this flag.
|
||||
|
@ -759,9 +879,42 @@ def ln_compare_features(our_features, their_features) -> int:
|
|||
# set the corresponding odd flag now.
|
||||
if flag % 2 == 0 and our_features & (1 << flag):
|
||||
our_features |= 1 << get_ln_flag_pair_of_bit(flag)
|
||||
# check that we have their required features
|
||||
for flag in their_flags:
|
||||
if flag not in our_flags and get_ln_flag_pair_of_bit(flag) not in our_flags:
|
||||
# we don't have this feature they wanted :(
|
||||
if flag % 2 == 0: # even flags are compulsory
|
||||
raise IncompatibleLightningFeatures(f"remote wanted feature we don't have: {LnFeatures(1 << flag)!r}")
|
||||
return our_features
|
||||
|
||||
|
||||
def validate_features(features: int) -> None:
|
||||
"""Raises IncompatibleOrInsaneFeatures if
|
||||
- a mandatory feature is listed that we don't recognize, or
|
||||
- the features are inconsistent
|
||||
"""
|
||||
features = LnFeatures(features)
|
||||
enabled_features = list_enabled_bits(features)
|
||||
for fbit in enabled_features:
|
||||
if (1 << fbit) & LN_FEATURES_IMPLEMENTED == 0 and fbit % 2 == 0:
|
||||
raise UnknownEvenFeatureBits(fbit)
|
||||
if not features.validate_transitive_dependecies():
|
||||
raise IncompatibleOrInsaneFeatures("not all transitive dependencies are set")
|
||||
|
||||
|
||||
def derive_payment_secret_from_payment_preimage(payment_preimage: bytes) -> bytes:
|
||||
"""Returns secret to be put into invoice.
|
||||
Derivation is deterministic, based on the preimage.
|
||||
Crucially the payment_hash must be derived in an independent way from this.
|
||||
"""
|
||||
# Note that this could be random data too, but then we would need to store it.
|
||||
# We derive it identically to clightning, so that we cannot be distinguished:
|
||||
# https://github.com/ElementsProject/lightning/blob/faac4b28adee5221e83787d64cd5d30b16b62097/lightningd/invoice.c#L115
|
||||
modified = bytearray(payment_preimage)
|
||||
modified[0] ^= 1
|
||||
return sha256(bytes(modified))
|
||||
|
||||
|
||||
class LNPeerAddr:
|
||||
|
||||
def __init__(self, host: str, port: int, pubkey: bytes):
|
||||
|
@ -955,3 +1108,11 @@ class UpdateAddHtlc:
|
|||
|
||||
def to_tuple(self):
|
||||
return (self.amount_msat, self.payment_hash, self.cltv_expiry, self.htlc_id, self.timestamp)
|
||||
|
||||
|
||||
class OnionFailureCodeMetaFlag(IntFlag):
|
||||
BADONION = 0x8000
|
||||
PERM = 0x4000
|
||||
NODE = 0x2000
|
||||
UPDATE = 0x1000
|
||||
|
||||
|
|
5
electrum/lnwire/README.md
Normal file
5
electrum/lnwire/README.md
Normal file
|
@ -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
Normal file
53
electrum/lnwire/onion_wire.csv
Normal file
|
@ -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
|
|
210
electrum/lnwire/peer_wire.csv
Normal file
210
electrum/lnwire/peer_wire.csv
Normal file
|
@ -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,
|
|
|
@ -52,10 +52,10 @@ from .lnutil import (Outpoint, LNPeerAddr,
|
|||
generate_keypair, LnKeyFamily, LOCAL, REMOTE,
|
||||
UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE,
|
||||
NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
|
||||
UpdateAddHtlc, Direction, LnLocalFeatures,
|
||||
UpdateAddHtlc, Direction, LnFeatures,
|
||||
ShortChannelID, PaymentAttemptLog, PaymentAttemptFailureDetails,
|
||||
BarePaymentAttemptLog)
|
||||
from .lnutil import ln_dummy_address, ln_compare_features
|
||||
BarePaymentAttemptLog, derive_payment_secret_from_payment_preimage)
|
||||
from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures
|
||||
from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
|
||||
from .lnonion import OnionFailureCode, process_onion_packet, OnionPacket
|
||||
from .lnmsg import decode_msg
|
||||
|
@ -147,9 +147,11 @@ class LNWorker(Logger):
|
|||
self.taskgroup = SilentTaskGroup()
|
||||
# set some feature flags as baseline for both LNWallet and LNGossip
|
||||
# note that e.g. DATA_LOSS_PROTECT is needed for LNGossip as many peers require it
|
||||
self.localfeatures = LnLocalFeatures(0)
|
||||
self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT
|
||||
self.localfeatures |= LnLocalFeatures.OPTION_STATIC_REMOTEKEY_OPT
|
||||
self.features = LnFeatures(0)
|
||||
self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
|
||||
self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT
|
||||
self.features |= LnFeatures.VAR_ONION_OPT
|
||||
self.features |= LnFeatures.PAYMENT_SECRET_OPT
|
||||
|
||||
def channels_for_peer(self, node_id):
|
||||
return {}
|
||||
|
@ -248,8 +250,8 @@ class LNWorker(Logger):
|
|||
if not node:
|
||||
return False
|
||||
try:
|
||||
ln_compare_features(self.localfeatures, node.features)
|
||||
except ValueError:
|
||||
ln_compare_features(self.features, node.features)
|
||||
except IncompatibleLightningFeatures:
|
||||
return False
|
||||
#self.logger.info(f'is_good {peer.host}')
|
||||
return True
|
||||
|
@ -366,8 +368,8 @@ class LNGossip(LNWorker):
|
|||
node = BIP32Node.from_rootseed(seed, xtype='standard')
|
||||
xprv = node.to_xprv()
|
||||
super().__init__(xprv)
|
||||
self.localfeatures |= LnLocalFeatures.GOSSIP_QUERIES_OPT
|
||||
self.localfeatures |= LnLocalFeatures.GOSSIP_QUERIES_REQ
|
||||
self.features |= LnFeatures.GOSSIP_QUERIES_OPT
|
||||
self.features |= LnFeatures.GOSSIP_QUERIES_REQ
|
||||
self.unknown_ids = set()
|
||||
|
||||
def start_network(self, network: 'Network'):
|
||||
|
@ -419,8 +421,8 @@ class LNWallet(LNWorker):
|
|||
self.db = wallet.db
|
||||
self.config = wallet.config
|
||||
LNWorker.__init__(self, xprv)
|
||||
self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
||||
self.localfeatures |= LnLocalFeatures.OPTION_STATIC_REMOTEKEY_REQ
|
||||
self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
||||
self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
|
||||
self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid
|
||||
self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
|
||||
self.sweep_address = wallet.get_receiving_address()
|
||||
|
@ -952,7 +954,12 @@ class LNWallet(LNWorker):
|
|||
if not peer:
|
||||
raise Exception('Dropped peer')
|
||||
await peer.initialized
|
||||
htlc = peer.pay(route, chan, int(lnaddr.amount * COIN * 1000), lnaddr.paymenthash, lnaddr.get_min_final_cltv_expiry())
|
||||
htlc = peer.pay(route=route,
|
||||
chan=chan,
|
||||
amount_msat=int(lnaddr.amount * COIN * 1000),
|
||||
payment_hash=lnaddr.paymenthash,
|
||||
min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
|
||||
payment_secret=lnaddr.payment_secret)
|
||||
self.network.trigger_callback('htlc_added', htlc, lnaddr, SENT)
|
||||
payment_attempt = await self.await_payment(lnaddr.paymenthash)
|
||||
if payment_attempt.success:
|
||||
|
@ -1047,7 +1054,7 @@ class LNWallet(LNWorker):
|
|||
return addr
|
||||
|
||||
@profiler
|
||||
def _create_route_from_invoice(self, decoded_invoice) -> LNPaymentRoute:
|
||||
def _create_route_from_invoice(self, decoded_invoice: 'LnAddr') -> LNPaymentRoute:
|
||||
amount_msat = int(decoded_invoice.amount * COIN * 1000)
|
||||
invoice_pubkey = decoded_invoice.pubkey.serialize()
|
||||
# use 'r' field from invoice
|
||||
|
@ -1091,8 +1098,13 @@ class LNWallet(LNWorker):
|
|||
fee_base_msat = channel_policy.fee_base_msat
|
||||
fee_proportional_millionths = channel_policy.fee_proportional_millionths
|
||||
cltv_expiry_delta = channel_policy.cltv_expiry_delta
|
||||
route.append(RouteEdge(node_pubkey, short_channel_id, fee_base_msat, fee_proportional_millionths,
|
||||
cltv_expiry_delta))
|
||||
node_info = self.channel_db.get_node_info_for_node_id(node_id=node_pubkey)
|
||||
route.append(RouteEdge(node_id=node_pubkey,
|
||||
short_channel_id=short_channel_id,
|
||||
fee_base_msat=fee_base_msat,
|
||||
fee_proportional_millionths=fee_proportional_millionths,
|
||||
cltv_expiry_delta=cltv_expiry_delta,
|
||||
node_features=node_info.features if node_info else 0))
|
||||
prev_node_id = node_pubkey
|
||||
# test sanity
|
||||
if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
|
||||
|
@ -1111,6 +1123,11 @@ class LNWallet(LNWorker):
|
|||
if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
|
||||
self.logger.info(f"rejecting insane route {route}")
|
||||
raise NoPathFound()
|
||||
assert len(route) > 0
|
||||
assert route[-1].node_id == invoice_pubkey
|
||||
# add features from invoice
|
||||
invoice_features = decoded_invoice.get_tag('9') or 0
|
||||
route[-1].node_features |= invoice_features
|
||||
return route
|
||||
|
||||
def add_request(self, amount_sat, message, expiry):
|
||||
|
@ -1130,6 +1147,7 @@ class LNWallet(LNWorker):
|
|||
"Other clients will likely not be able to send to us.")
|
||||
payment_preimage = os.urandom(32)
|
||||
payment_hash = sha256(payment_preimage)
|
||||
|
||||
info = PaymentInfo(payment_hash, amount_sat, RECEIVED, PR_UNPAID)
|
||||
amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
|
||||
if expiry == 0:
|
||||
|
@ -1138,12 +1156,15 @@ class LNWallet(LNWorker):
|
|||
# Our higher level invoices code however uses 0 for "never".
|
||||
# Hence set some high expiration here
|
||||
expiry = 100 * 365 * 24 * 60 * 60 # 100 years
|
||||
lnaddr = LnAddr(payment_hash, amount_btc,
|
||||
lnaddr = LnAddr(paymenthash=payment_hash,
|
||||
amount=amount_btc,
|
||||
tags=[('d', message),
|
||||
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
|
||||
('x', expiry)]
|
||||
('x', expiry),
|
||||
('9', self.features.for_invoice())]
|
||||
+ routing_hints,
|
||||
date = timestamp)
|
||||
date=timestamp,
|
||||
payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage))
|
||||
invoice = lnencode(lnaddr, self.node_keypair.privkey)
|
||||
key = bh2u(lnaddr.paymenthash)
|
||||
req = {
|
||||
|
|
|
@ -6,6 +6,7 @@ import unittest
|
|||
|
||||
from electrum.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, lndecode, u5_to_bitarray, bitarray_to_u5
|
||||
from electrum.segwit_addr import bech32_encode, bech32_decode
|
||||
from electrum.lnutil import UnknownEvenFeatureBits, derive_payment_secret_from_payment_preimage
|
||||
|
||||
from . import ElectrumTestCase
|
||||
|
||||
|
@ -61,16 +62,28 @@ class TestBolt11(ElectrumTestCase):
|
|||
|
||||
|
||||
tests = [
|
||||
LnAddr(RHASH, tags=[('d', '')]),
|
||||
LnAddr(RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]),
|
||||
LnAddr(RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),
|
||||
LnAddr(RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),
|
||||
LnAddr(RHASH, amount=24, tags=[
|
||||
('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3), (unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]), ('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'), ('h', longdescription)]),
|
||||
LnAddr(RHASH, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription)]),
|
||||
LnAddr(RHASH, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription)]),
|
||||
LnAddr(RHASH, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription)]),
|
||||
LnAddr(RHASH, amount=24, tags=[('n', PUBKEY), ('h', longdescription)]),
|
||||
LnAddr(paymenthash=RHASH, tags=[('d', '')]),
|
||||
LnAddr(paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60)]),
|
||||
LnAddr(paymenthash=RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),
|
||||
LnAddr(paymenthash=RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[
|
||||
('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3),
|
||||
(unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]),
|
||||
('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'),
|
||||
('h', longdescription)]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription)]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription)]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription)]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('n', PUBKEY), ('h', longdescription)]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 514)]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 8))]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9))]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 7) + (1 << 11))]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 12))]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 13))]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 14))]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 10 + (1 << 9) + (1 << 15))]),
|
||||
LnAddr(paymenthash=RHASH, amount=24, tags=[('h', longdescription), ('9', 33282)], payment_secret=b"\x11" * 32),
|
||||
]
|
||||
|
||||
# Roundtrip
|
||||
|
@ -81,14 +94,14 @@ class TestBolt11(ElectrumTestCase):
|
|||
def test_n_decoding(self):
|
||||
# We flip the signature recovery bit, which would normally give a different
|
||||
# pubkey.
|
||||
hrp, data = bech32_decode(lnencode(LnAddr(RHASH, amount=24, tags=[('d', '')]), PRIVKEY), True)
|
||||
hrp, data = bech32_decode(lnencode(LnAddr(paymenthash=RHASH, amount=24, tags=[('d', '')]), PRIVKEY), True)
|
||||
databits = u5_to_bitarray(data)
|
||||
databits.invert(-1)
|
||||
lnaddr = lndecode(bech32_encode(hrp, bitarray_to_u5(databits)), verbose=True)
|
||||
assert lnaddr.pubkey.serialize() != PUBKEY
|
||||
|
||||
# But not if we supply expliciy `n` specifier!
|
||||
hrp, data = bech32_decode(lnencode(LnAddr(RHASH, amount=24,
|
||||
hrp, data = bech32_decode(lnencode(LnAddr(paymenthash=RHASH, amount=24,
|
||||
tags=[('d', ''),
|
||||
('n', PUBKEY)]),
|
||||
PRIVKEY), True)
|
||||
|
@ -98,9 +111,28 @@ class TestBolt11(ElectrumTestCase):
|
|||
assert lnaddr.pubkey.serialize() == PUBKEY
|
||||
|
||||
def test_min_final_cltv_expiry_decoding(self):
|
||||
self.assertEqual(144, lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe", expected_hrp="sb").get_min_final_cltv_expiry())
|
||||
lnaddr = lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe",
|
||||
expected_hrp="sb")
|
||||
self.assertEqual(144, lnaddr.get_min_final_cltv_expiry())
|
||||
|
||||
def test_min_final_cltv_expiry_roundtrip(self):
|
||||
lnaddr = LnAddr(RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', 150)])
|
||||
lnaddr = LnAddr(paymenthash=RHASH, amount=Decimal('0.001'), tags=[('d', '1 cup coffee'), ('x', 60), ('c', 150)])
|
||||
invoice = lnencode(lnaddr, PRIVKEY)
|
||||
self.assertEqual(150, lndecode(invoice).get_min_final_cltv_expiry())
|
||||
|
||||
def test_features(self):
|
||||
lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl")
|
||||
self.assertEqual(514, lnaddr.get_tag('9'))
|
||||
|
||||
with self.assertRaises(UnknownEvenFeatureBits):
|
||||
lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7")
|
||||
|
||||
def test_payment_secret(self):
|
||||
lnaddr = lndecode("lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygsdq5vdhkven9v5sxyetpdees9q5sqqqqqqqqqqqqqqqpqsqvvh7ut50r00p3pg34ea68k7zfw64f8yx9jcdk35lh5ft8qdr8g4r0xzsdcrmcy9hex8un8d8yraewvhqc9l0sh8l0e0yvmtxde2z0hgpzsje5l")
|
||||
self.assertEqual((1 << 9) + (1 << 15) + (1 << 99), lnaddr.get_tag('9'))
|
||||
self.assertEqual(b"\x11" * 32, lnaddr.payment_secret)
|
||||
|
||||
def test_derive_payment_secret_from_payment_preimage(self):
|
||||
preimage = bytes.fromhex("cc3fc000bdeff545acee53ada12ff96060834be263f77d645abbebc3a8d53b92")
|
||||
self.assertEqual("bfd660b559b3f452c6bb05b8d2906f520c151c107b733863ed0cc53fc77021a8",
|
||||
derive_payment_secret_from_payment_preimage(preimage).hex())
|
||||
|
|
385
electrum/tests/test_lnmsg.py
Normal file
385
electrum/tests/test_lnmsg.py
Normal file
|
@ -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")))
|
|
@ -21,7 +21,7 @@ from electrum.util import bh2u, create_and_start_event_loop
|
|||
from electrum.lnpeer import Peer
|
||||
from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey
|
||||
from electrum.lnutil import LightningPeerConnectionClosed, RemoteMisbehaving
|
||||
from electrum.lnutil import PaymentFailure, LnLocalFeatures, HTLCOwner
|
||||
from electrum.lnutil import PaymentFailure, LnFeatures, HTLCOwner
|
||||
from electrum.lnchannel import channel_states, peer_states, Channel
|
||||
from electrum.lnrouter import LNPathFinder
|
||||
from electrum.channel_db import ChannelDB
|
||||
|
@ -95,8 +95,8 @@ class MockLNWallet(Logger):
|
|||
self.payments = {}
|
||||
self.logs = defaultdict(list)
|
||||
self.wallet = MockWallet()
|
||||
self.localfeatures = LnLocalFeatures(0)
|
||||
self.localfeatures |= LnLocalFeatures.OPTION_DATA_LOSS_PROTECT_OPT
|
||||
self.features = LnFeatures(0)
|
||||
self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
|
||||
self.pending_payments = defaultdict(asyncio.Future)
|
||||
chan.lnworker = self
|
||||
chan.node_id = remote_keypair.pubkey
|
||||
|
@ -235,8 +235,8 @@ class TestPeer(ElectrumTestCase):
|
|||
w2.save_preimage(RHASH, payment_preimage)
|
||||
w2.save_payment_info(info)
|
||||
lnaddr = LnAddr(
|
||||
RHASH,
|
||||
amount_btc,
|
||||
paymenthash=RHASH,
|
||||
amount=amount_btc,
|
||||
tags=[('c', lnutil.MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
|
||||
('d', 'coffee')
|
||||
])
|
||||
|
@ -317,8 +317,9 @@ class TestPeer(ElectrumTestCase):
|
|||
alice_init_balance_msat = alice_channel.balance(HTLCOwner.LOCAL)
|
||||
bob_init_balance_msat = bob_channel.balance(HTLCOwner.LOCAL)
|
||||
num_payments = 50
|
||||
payment_value_sat = 10000 # make it large enough so that there are actually HTLCs on the ctx
|
||||
#pay_reqs1 = [self.prepare_invoice(w1, amount_sat=1) for i in range(num_payments)]
|
||||
pay_reqs2 = [self.prepare_invoice(w2, amount_sat=1) for i in range(num_payments)]
|
||||
pay_reqs2 = [self.prepare_invoice(w2, amount_sat=payment_value_sat) for i in range(num_payments)]
|
||||
max_htlcs_in_flight = asyncio.Semaphore(5)
|
||||
async def single_payment(pay_req):
|
||||
async with max_htlcs_in_flight:
|
||||
|
@ -333,10 +334,10 @@ class TestPeer(ElectrumTestCase):
|
|||
await gath
|
||||
with self.assertRaises(concurrent.futures.CancelledError):
|
||||
run(f())
|
||||
self.assertEqual(alice_init_balance_msat - num_payments * 1000, alice_channel.balance(HTLCOwner.LOCAL))
|
||||
self.assertEqual(alice_init_balance_msat - num_payments * 1000, bob_channel.balance(HTLCOwner.REMOTE))
|
||||
self.assertEqual(bob_init_balance_msat + num_payments * 1000, bob_channel.balance(HTLCOwner.LOCAL))
|
||||
self.assertEqual(bob_init_balance_msat + num_payments * 1000, alice_channel.balance(HTLCOwner.REMOTE))
|
||||
self.assertEqual(alice_init_balance_msat - num_payments * payment_value_sat * 1000, alice_channel.balance(HTLCOwner.LOCAL))
|
||||
self.assertEqual(alice_init_balance_msat - num_payments * payment_value_sat * 1000, bob_channel.balance(HTLCOwner.REMOTE))
|
||||
self.assertEqual(bob_init_balance_msat + num_payments * payment_value_sat * 1000, bob_channel.balance(HTLCOwner.LOCAL))
|
||||
self.assertEqual(bob_init_balance_msat + num_payments * payment_value_sat * 1000, alice_channel.balance(HTLCOwner.REMOTE))
|
||||
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
def test_close(self):
|
||||
|
@ -354,7 +355,12 @@ class TestPeer(ElectrumTestCase):
|
|||
await asyncio.wait_for(p2.initialized, 1)
|
||||
# alice sends htlc
|
||||
route = w1._create_route_from_invoice(decoded_invoice=lnaddr)
|
||||
htlc = p1.pay(route, alice_channel, int(lnaddr.amount * COIN * 1000), lnaddr.paymenthash, lnaddr.get_min_final_cltv_expiry())
|
||||
htlc = p1.pay(route=route,
|
||||
chan=alice_channel,
|
||||
amount_msat=int(lnaddr.amount * COIN * 1000),
|
||||
payment_hash=lnaddr.paymenthash,
|
||||
min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
|
||||
payment_secret=lnaddr.payment_secret)
|
||||
# alice closes
|
||||
await p1.close_channel(alice_channel.channel_id)
|
||||
gath.cancel()
|
||||
|
|
|
@ -4,9 +4,9 @@ import shutil
|
|||
import asyncio
|
||||
|
||||
from electrum.util import bh2u, bfh, create_and_start_event_loop
|
||||
from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet, OnionPerHop,
|
||||
from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet,
|
||||
process_onion_packet, _decode_onion_error, decode_onion_error,
|
||||
OnionFailureCode)
|
||||
OnionFailureCode, OnionPacket)
|
||||
from electrum import bitcoin, lnrouter
|
||||
from electrum.constants import BitcoinTestnet
|
||||
from electrum.simple_config import SimpleConfig
|
||||
|
@ -57,46 +57,45 @@ class Test_LNRouter(TestCaseForTestnet):
|
|||
'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02cccccccccccccccccccccccccccccccc',
|
||||
'short_channel_id': bfh('0000000000000001'),
|
||||
'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
|
||||
'len': b'\x00\x00', 'features': b''}, trusted=True)
|
||||
'len': 0, 'features': b''}, trusted=True)
|
||||
self.assertEqual(cdb.num_channels, 1)
|
||||
cdb.add_channel_announcement({'node_id_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
||||
'bitcoin_key_1': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
||||
'short_channel_id': bfh('0000000000000002'),
|
||||
'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
|
||||
'len': b'\x00\x00', 'features': b''}, trusted=True)
|
||||
'len': 0, 'features': b''}, trusted=True)
|
||||
cdb.add_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
'short_channel_id': bfh('0000000000000003'),
|
||||
'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
|
||||
'len': b'\x00\x00', 'features': b''}, trusted=True)
|
||||
'len': 0, 'features': b''}, trusted=True)
|
||||
cdb.add_channel_announcement({'node_id_1': b'\x02cccccccccccccccccccccccccccccccc', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
|
||||
'bitcoin_key_1': b'\x02cccccccccccccccccccccccccccccccc', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd',
|
||||
'short_channel_id': bfh('0000000000000004'),
|
||||
'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
|
||||
'len': b'\x00\x00', 'features': b''}, trusted=True)
|
||||
'len': 0, 'features': b''}, trusted=True)
|
||||
cdb.add_channel_announcement({'node_id_1': b'\x02dddddddddddddddddddddddddddddddd', 'node_id_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
||||
'bitcoin_key_1': b'\x02dddddddddddddddddddddddddddddddd', 'bitcoin_key_2': b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
|
||||
'short_channel_id': bfh('0000000000000005'),
|
||||
'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
|
||||
'len': b'\x00\x00', 'features': b''}, trusted=True)
|
||||
'len': 0, 'features': b''}, trusted=True)
|
||||
cdb.add_channel_announcement({'node_id_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'node_id_2': b'\x02dddddddddddddddddddddddddddddddd',
|
||||
'bitcoin_key_1': b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'bitcoin_key_2': b'\x02dddddddddddddddddddddddddddddddd',
|
||||
'short_channel_id': bfh('0000000000000006'),
|
||||
'chain_hash': BitcoinTestnet.rev_genesis_bytes(),
|
||||
'len': b'\x00\x00', 'features': b''}, trusted=True)
|
||||
o = lambda i: i.to_bytes(8, "big")
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(99), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(999), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(99999999), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150), 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': b'\x00\x00\x00\x00'})
|
||||
'len': 0, 'features': b''}, trusted=True)
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000001'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 99, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000002'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000003'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000004'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000005'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 999, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x00', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 99999999, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
cdb.add_channel_update({'short_channel_id': bfh('0000000000000006'), 'message_flags': b'\x00', 'channel_flags': b'\x01', 'cltv_expiry_delta': 10, 'htlc_minimum_msat': 250, 'fee_base_msat': 100, 'fee_proportional_millionths': 150, 'chain_hash': BitcoinTestnet.rev_genesis_bytes(), 'timestamp': 0})
|
||||
path = path_finder.find_path_for_payment(b'\x02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 100000)
|
||||
self.assertEqual([(b'\x02bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', b'\x00\x00\x00\x00\x00\x00\x00\x03'),
|
||||
(b'\x02eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', b'\x00\x00\x00\x00\x00\x00\x00\x02'),
|
||||
|
@ -112,7 +111,7 @@ class Test_LNRouter(TestCaseForTestnet):
|
|||
cdb.sql_thread.join(timeout=1)
|
||||
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
def test_new_onion_packet(self):
|
||||
def test_new_onion_packet_legacy(self):
|
||||
# test vector from bolt-04
|
||||
payment_path_pubkeys = [
|
||||
bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
|
||||
|
@ -124,28 +123,127 @@ class Test_LNRouter(TestCaseForTestnet):
|
|||
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
|
||||
associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
|
||||
hops_data = [
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000')
|
||||
)),
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001')
|
||||
)),
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002')
|
||||
)),
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003')
|
||||
)),
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004')
|
||||
)),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 0},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 0},
|
||||
"short_channel_id": {"short_channel_id": bfh('0000000000000000')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 1},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 1},
|
||||
"short_channel_id": {"short_channel_id": bfh('0101010101010101')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 2},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 2},
|
||||
"short_channel_id": {"short_channel_id": bfh('0202020202020202')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 3},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 3},
|
||||
"short_channel_id": {"short_channel_id": bfh('0303030303030303')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 4},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 4},
|
||||
"short_channel_id": {"short_channel_id": bfh('0404040404040404')},
|
||||
}),
|
||||
]
|
||||
packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data)
|
||||
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71e87f9aab8f6378c6ff744c1f34b393ad28d065b535c1a8668d85d3b34a1b3befd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a1f9e7abc789266cc861cabd95818c0fc8efbdfdc14e3f7c2bc7eb8d6a79ef75ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d014698cf05d742557763d9cb743faeae65dcc79dddaecf27fe5942be5380d15e9a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040a2a2fba158a0d8085926dc2e44f0c88bf487da56e13ef2d5e676a8589881b4869ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565a9f99728426ce2380a9580e2a9442481ceae7679906c30b1a0e21a10f26150e0645ab6edfdab1ce8f8bea7b1dee511c5fd38ac0e702c1c15bb86b52bca1b71e15b96982d262a442024c33ceb7dd8f949063c2e5e613e873250e2f8708bd4e1924abd45f65c2fa5617bfb10ee9e4a42d6b5811acc8029c16274f937dac9e8817c7e579fdb767ffe277f26d413ced06b620ede8362081da21cf67c2ca9d6f15fe5bc05f82f5bb93f8916bad3d63338ca824f3bbc11b57ce94a5fa1bc239533679903d6fec92a8c792fd86e2960188c14f21e399cfd72a50c620e10aefc6249360b463df9a89bf6836f4f26359207b765578e5ed76ae9f31b1cc48324be576e3d8e44d217445dba466f9b6293fdf05448584eb64f61e02903f834518622b7d4732471c6e0e22e22d1f45e31f0509eab39cdea5980a492a1da2aaac55a98a01216cd4bfe7abaa682af0fbff2dfed030ba28f1285df750e4d3477190dd193f8643b61d8ac1c427d590badb1f61a05d480908fbdc7c6f0502dd0c4abb51d725e92f95da2a8facb79881a844e2026911adcc659d1fb20a2fce63787c8bb0d9f6789c4b231c76da81c3f0718eb7156565a081d2be6b4170c0e0bcebddd459f53db2590c974bca0d705c055dee8c629bf854a5d58edc85228499ec6dde80cce4c8910b81b1e9e8b0f43bd39c8d69c3a80672729b7dc952dd9448688b6bd06afc2d2819cda80b66c57b52ccf7ac1a86601410d18d0c732f69de792e0894a9541684ef174de766fd4ce55efea8f53812867be6a391ac865802dbc26d93959df327ec2667c7256aa5a1d3c45a69a6158f285d6c97c3b8eedb09527848500517995a9eae4cd911df531544c77f5a9a2f22313e3eb72ca7a07dba243476bc926992e0d1e58b4a2fc8c7b01e0cad726237933ea319bad7537d39f3ed635d1e6c1d29e97b3d2160a09e30ee2b65ac5bce00996a73c008bcf351cecb97b6833b6d121dcf4644260b2946ea204732ac9954b228f0beaa15071930fd9583dfc466d12b5f0eeeba6dcf23d5ce8ae62ee5796359d97a4a15955c778d868d0ef9991d9f2833b5bb66119c5f8b396fd108baed7906cbb3cc376d13551caed97fece6f42a4c908ee279f1127fda1dd3ee77d8de0a6f3c135fa3f1cffe38591b6738dc97b55f0acc52be9753ce53e64d7e497bb00ca6123758df3b68fad99e35c04389f7514a8e36039f541598a417275e77869989782325a15b5342ac5011ff07af698584b476b35d941a4981eac590a07a092bb50342da5d3341f901aa07964a8d02b623c7b106dd0ae50bfa007a22d46c8772fa55558176602946cb1d11ea5460db7586fb89c6d3bcd3ab6dd20df4a4db63d2e7d52380800ad812b8640887e027e946df96488b47fbc4a4fadaa8beda4abe446fafea5403fae2ef'),
|
||||
packet.to_bytes())
|
||||
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
def test_process_onion_packet(self):
|
||||
def test_new_onion_packet_mixed_payloads(self):
|
||||
# test vector from bolt-04
|
||||
payment_path_pubkeys = [
|
||||
bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
|
||||
bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'),
|
||||
bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'),
|
||||
bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'),
|
||||
bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'),
|
||||
]
|
||||
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
|
||||
associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
|
||||
hops_data = [
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 0},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 0},
|
||||
"short_channel_id": {"short_channel_id": bfh('0000000000000000')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=True),
|
||||
OnionHopsDataSingle(is_tlv_payload=True),
|
||||
OnionHopsDataSingle(is_tlv_payload=True),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 4},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 4},
|
||||
"short_channel_id": {"short_channel_id": bfh('0404040404040404')},
|
||||
}),
|
||||
]
|
||||
hops_data[1]._raw_bytes_payload = bfh("0101010101010101000000000000000100000001")
|
||||
hops_data[2]._raw_bytes_payload = bfh("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff")
|
||||
hops_data[3]._raw_bytes_payload = bfh("0303030303030303000000000000000300000003")
|
||||
packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data)
|
||||
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a710f8eaf9ccc768f66bb5dec1f7827f33c43fe2ddd05614c8283aa78e9e7573f87c50f7d61ab590531cf08000178a333a347f8b4072e1cea42da7552402b10765adae3f581408f35ff0a71a34b78b1d8ecae77df96c6404bae9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf057f5995d9731c4bf796fb0e41c885d488dcbc68eb742e27f44310b276edc6f652658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d710ee5c193921909bdd75db331cd9d7581a39fca50814ed8d9d402b86e7f8f6ac2f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa3379834bc2380d58e9d23237821475a1874484783a15d68f47d3dc339f38d9bf925655d5c946778680fd6d1f062f84128895aff09d35d6c92cca63d3f95a9ee8f2a84f383b4d6a087533e65de12fc8dcaf85777736a2088ff4b22462265028695b37e70963c10df8ef2458756c73007dc3e544340927f9e9f5ea4816a9fd9832c311d122e9512739a6b4714bba590e31caa143ce83cb84b36c738c60c3190ff70cd9ac286a9fd2ab619399b68f1f7447be376ce884b5913c8496d01cbf7a44a60b6e6747513f69dc538f340bc1388e0fde5d0c1db50a4dcb9cc0576e0e2474e4853af9623212578d502757ffb2e0e749695ed70f61c116560d0d4154b64dcf3cbf3c91d89fb6dd004dc19588e3479fcc63c394a4f9e8a3b8b961fce8a532304f1337f1a697a1bb14b94d2953f39b73b6a3125d24f27fcd4f60437881185370bde68a5454d816e7a70d4cea582effab9a4f1b730437e35f7a5c4b769c7b72f0346887c1e63576b2f1e2b3706142586883f8cf3a23595cc8e35a52ad290afd8d2f8bcd5b4c1b891583a4159af7110ecde092079209c6ec46d2bda60b04c519bb8bc6dffb5c87f310814ef2f3003671b3c90ddf5d0173a70504c2280d31f17c061f4bb12a978122c8a2a618bb7d1edcf14f84bf0fa181798b826a254fca8b6d7c81e0beb01bd77f6461be3c8647301d02b04753b0771105986aa0cbc13f7718d64e1b3437e8eef1d319359914a7932548c91570ef3ea741083ca5be5ff43c6d9444d29df06f76ec3dc936e3d180f4b6d0fbc495487c7d44d7c8fe4a70d5ff1461d0d9593f3f898c919c363fa18341ce9dae54f898ccf3fe792136682272941563387263c51b2a2f32363b804672cc158c9230472b554090a661aa81525d11876eefdcc45442249e61e07284592f1606491de5c0324d3af4be035d7ede75b957e879e9770cdde2e1bbc1ef75d45fe555f1ff6ac296a2f648eeee59c7c08260226ea333c285bcf37a9bbfa57ba2ab8083c4be6fc2ebe279537d22da96a07392908cf22b233337a74fe5c603b51712b43c3ee55010ee3d44dd9ba82bba3145ec358f863e04bbfa53799a7a9216718fd5859da2f0deb77b8e315ad6868fdec9400f45a48e6dc8ddbaeb3'),
|
||||
packet.to_bytes())
|
||||
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
def test_process_onion_packet_mixed_payloads(self):
|
||||
# this test is not from bolt-04, but is based on the one there;
|
||||
# here the TLV payloads are actually sane...
|
||||
payment_path_pubkeys = [
|
||||
bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
|
||||
bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'),
|
||||
bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'),
|
||||
bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'),
|
||||
bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'),
|
||||
]
|
||||
payment_path_privkeys = [
|
||||
bfh('4141414141414141414141414141414141414141414141414141414141414141'),
|
||||
bfh('4242424242424242424242424242424242424242424242424242424242424242'),
|
||||
bfh('4343434343434343434343434343434343434343434343434343434343434343'),
|
||||
bfh('4444444444444444444444444444444444444444444444444444444444444444'),
|
||||
bfh('4545454545454545454545454545454545454545454545454545454545454545'),
|
||||
]
|
||||
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
|
||||
associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
|
||||
hops_data = [
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 0},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 0},
|
||||
"short_channel_id": {"short_channel_id": bfh('0000000000000000')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=True, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 1},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 1},
|
||||
"short_channel_id": {"short_channel_id": bfh('0101010101010101')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=True, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 2},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 2},
|
||||
"short_channel_id": {"short_channel_id": bfh('0202020202020202')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=True, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 3},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 3},
|
||||
"short_channel_id": {"short_channel_id": bfh('0303030303030303')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 4},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 4},
|
||||
"short_channel_id": {"short_channel_id": bfh('0404040404040404')},
|
||||
}),
|
||||
]
|
||||
packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data)
|
||||
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71bde5adfa90b337f34616d8673d09dd055937273045566ce537ffbe3f9d1f263dc10c7d61ae590536c609010079a232a247922a5395359a63dfbefb85f40317e23254f3023f7d4a98f746c9ab06647645ce55c67308e3c77dc87a1caeac51b03b23c60f05e536e1d757c8c1093e34accfc4f97b5920f6dd2069d5b9ddbb384c3ac575e999a92a4434470ab0aa040c4c3cace3162a405842a88be783e64fad54bd6727c23fc446b7ec0dc3eec5a03eb6c70ec2784911c9e6d274322ec465f0972eb8e771b149f319582ba64dbc2b8e56a3ea79002801c09354f1541cf79bd1dccf5d6bd6b6bacc87a0f24ce497e14e8037e5a79fb4d9ca63fe47f17765963e8f17468a5eaec19a6cca2bfc4e4a366fea3a92112a945856be55e45197ecbab523025e7589529c30cc8addc8fa39d23ef64fa2e51a219c3bd4d3c484832f8e5af16bc46cdba0403991f4fc1b74beef857acf15fefed82ac8678ca66d26262c681beddfdb485aa498813b1a6c5833f1339c1a35244ab76baa0ccaf681ec1f54004e387063335648a77b65d90dde74f1c4b0a729ca25fa53256f7db6d35818b4e5910ba78ec69cf3646bf248ef46cf9cc33062662de2afe4dcf005951b85fd759429fa1ae490b78b14132ccb791232a6c680f03634c0136817f51bf9603a0dba405e7b347830be4327fceccd4734456842b82cf6275393b279bc6ac93d743e00a2d6042960089f70c782ce554b9f73eeeefeea50df7f6f80de1c4e869a7b502f9a5df30d1175402fa780812d35c6d489a30bb0cea53a1088669a238cccf416ecb37f8d8e6ea1327b64979d48e937db69a44a902923a75113685a4aca4a8d9c62b388b48d9c9e2ab9c2df4d529223144de6e16f2dd95a063da79163b3fe006a80263cde4410648f7c3e1f4a7707f82eb0e209002d972c7e57b4ff8ce063fa7b4140f52f569f0cc8793a97a170613efb6b27ba3a0370f8ea74fc0d6aabba54e0ee967abc70e87b580d2aac244236b7752db9d83b159afc1faf6b44b697643235bf59e99f43428caff409d26b9139538865b1f5cf4699f9296088aca461209024ad1dd00e3566e4fde2117b7b3ffced6696b735816a00199890056de86dcbb1b930228143dbf04f07c0eb34370089ea55c43b2c4546cbe1ff0c3a6217d994af9b4225f4b5acb1e3129f5f5b98d381a4692a8561c670b2ee95869f9614e76bb07f623c5194e1c9d26334026f8f5437ec1cde526f914fa094a465f0adcea32b79bfa44d2562536b0d8366da9ee577666c1d5e39615444ca5c900b8199fafac002b8235688eaa0c6887475a913b37d9a4ed43a894ea4576102e5d475ae0b962240ea95fc367b7ac214a4f8682448a9c0d2eea35727bdedc235a975ecc8148a5b03d6291a051dbefe19c8b344d2713c6664dd94ced53c6be39a837fbf1169cca6a12b0a2710f443ba1afeecb51e94236b2a6ed1c2f365b595443b1515de86dcb8c67282807789b47c331cde2fdd721262bef165fa96b7919d11bc5f2022f5affffdd747c7dbe3de8add829a0a8913519fdf7dba4e8a7a25456d2d559746d39ea6ffa31c7b904792fb734bba30f2e1adf7457a994513a1807785fe7b22bf419d1f407f8e2db8b22c0512b078c0cfdfd599e6c4a9d0cc624b9e24b87f30541c3248cd6643df15d251775cc457df4ea6b4e4c5990d87541028c6f0eb28502db1c11a92797168d0b68cb0a0d345b3a3ad05fc4016862f403c64670c41a2c0c6d4e384f5f7da6a204a24530a51182fd7164f120e74a78decb1ab6cda6b9cfc68ac0a35f7a57e750ead65a8e0429cc16e733b9e4feaea25d06c1a4768'),
|
||||
packet.to_bytes())
|
||||
for i, privkey in enumerate(payment_path_privkeys):
|
||||
processed_packet = process_onion_packet(packet, associated_data, privkey)
|
||||
self.assertEqual(hops_data[i].to_bytes(), processed_packet.hop_data.to_bytes())
|
||||
packet = processed_packet.next_packet
|
||||
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
def test_process_onion_packet_legacy(self):
|
||||
# this test is not from bolt-04, but is based on the one there;
|
||||
# except here we have the privkeys for these pubkeys
|
||||
payment_path_pubkeys = [
|
||||
|
@ -165,28 +263,38 @@ class Test_LNRouter(TestCaseForTestnet):
|
|||
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
|
||||
associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
|
||||
hops_data = [
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000')
|
||||
)),
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001')
|
||||
)),
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002')
|
||||
)),
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003')
|
||||
)),
|
||||
OnionHopsDataSingle(OnionPerHop(
|
||||
bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004')
|
||||
)),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 0},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 0},
|
||||
"short_channel_id": {"short_channel_id": bfh('0000000000000000')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 1},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 1},
|
||||
"short_channel_id": {"short_channel_id": bfh('0101010101010101')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 2},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 2},
|
||||
"short_channel_id": {"short_channel_id": bfh('0202020202020202')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 3},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 3},
|
||||
"short_channel_id": {"short_channel_id": bfh('0303030303030303')},
|
||||
}),
|
||||
OnionHopsDataSingle(is_tlv_payload=False, payload={
|
||||
"amt_to_forward": {"amt_to_forward": 4},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": 4},
|
||||
"short_channel_id": {"short_channel_id": bfh('0404040404040404')},
|
||||
}),
|
||||
]
|
||||
packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data)
|
||||
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661954176cd9869da33d713aa219fcef1e5c806fef11e696bcc66844de8271c27974a049d041ffc5be934b8575c6ff4371f2f88d4edfd73e445534d3f6ae15b64b0d8308390bebf8d149002e31bdc283056477ba27c8054c248ad7306de31663a7c99ec65b251704041f7c4cc40a0016ba172fbf805ec59132a65a4c7eb1f41337931c5df0f840704535729262d30c6132d1b390f073edec8fa057176c6268b6ad06a82ff0229c3be444ee50b40686bc1306838b93c65771de1b6ca05dace1ff9814a6e58b2dd71e8244c83e28b2ed5a3b09e9e7df5c8c747e5765ba366a4f7407a6c6b0a32fb5521cce7cd668f7434c909c1be027d8595d85893e5f612c49a93eeeed80a78bab9c4a621ce0f6f5df7d64a9c8d435db19de192d9db522c7f7b4e201fc1b61a9bd3efd062ae24455d463818b01e2756c7d0691bc3ac4c017be34c9a8b2913bb1b94056bf7a21730afc3f254ffa41ca140a5d87ff470f536d08619e8004d50de2fe5954d6aa4a00570da397ba15ae9ea4d7d1f136256a9093f0a787a36cbb3520b6a3cf4d1b13b16bf399c4b0326da1382a90bd79cf92f4808c8c84eaa50a8ccf44acbde0e35b2e6b72858c8446d6a05f3ba70fb4adc70af27cea9bd1dc1ea35fb3cc236b8b9b69b614903db339b22ad5dc2ddda7ac65fd7de24e60b7dbba7aafc9d26c0f9fcb03f1bb85dfc21762f862620651db52ea703ae60aa7e07febf11caa95c4245a4b37eb9c233e1ab1604fb85849e7f49cb9f7c681c4d91b7c320eb4b89b9c6bcb636ceadda59f6ed47aa5b1ea0a946ea98f6ad2614e79e0d4ef96d6d65903adb0479709e03008bbdf3355dd87df7d68965fdf1ad5c68b6dc2761b96b10f8eb4c0329a646bf38cf4702002e61565231b4ac7a9cde63d23f7b24c9d42797b3c434558d71ed8bf9fcab2c2aee3e8b38c19f9edc3ad3dfe9ebba7387ce4764f97ed1c1a83552dff8315546761479a6f929c39bcca0891d4a967d1b77fa80feed6ae74ac82ed5fb7be225c3f2b0ebdc652afc2255c47bc318ac645bbf19c0819ff527ff6708a78e19c8ca3dc8087035e10d5ac976e84b71148586c8a5a7b26ed11b5b401ce7bb2ac532207eaa24d2f53aaa8024607da764d807c91489e82fcad04e6b8992a507119367f576ee5ffe6807d5723d60234d4c3f94adce0acfed9dba535ca375446a4e9b500b74ad2a66e1c6b0fc38933f282d3a4a877bceceeca52b46e731ca51a9534224a883c4a45587f973f73a22069a4154b1da03d307d8575c821bef0eef87165b9a1bbf902ecfca82ddd805d10fbb7147b496f6772f01e9bf542b00288f3a6efab32590c1f34535ece03a0587ca187d27a98d4c9aa7c044794baa43a81abbe307f51d0bda6e7b4cf62c4be553b176321777e7fd483d6cec16df137293aaf3ad53608e1c7831368675bb9608db04d5c859e7714edab3d2389837fa071f0795adfabc51507b1adbadc7f83e80bd4e4eb9ed1a89c9e0a6dc16f38d55181d5666b02150651961aab34faef97d80fa4e1960864dfec3b687fd4eadf7aa6c709cb4698ae86ae112f386f33731d996b9d41926a2e820c6ba483a61674a4bae03af37e872ffdc0a9a8a034327af17e13e9e7ac619c9188c2a5c12a6ebf887721455c0e2822e67a621ed49f1f50dfc38b71c29d0224954e84ced086c80de552cca3a14adbe43035901225bafc3db3b672c780e4fa12b59221f93690527efc16a28e7c63d1a99fc881f023b03a157076a7e999a715ed37521adb483e2477d75ba5a55d4abad22b024c5317334b6544f15971591c774d896229e4e668fc1c7958fbd76fa0b152a6f14c95692083badd066b6621367fd73d88ba8d860566e6d55b871d80c68296b80ae8847d'),
|
||||
packet.to_bytes())
|
||||
for i, privkey in enumerate(payment_path_privkeys):
|
||||
processed_packet = process_onion_packet(packet, associated_data, privkey)
|
||||
self.assertEqual(hops_data[i].per_hop.to_bytes(), processed_packet.hop_data.per_hop.to_bytes())
|
||||
self.assertEqual(hops_data[i].to_bytes(), processed_packet.hop_data.to_bytes())
|
||||
packet = processed_packet.next_packet
|
||||
|
||||
@needs_test_with_all_chacha20_implementations
|
||||
|
|
|
@ -8,7 +8,7 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see
|
|||
make_htlc_tx_inputs, secret_to_pubkey, derive_blinded_pubkey, derive_privkey,
|
||||
derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret,
|
||||
get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError,
|
||||
ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc)
|
||||
ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures)
|
||||
from electrum.util import bh2u, bfh, MyEncoder
|
||||
from electrum.transaction import Transaction, PartialTransaction
|
||||
|
||||
|
@ -755,3 +755,53 @@ class TestLNUtil(ElectrumTestCase):
|
|||
with self.assertRaises(ConnStringFormatError):
|
||||
extract_nodeid("00" * 33 + "@")
|
||||
self.assertEqual(extract_nodeid("00" * 33 + "@localhost"), (b"\x00" * 33, "localhost"))
|
||||
|
||||
def test_ln_features_validate_transitive_dependecies(self):
|
||||
features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
||||
self.assertTrue(features.validate_transitive_dependecies())
|
||||
features = LnFeatures.PAYMENT_SECRET_OPT
|
||||
self.assertFalse(features.validate_transitive_dependecies())
|
||||
features = LnFeatures.PAYMENT_SECRET_REQ
|
||||
self.assertFalse(features.validate_transitive_dependecies())
|
||||
features = LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ
|
||||
self.assertTrue(features.validate_transitive_dependecies())
|
||||
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ
|
||||
self.assertFalse(features.validate_transitive_dependecies())
|
||||
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT
|
||||
self.assertTrue(features.validate_transitive_dependecies())
|
||||
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ
|
||||
self.assertTrue(features.validate_transitive_dependecies())
|
||||
|
||||
def test_ln_features_for_init_message(self):
|
||||
features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
||||
self.assertEqual(features, features.for_init_message())
|
||||
features = LnFeatures.PAYMENT_SECRET_OPT
|
||||
self.assertEqual(features, features.for_init_message())
|
||||
features = LnFeatures.PAYMENT_SECRET_REQ
|
||||
self.assertEqual(features, features.for_init_message())
|
||||
features = LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ
|
||||
self.assertEqual(features, features.for_init_message())
|
||||
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ
|
||||
self.assertEqual(features, features.for_init_message())
|
||||
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT
|
||||
self.assertEqual(features, features.for_init_message())
|
||||
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ
|
||||
self.assertEqual(features, features.for_init_message())
|
||||
|
||||
def test_ln_features_for_invoice(self):
|
||||
features = LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
||||
self.assertEqual(LnFeatures(0), features.for_invoice())
|
||||
features = LnFeatures.PAYMENT_SECRET_OPT
|
||||
self.assertEqual(features, features.for_invoice())
|
||||
features = LnFeatures.PAYMENT_SECRET_REQ
|
||||
self.assertEqual(features, features.for_invoice())
|
||||
features = LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ
|
||||
self.assertEqual(features, features.for_invoice())
|
||||
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
||||
self.assertEqual(LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ,
|
||||
features.for_invoice())
|
||||
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
||||
self.assertEqual(LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_OPT,
|
||||
features.for_invoice())
|
||||
features = LnFeatures.BASIC_MPP_OPT | LnFeatures.PAYMENT_SECRET_REQ | LnFeatures.VAR_ONION_REQ
|
||||
self.assertEqual(features, features.for_invoice())
|
||||
|
|
Loading…
Add table
Reference in a new issue