From c109d5e7223c1fce4d34e1ea202314d896adbe32 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 7 Jan 2023 12:20:03 +0100 Subject: [PATCH 1/5] lnwire: update csv files with recent BOLTs Note: there are no more optional fields in msgdata, per https://github.com/lightning/bolts/commit/f068dd0d8dfa5ae75feedd99f269e23be4777381 --- electrum/lnonion.py | 4 +-- electrum/lnpeer.py | 21 ++++++++------- electrum/lnwire/README.md | 4 ++- electrum/lnwire/onion_wire.csv | 47 +++++++++++++++++++--------------- electrum/lnwire/peer_wire.csv | 27 +++++++++++-------- electrum/lnworker.py | 2 +- electrum/tests/test_lnmsg.py | 29 --------------------- 7 files changed, 60 insertions(+), 74 deletions(-) diff --git a/electrum/lnonion.py b/electrum/lnonion.py index cbf7ca2fd..8b353aeb8 100644 --- a/electrum/lnonion.py +++ b/electrum/lnonion.py @@ -122,7 +122,7 @@ class OnionHopsDataSingle: # called HopData in lnd else: # tlv payload_fd = io.BytesIO() OnionWireSerializer.write_tlv_stream(fd=payload_fd, - tlv_stream_name="tlv_payload", + tlv_stream_name="payload", **self.payload) payload_bytes = payload_fd.getvalue() with io.BytesIO() as fd: @@ -157,7 +157,7 @@ class OnionHopsDataSingle: # called HopData in lnd 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") + tlv_stream_name="payload") ret.hmac = fd.read(PER_HOP_HMAC_SIZE) assert len(ret.hmac) == PER_HOP_HMAC_SIZE return ret diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index f83db1ba5..273626295 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -552,7 +552,7 @@ class Peer(Logger): def on_reply_channel_range(self, payload): first = payload['first_blocknum'] num = payload['number_of_blocks'] - complete = bool(int.from_bytes(payload['complete'], 'big')) + complete = bool(int.from_bytes(payload['sync_complete'], 'big')) encoded = payload['encoded_short_ids'] ids = self.decode_short_ids(encoded) #self.logger.info(f"on_reply_channel_range. >>> first_block {first}, num_blocks {num}, num_ids {len(ids)}, complete {repr(payload['complete'])}") @@ -1273,7 +1273,7 @@ class Peer(Logger): chan.peer_state = PeerState.GOOD if chan.is_funded() and their_next_local_ctn == next_local_ctn == 1: - self.send_funding_locked(chan) + self.send_channel_ready(chan) # checks done if chan.is_funded() and chan.config[LOCAL].funding_locked_received: self.mark_open(chan) @@ -1282,20 +1282,23 @@ class Peer(Logger): if chan.get_state() == ChannelState.SHUTDOWN: await self.send_shutdown(chan) - def send_funding_locked(self, chan: Channel): + def send_channel_ready(self, chan: Channel): channel_id = chan.channel_id per_commitment_secret_index = RevocationStore.START_INDEX - 1 - per_commitment_point_second = secret_to_pubkey(int.from_bytes( + second_per_commitment_point = secret_to_pubkey(int.from_bytes( get_per_commitment_secret_from_seed(chan.config[LOCAL].per_commitment_secret_seed, per_commitment_secret_index), 'big')) - # note: if funding_locked was not yet received, we might send it multiple times - self.send_message("funding_locked", channel_id=channel_id, next_per_commitment_point=per_commitment_point_second) + # note: if 'channel_ready' was not yet received, we might send it multiple times + self.send_message( + "channel_ready", + channel_id=channel_id, + second_per_commitment_point=second_per_commitment_point) if chan.is_funded() and chan.config[LOCAL].funding_locked_received: self.mark_open(chan) - def on_funding_locked(self, chan: Channel, payload): - self.logger.info(f"on_funding_locked. channel: {bh2u(chan.channel_id)}") + def on_channel_ready(self, chan: Channel, payload): + self.logger.info(f"on_channel_ready. channel: {bh2u(chan.channel_id)}") if not chan.config[LOCAL].funding_locked_received: - their_next_point = payload["next_per_commitment_point"] + their_next_point = payload["second_per_commitment_point"] chan.config[REMOTE].next_per_commitment_point = their_next_point chan.config[LOCAL].funding_locked_received = True self.lnworker.save_channel(chan) diff --git a/electrum/lnwire/README.md b/electrum/lnwire/README.md index 72fd48f37..2d9ef69c0 100644 --- a/electrum/lnwire/README.md +++ b/electrum/lnwire/README.md @@ -1,5 +1,7 @@ -These files are generated from the BOLT repository: +These files have been 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 ``` + +Note: Trampoline messages were added manually to onion_wire.csv diff --git a/electrum/lnwire/onion_wire.csv b/electrum/lnwire/onion_wire.csv index 3001f3f62..07b10cdd8 100644 --- a/electrum/lnwire/onion_wire.csv +++ b/electrum/lnwire/onion_wire.csv @@ -1,23 +1,25 @@ -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, -tlvtype,tlv_payload,invoice_features,66097 -tlvdata,tlv_payload,invoice_features,invoice_features,u64, -tlvtype,tlv_payload,outgoing_node_id,66098 -tlvdata,tlv_payload,outgoing_node_id,outgoing_node_id,byte,33 -tlvtype,tlv_payload,invoice_routing_info,66099 -tlvdata,tlv_payload,invoice_routing_info,invoice_routing_info,byte,... -tlvtype,tlv_payload,trampoline_onion_packet,66100 -tlvdata,tlv_payload,trampoline_onion_packet,version,byte,1 -tlvdata,tlv_payload,trampoline_onion_packet,public_key,byte,33 -tlvdata,tlv_payload,trampoline_onion_packet,hops_data,byte,400 -tlvdata,tlv_payload,trampoline_onion_packet,hmac,byte,32 +tlvtype,payload,amt_to_forward,2 +tlvdata,payload,amt_to_forward,amt_to_forward,tu64, +tlvtype,payload,outgoing_cltv_value,4 +tlvdata,payload,outgoing_cltv_value,outgoing_cltv_value,tu32, +tlvtype,payload,short_channel_id,6 +tlvdata,payload,short_channel_id,short_channel_id,short_channel_id, +tlvtype,payload,payment_data,8 +tlvdata,payload,payment_data,payment_secret,byte,32 +tlvdata,payload,payment_data,total_msat,tu64, +tlvtype,payload,payment_metadata,16 +tlvdata,payload,payment_metadata,payment_metadata,byte,... +tlvtype,payload,invoice_features,66097 +tlvdata,payload,invoice_features,invoice_features,u64, +tlvtype,payload,outgoing_node_id,66098 +tlvdata,payload,outgoing_node_id,outgoing_node_id,byte,33 +tlvtype,payload,invoice_routing_info,66099 +tlvdata,payload,invoice_routing_info,invoice_routing_info,byte,... +tlvtype,payload,trampoline_onion_packet,66100 +tlvdata,payload,trampoline_onion_packet,version,byte,1 +tlvdata,payload,trampoline_onion_packet,public_key,byte,33 +tlvdata,payload,trampoline_onion_packet,hops_data,byte,400 +tlvdata,payload,trampoline_onion_packet,hmac,byte,32 msgtype,invalid_realm,PERM|1 msgtype,temporary_node_failure,NODE|2 msgtype,permanent_node_failure,PERM|NODE|2 @@ -57,8 +59,11 @@ 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 +msgdata,channel_disabled,disabled_flags,u16, +msgdata,channel_disabled,len,u16, +msgdata,channel_disabled,channel_update,byte,len msgtype,expiry_too_far,21 msgtype,invalid_onion_payload,PERM|22 -msgdata,invalid_onion_payload,type,varint, +msgdata,invalid_onion_payload,type,bigsize, msgdata,invalid_onion_payload,offset,u16, msgtype,mpp_timeout,23 diff --git a/electrum/lnwire/peer_wire.csv b/electrum/lnwire/peer_wire.csv index 87bd89f77..b4018f0fc 100644 --- a/electrum/lnwire/peer_wire.csv +++ b/electrum/lnwire/peer_wire.csv @@ -6,6 +6,8 @@ msgdata,init,features,byte,flen msgdata,init,tlvs,init_tlvs, tlvtype,init_tlvs,networks,1 tlvdata,init_tlvs,networks,chains,chain_hash,... +tlvtype,init_tlvs,remote_addr,3 +tlvdata,init_tlvs,remote_addr,data,byte,... msgtype,error,17 msgdata,error,channel_id,channel_id, msgdata,error,len,u16, @@ -87,9 +89,12 @@ 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,channel_ready,36 +msgdata,channel_ready,channel_id,channel_id, +msgdata,channel_ready,second_per_commitment_point,point, +msgdata,channel_ready,tlvs,channel_ready_tlvs, +tlvtype,channel_ready_tlvs,short_channel_id,1 +tlvdata,channel_ready_tlvs,short_channel_id,alias,short_channel_id, msgtype,shutdown,38 msgdata,shutdown,channel_id,channel_id, msgdata,shutdown,len,u16, @@ -139,8 +144,8 @@ 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 +msgdata,channel_reestablish,your_last_per_commitment_secret,byte,32 +msgdata,channel_reestablish,my_current_per_commitment_point,point, msgtype,announcement_signatures,259 msgdata,announcement_signatures,channel_id,channel_id, msgdata,announcement_signatures,short_channel_id,short_channel_id, @@ -180,35 +185,35 @@ 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 +msgdata,channel_update,htlc_maximum_msat,u64, 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,encoding_type,byte, 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, +msgdata,reply_short_channel_ids_end,full_information,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, +tlvdata,query_channel_range_tlvs,query_option,query_option_flags,bigsize, 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,sync_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,encoding_type,byte, 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,... diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 1290396d3..9e01c9ebf 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1002,7 +1002,7 @@ class LNWallet(LNWorker): elif chan.get_state() == ChannelState.FUNDED: peer = self._peers.get(chan.node_id) if peer and peer.is_initialized(): - peer.send_funding_locked(chan) + peer.send_channel_ready(chan) elif chan.get_state() == ChannelState.OPEN: peer = self._peers.get(chan.node_id) diff --git a/electrum/tests/test_lnmsg.py b/electrum/tests/test_lnmsg.py index 790a481bf..57e8f2ea6 100644 --- a/electrum/tests/test_lnmsg.py +++ b/electrum/tests/test_lnmsg.py @@ -213,35 +213,6 @@ class TestLNMsg(TestCaseForTestnet): ), 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( From 020637b5d491a4bc7ba78017f38baf7d5a9e7f99 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 13 Jan 2023 15:45:20 +0100 Subject: [PATCH 2/5] =?UTF-8?q?fix=20field=20type:=20'varint'=20was=20reb?= =?UTF-8?q?=C5=84amed=20to=20'bigsize'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electrum/lnmsg.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/electrum/lnmsg.py b/electrum/lnmsg.py index b3db73944..a0403f7f7 100644 --- a/electrum/lnmsg.py +++ b/electrum/lnmsg.py @@ -129,7 +129,7 @@ def _read_field(*, fd: io.BytesIO, field_type: str, count: Union[int, str]) -> U if len(raw) > 0 and raw[0] == 0x00: raise FieldEncodingNotMinimal() return int.from_bytes(raw, byteorder="big", signed=False) - elif field_type == 'varint': + elif field_type == 'bigsize': assert count == 1, count val = read_bigsize_int(fd) if val is None: @@ -203,7 +203,7 @@ def _write_field(*, fd: io.BytesIO, field_type: str, count: Union[int, str], if nbytes_written != len(value): raise Exception(f"tried to write {len(value)} bytes, but only wrote {nbytes_written}!?") return - elif field_type == 'varint': + elif field_type == 'bigsize': assert count == 1, count if isinstance(value, int): value = write_bigsize_int(value) @@ -243,8 +243,8 @@ def _write_field(*, fd: io.BytesIO, field_type: str, count: Union[int, str], 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_type = _read_field(fd=fd, field_type="bigsize", count=1) + tlv_len = _read_field(fd=fd, field_type="bigsize", count=1) tlv_val = _read_field(fd=fd, field_type="byte", count=tlv_len) return tlv_type, tlv_val @@ -252,8 +252,8 @@ def _read_tlv_record(*, fd: io.BytesIO) -> Tuple[int, bytes]: 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="bigsize", count=1, value=tlv_type) + _write_field(fd=fd, field_type="bigsize", count=1, value=tlv_len) _write_field(fd=fd, field_type="byte", count=tlv_len, value=tlv_val) From 89df68d66a739e4e4af9fb9b5c80f8d8a85b31a0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 13 Jan 2023 15:47:02 +0100 Subject: [PATCH 3/5] channel_db: protect load_db against raw messages that can no longer be parsed --- electrum/channel_db.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/electrum/channel_db.py b/electrum/channel_db.py index a8e2452df..bb2213570 100644 --- a/electrum/channel_db.py +++ b/electrum/channel_db.py @@ -46,6 +46,7 @@ from .lnverifier import LNChannelVerifier, verify_sig_for_channel_update from .lnmsg import decode_msg from . import ecc from .crypto import sha256d +from .lnmsg import FailedToParseMsg if TYPE_CHECKING: from .network import Network @@ -725,6 +726,8 @@ class ChannelDB(SqlDB): ci = ChannelInfo.from_raw_msg(msg) except IncompatibleOrInsaneFeatures: continue + except FailedToParseMsg: + continue self._channels[ShortChannelID.normalize(short_channel_id)] = ci c.execute("""SELECT * FROM node_info""") for node_id, msg in c: @@ -732,11 +735,16 @@ class ChannelDB(SqlDB): node_info, node_addresses = NodeInfo.from_raw_msg(msg) except IncompatibleOrInsaneFeatures: continue + except FailedToParseMsg: + continue # don't load node_addresses because they dont have timestamps self._nodes[node_id] = node_info c.execute("""SELECT * FROM policy""") for key, msg in c: - p = Policy.from_raw_msg(key, msg) + try: + p = Policy.from_raw_msg(key, msg) + except FailedToParseMsg: + continue self._policies[(p.start_node, p.short_channel_id)] = p for channel_info in self._channels.values(): self._channels_for_node[channel_info.node1_id].add(channel_info.short_channel_id) From b9393b06034435e4b2e3280df5f6e1103d0821a2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 7 Jan 2023 12:20:03 +0100 Subject: [PATCH 4/5] Support scid alias: - save remote alias for use in invoices - derive local alias from wallet xpub - send channel_type without the option_scid_alias bit (apparently LND does not like it) --- electrum/gui/qt/channel_details.py | 5 +++-- electrum/lnchannel.py | 12 ++++++++++++ electrum/lnpeer.py | 20 ++++++++++++++++++-- electrum/lnutil.py | 27 +++++++++++++++++++-------- electrum/lnworker.py | 17 ++++++++++++----- electrum/tests/test_lnpeer.py | 4 ++++ 6 files changed, 68 insertions(+), 17 deletions(-) diff --git a/electrum/gui/qt/channel_details.py b/electrum/gui/qt/channel_details.py index d60a211cb..f49a851cc 100644 --- a/electrum/gui/qt/channel_details.py +++ b/electrum/gui/qt/channel_details.py @@ -180,10 +180,11 @@ class ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin, QtEventListener): form.addRow(QLabel(_('Remote Node') + ':'), remote_id_e) channel_id_e = ShowQRLineEdit(chan.channel_id.hex(), self.window.config, title=_("Channel ID")) form.addRow(QLabel(_('Channel ID') + ':'), channel_id_e) - form.addRow(QLabel(_('Short Channel ID') + ':'), QLabel(str(chan.short_channel_id))) + alias = chan.get_remote_alias() + if alias: + form.addRow(QLabel(_('Alias') + ':'), QLabel('0x'+alias.hex())) form.addRow(QLabel(_('State') + ':'), SelectableLabel(chan.get_state_for_GUI())) - self.capacity = self.format_sat(chan.get_capacity()) form.addRow(QLabel(_('Capacity') + ':'), SelectableLabel(self.capacity)) if not chan.is_backup(): diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index 1fe499110..3eefe48f5 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -614,6 +614,18 @@ class Channel(AbstractChannel): self.should_request_force_close = False self.unconfirmed_closing_txid = None # not a state, only for GUI + def get_local_alias(self) -> bytes: + # deterministic, same secrecy level as wallet master pubkey + wallet_fingerprint = bytes(self.lnworker.wallet.get_fingerprint(), "utf8") + return sha256(wallet_fingerprint + self.channel_id)[0:8] + + def save_remote_alias(self, alias: bytes): + self.storage['alias'] = alias.hex() + + def get_remote_alias(self) -> Optional[bytes]: + alias = self.storage.get('alias') + return bytes.fromhex(alias) if alias else None + def has_onchain_backup(self): return self.storage.get('has_onchain_backup', False) diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 273626295..d1151441b 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -388,7 +388,7 @@ class Peer(Logger): if not self.channels: return for chan in self.channels.values(): - if chan.short_channel_id == payload['short_channel_id']: + if payload['short_channel_id'] in [chan.short_channel_id, chan.get_local_alias()]: chan.set_remote_update(payload) self.logger.info(f"saved remote channel_update gossip msg for chan {chan.get_id_for_log()}") break @@ -712,6 +712,8 @@ class Peer(Logger): open_channel_tlvs = {} assert self.their_features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT) our_channel_type = ChannelType(ChannelType.OPTION_STATIC_REMOTEKEY) + # We do not set the option_scid_alias bit in channel_type because LND rejects it. + # Eclair accepts channel_type with that bit, but does not require it. # if option_channel_type is negotiated: MUST set channel_type if self.is_channel_type(): @@ -1287,16 +1289,30 @@ class Peer(Logger): per_commitment_secret_index = RevocationStore.START_INDEX - 1 second_per_commitment_point = secret_to_pubkey(int.from_bytes( get_per_commitment_secret_from_seed(chan.config[LOCAL].per_commitment_secret_seed, per_commitment_secret_index), 'big')) + + channel_ready_tlvs = {} + if self.their_features.supports(LnFeatures.OPTION_SCID_ALIAS_OPT): + # LND requires that we send an alias if the option has been negotiated in INIT. + # otherwise, the channel will not be marked as active. + # This does not apply if the channel was previously marked active without an alias. + channel_ready_tlvs['short_channel_id'] = {'alias':chan.get_local_alias()} + # note: if 'channel_ready' was not yet received, we might send it multiple times self.send_message( "channel_ready", channel_id=channel_id, - second_per_commitment_point=second_per_commitment_point) + second_per_commitment_point=second_per_commitment_point, + channel_ready_tlvs=channel_ready_tlvs) if chan.is_funded() and chan.config[LOCAL].funding_locked_received: self.mark_open(chan) def on_channel_ready(self, chan: Channel, payload): self.logger.info(f"on_channel_ready. channel: {bh2u(chan.channel_id)}") + # save remote alias for use in invoices + scid_alias = payload.get('channel_ready_tlvs', {}).get('short_channel_id', {}).get('alias') + if scid_alias: + chan.save_remote_alias(scid_alias) + if not chan.config[LOCAL].funding_locked_received: their_next_point = payload["second_per_commitment_point"] chan.config[REMOTE].next_per_commitment_point = their_next_point diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 9a5012f8d..2b1f3a7ce 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1116,6 +1116,12 @@ class LnFeatures(IntFlag): _ln_feature_contexts[OPTION_CHANNEL_TYPE_REQ] = (LNFC.INIT | LNFC.NODE_ANN) _ln_feature_contexts[OPTION_CHANNEL_TYPE_OPT] = (LNFC.INIT | LNFC.NODE_ANN) + OPTION_SCID_ALIAS_REQ = 1 << 46 + OPTION_SCID_ALIAS_OPT = 1 << 47 + + _ln_feature_contexts[OPTION_SCID_ALIAS_REQ] = (LNFC.INIT | LNFC.NODE_ANN) + _ln_feature_contexts[OPTION_SCID_ALIAS_OPT] = (LNFC.INIT | LNFC.NODE_ANN) + def validate_transitive_dependencies(self) -> bool: # for all even bit set, set corresponding odd bit: features = self # copy @@ -1198,6 +1204,8 @@ class ChannelType(IntFlag): OPTION_STATIC_REMOTEKEY = 1 << 12 OPTION_ANCHOR_OUTPUTS = 1 << 20 OPTION_ANCHORS_ZERO_FEE_HTLC_TX = 1 << 22 + OPTION_SCID_ALIAS = 1 << 46 + OPTION_ZEROCONF = 1 << 50 def discard_unknown_and_check(self): """Discards unknown flags and checks flag combination.""" @@ -1215,13 +1223,12 @@ class ChannelType(IntFlag): return final_channel_type def check_combinations(self): - if self == ChannelType.OPTION_STATIC_REMOTEKEY: - pass - elif self == ChannelType.OPTION_ANCHOR_OUTPUTS | ChannelType.OPTION_STATIC_REMOTEKEY: - pass - elif self == ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX | ChannelType.OPTION_STATIC_REMOTEKEY: - pass - else: + basic_type = self & ~(ChannelType.OPTION_SCID_ALIAS | ChannelType.OPTION_ZEROCONF) + if basic_type not in [ + ChannelType.OPTION_STATIC_REMOTEKEY, + ChannelType.OPTION_ANCHOR_OUTPUTS | ChannelType.OPTION_STATIC_REMOTEKEY, + ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX | ChannelType.OPTION_STATIC_REMOTEKEY + ]: raise ValueError("Channel type is not a valid flag combination.") def complies_with_features(self, features: LnFeatures) -> bool: @@ -1240,7 +1247,10 @@ class ChannelType(IntFlag): @property def name_minimal(self): - return self.name.replace('OPTION_', '') + if self.name: + return self.name.replace('OPTION_', '') + else: + return str(self) del LNFC # name is ambiguous without context @@ -1259,6 +1269,7 @@ LN_FEATURES_IMPLEMENTED = ( | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM | LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ_ELECTRUM | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ | LnFeatures.OPTION_CHANNEL_TYPE_OPT | LnFeatures.OPTION_CHANNEL_TYPE_REQ + | LnFeatures.OPTION_SCID_ALIAS_OPT | LnFeatures.OPTION_SCID_ALIAS_REQ ) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 9e01c9ebf..2c869f20d 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -187,6 +187,7 @@ LNWALLET_FEATURES = BASE_FEATURES\ | LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM\ | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT\ | LnFeatures.OPTION_CHANNEL_TYPE_OPT\ + | LnFeatures.OPTION_SCID_ALIAS_OPT\ LNGOSSIP_FEATURES = BASE_FEATURES\ | LnFeatures.GOSSIP_QUERIES_OPT\ @@ -1361,6 +1362,7 @@ class LNWallet(LNWorker): # send a single htlc short_channel_id = route[0].short_channel_id chan = self.get_channel_by_short_id(short_channel_id) + assert chan, ShortChannelID(short_channel_id) peer = self._peers.get(route[0].node_id) if not peer: raise PaymentFailure('Dropped peer') @@ -1708,6 +1710,7 @@ class LNWallet(LNWorker): my_sending_channels: List[Channel], full_path: Optional[LNPaymentPath]) -> LNPaymentRoute: + my_sending_aliases = set(chan.get_local_alias() for chan in my_sending_channels) my_sending_channels = {chan.short_channel_id: chan for chan in my_sending_channels if chan.short_channel_id is not None} # Collect all private edges from route hints. @@ -1719,6 +1722,10 @@ class LNWallet(LNWorker): private_path_nodes = [edge[0] for edge in private_path][1:] + [invoice_pubkey] private_path_rest = [edge[1:] for edge in private_path] start_node = private_path[0][0] + # remove aliases from direct routes + if len(private_path) == 1 and private_path[0][1] in my_sending_aliases: + self.logger.info(f'create_route: skipping alias {ShortChannelID(private_path[0][1])}') + continue for end_node, edge_rest in zip(private_path_nodes, private_path_rest): short_channel_id, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta = edge_rest short_channel_id = ShortChannelID(short_channel_id) @@ -2024,9 +2031,9 @@ class LNWallet(LNWorker): scid_to_my_channels = {chan.short_channel_id: chan for chan in channels if chan.short_channel_id is not None} for chan in channels: - chan_id = chan.short_channel_id - assert isinstance(chan_id, bytes), chan_id - channel_info = get_mychannel_info(chan_id, scid_to_my_channels) + alias_or_scid = chan.get_remote_alias() or chan.short_channel_id + assert isinstance(alias_or_scid, bytes), alias_or_scid + channel_info = get_mychannel_info(chan.short_channel_id, scid_to_my_channels) # note: as a fallback, if we don't have a channel update for the # incoming direction of our private channel, we fill the invoice with garbage. # the sender should still be able to pay us, but will incur an extra round trip @@ -2044,11 +2051,11 @@ class LNWallet(LNWorker): missing_info = False if missing_info: self.logger.info( - f"Warning. Missing channel update for our channel {chan_id}; " + f"Warning. Missing channel update for our channel {chan.short_channel_id}; " f"filling invoice with incorrect data.") routing_hints.append(('r', [( chan.node_id, - chan_id, + alias_or_scid, fee_base_msat, fee_proportional_millionths, cltv_expiry_delta)])) diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index df2c930c4..1aa2510bb 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -123,6 +123,9 @@ class MockWallet: def is_mine(self, addr): return True + def get_fingerprint(self): + return '' + class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): MPP_EXPIRY = 2 # HTLC timestamps are cast to int, so this cannot be 1 @@ -152,6 +155,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): self.features |= LnFeatures.PAYMENT_SECRET_OPT self.features |= LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT_ELECTRUM self.features |= LnFeatures.OPTION_CHANNEL_TYPE_OPT + self.features |= LnFeatures.OPTION_SCID_ALIAS_OPT self.pending_payments = defaultdict(asyncio.Future) for chan in chans: chan.lnworker = self From e74857f1ce4e2b5e33f0238ffdb55d939c4886fa Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 13 Jan 2023 16:11:11 +0100 Subject: [PATCH 5/5] lnmsg: remove handling of optional fields in msgdata --- electrum/lnmsg.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/electrum/lnmsg.py b/electrum/lnmsg.py index a0403f7f7..1ff50dc54 100644 --- a/electrum/lnmsg.py +++ b/electrum/lnmsg.py @@ -454,10 +454,7 @@ class LNSerializer: 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 + 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, @@ -507,15 +504,10 @@ class LNSerializer: 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 + parsed[field_name] = _read_field( + fd=fd, + field_type=field_type, + count=field_count) else: raise Exception(f"unexpected row in scheme: {row!r}") except FailedToParseMsg as e: