Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for option_scid_alias #8135

Merged
merged 5 commits into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion electrum/channel_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from .lnmsg import decode_msg
from . import ecc
from .crypto import sha256d
from .lnmsg import FailedToParseMsg

if TYPE_CHECKING:
from .network import Network
Expand Down Expand Up @@ -725,18 +726,25 @@ def newest_ts_for_node_id(node_id):
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:
try:
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)
Expand Down
5 changes: 3 additions & 2 deletions electrum/gui/qt/channel_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,11 @@ def get_common_form(self, chan):
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():
Expand Down
12 changes: 12 additions & 0 deletions electrum/lnchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,18 @@ def __init__(self, state: 'StoredDict', *, name=None, lnworker=None, initial_fee
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)

Expand Down
30 changes: 11 additions & 19 deletions electrum/lnmsg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -243,17 +243,17 @@ 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


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)


Expand Down Expand Up @@ -454,10 +454,7 @@ def encode_msg(self, msg_type: str, **kwargs) -> bytes:
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,
Expand Down Expand Up @@ -507,15 +504,10 @@ def decode_msg(self, data: bytes) -> Tuple[str, dict]:
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:
Expand Down
4 changes: 2 additions & 2 deletions electrum/lnonion.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def to_bytes(self) -> bytes:
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:
Expand Down Expand Up @@ -157,7 +157,7 @@ def from_fd(cls, fd: io.BytesIO) -> 'OnionHopsDataSingle':
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
Expand Down
39 changes: 29 additions & 10 deletions electrum/lnpeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ def maybe_save_remote_update(self, payload):
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
Expand Down Expand Up @@ -552,7 +552,7 @@ def decode_short_ids(self, encoded):
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'])}")
Expand Down Expand Up @@ -712,6 +712,8 @@ async def channel_establishment_flow(
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():
Expand Down Expand Up @@ -1273,7 +1275,7 @@ def resend_revoke_and_ack():

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)
Expand All @@ -1282,20 +1284,37 @@ def resend_revoke_and_ack():
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)

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,
channel_ready_tlvs=channel_ready_tlvs)
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)}")
# 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["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)
Expand Down
27 changes: 19 additions & 8 deletions electrum/lnutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand All @@ -1215,13 +1223,12 @@ def discard_unknown_and_check(self):
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:
Expand All @@ -1240,7 +1247,10 @@ def to_bytes_minimal(self):

@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
Expand All @@ -1259,6 +1269,7 @@ def name_minimal(self):
| 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
)


Expand Down
4 changes: 3 additions & 1 deletion electrum/lnwire/README.md
Original file line number Diff line number Diff line change
@@ -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
47 changes: 26 additions & 21 deletions electrum/lnwire/onion_wire.csv
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Loading