Skip to content

Commit

Permalink
support scid alias:
Browse files Browse the repository at this point in the history
 - 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)
  • Loading branch information
ecdsa committed Jan 10, 2023
1 parent 438de3a commit 7ec4f00
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 17 deletions.
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
20 changes: 18 additions & 2 deletions electrum/lnpeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,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 @@ -706,6 +706,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 @@ -1281,16 +1283,30 @@ def send_channel_ready(self, chan: Channel):
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
Expand Down
27 changes: 19 additions & 8 deletions electrum/lnutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,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 @@ -1191,6 +1197,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 @@ -1208,13 +1216,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 @@ -1233,7 +1240,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 @@ -1252,6 +1262,7 @@ def name_minimal(self):
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_REQ
| 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
11 changes: 6 additions & 5 deletions electrum/lnworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class ErrorAddingPeer(Exception): pass
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT\
| LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT\
| LnFeatures.OPTION_CHANNEL_TYPE_OPT\
| LnFeatures.OPTION_SCID_ALIAS_OPT\

LNGOSSIP_FEATURES = BASE_FEATURES\
| LnFeatures.GOSSIP_QUERIES_OPT\
Expand Down Expand Up @@ -2021,9 +2022,9 @@ def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=No
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
Expand All @@ -2041,11 +2042,11 @@ def calc_routing_hints_for_invoice(self, amount_msat: Optional[int], channels=No
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)]))
Expand Down
4 changes: 4 additions & 0 deletions electrum/tests/test_lnpeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ def is_lightning_backup(self):
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
Expand Down Expand Up @@ -152,6 +155,7 @@ def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_que
self.features |= LnFeatures.PAYMENT_SECRET_OPT
self.features |= LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT
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
Expand Down

0 comments on commit 7ec4f00

Please sign in to comment.