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)
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)]))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user