Browse Source

lnchannel: implement freezing channels (for receiving)

A bit weird, I know... :)
It allows for rebalancing our own channels! :P
hard-fail-on-bad-server-string
SomberNight 5 years ago
parent
commit
3ed6afce64
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 39
      electrum/gui/qt/channels_list.py
  2. 36
      electrum/lnchannel.py
  3. 5
      electrum/lnrouter.py
  4. 7
      electrum/lnworker.py

39
electrum/gui/qt/channels_list.py

@ -146,10 +146,15 @@ class ChannelsList(MyTreeView):
cc.addAction(_("Long Channel ID"), lambda: self.place_text_on_clipboard(channel_id.hex(), cc.addAction(_("Long Channel ID"), lambda: self.place_text_on_clipboard(channel_id.hex(),
title=_("Long Channel ID"))) title=_("Long Channel ID")))
if not chan.is_frozen(): if not chan.is_frozen_for_sending():
menu.addAction(_("Freeze"), lambda: chan.set_frozen(True)) menu.addAction(_("Freeze (for sending)"), lambda: chan.set_frozen_for_sending(True))
else: else:
menu.addAction(_("Unfreeze"), lambda: chan.set_frozen(False)) menu.addAction(_("Unfreeze (for sending)"), lambda: chan.set_frozen_for_sending(False))
if not chan.is_frozen_for_receiving():
menu.addAction(_("Freeze (for receiving)"), lambda: chan.set_frozen_for_receiving(True))
else:
menu.addAction(_("Unfreeze (for receiving)"), lambda: chan.set_frozen_for_receiving(False))
funding_tx = self.parent.wallet.db.get_transaction(chan.funding_outpoint.txid) funding_tx = self.parent.wallet.db.get_transaction(chan.funding_outpoint.txid)
if funding_tx: if funding_tx:
@ -212,18 +217,22 @@ class ChannelsList(MyTreeView):
def _update_chan_frozen_bg(self, *, chan: Channel, items: Sequence[QStandardItem]): def _update_chan_frozen_bg(self, *, chan: Channel, items: Sequence[QStandardItem]):
assert self._default_item_bg_brush is not None assert self._default_item_bg_brush is not None
for col in [ # frozen for sending
self.Columns.LOCAL_BALANCE, item = items[self.Columns.LOCAL_BALANCE]
self.Columns.REMOTE_BALANCE, if chan.is_frozen_for_sending():
self.Columns.CHANNEL_STATUS, item.setBackground(ColorScheme.BLUE.as_color(True))
]: item.setToolTip(_("This channel is frozen for sending. It will not be used for outgoing payments."))
item = items[col] else:
if chan.is_frozen(): item.setBackground(self._default_item_bg_brush)
item.setBackground(ColorScheme.BLUE.as_color(True)) item.setToolTip("")
item.setToolTip(_("This channel is frozen. Frozen channels will not be used for outgoing payments.")) # frozen for receiving
else: item = items[self.Columns.REMOTE_BALANCE]
item.setBackground(self._default_item_bg_brush) if chan.is_frozen_for_receiving():
item.setToolTip("") item.setBackground(ColorScheme.BLUE.as_color(True))
item.setToolTip(_("This channel is frozen for receiving. It will not be included in invoices."))
else:
item.setBackground(self._default_item_bg_brush)
item.setToolTip("")
def update_can_send(self, lnworker: LNWallet): def update_can_send(self, lnworker: LNWallet):
msg = _('Can send') + ' ' + self.parent.format_amount(lnworker.num_sats_can_send())\ msg = _('Can send') + ' ' + self.parent.format_amount(lnworker.num_sats_can_send())\

36
electrum/lnchannel.py

@ -392,18 +392,30 @@ class Channel(Logger):
def is_redeemed(self): def is_redeemed(self):
return self.get_state() == channel_states.REDEEMED return self.get_state() == channel_states.REDEEMED
def is_frozen(self) -> bool: def is_frozen_for_sending(self) -> bool:
"""Whether the user has marked this channel as frozen. """Whether the user has marked this channel as frozen for sending.
Frozen channels are not supposed to be used for new outgoing payments. Frozen channels are not supposed to be used for new outgoing payments.
(note that payment-forwarding ignores this option) (note that payment-forwarding ignores this option)
""" """
return self.storage.get('frozen_for_sending', False) return self.storage.get('frozen_for_sending', False)
def set_frozen(self, b: bool) -> None: def set_frozen_for_sending(self, b: bool) -> None:
self.storage['frozen_for_sending'] = bool(b) self.storage['frozen_for_sending'] = bool(b)
if self.lnworker: if self.lnworker:
self.lnworker.network.trigger_callback('channel', self) self.lnworker.network.trigger_callback('channel', self)
def is_frozen_for_receiving(self) -> bool:
"""Whether the user has marked this channel as frozen for receiving.
Frozen channels are not supposed to be used for new incoming payments.
(note that payment-forwarding ignores this option)
"""
return self.storage.get('frozen_for_receiving', False)
def set_frozen_for_receiving(self, b: bool) -> None:
self.storage['frozen_for_receiving'] = bool(b)
if self.lnworker:
self.lnworker.network.trigger_callback('channel', self)
def _assert_can_add_htlc(self, *, htlc_proposer: HTLCOwner, amount_msat: int) -> None: def _assert_can_add_htlc(self, *, htlc_proposer: HTLCOwner, amount_msat: int) -> None:
"""Raises PaymentFailure if the htlc_proposer cannot add this new HTLC. """Raises PaymentFailure if the htlc_proposer cannot add this new HTLC.
(this is relevant both for forwarding and endpoint) (this is relevant both for forwarding and endpoint)
@ -437,11 +449,9 @@ class Channel(Logger):
if amount_msat > LN_MAX_HTLC_VALUE_MSAT and not self._ignore_max_htlc_value: if amount_msat > LN_MAX_HTLC_VALUE_MSAT and not self._ignore_max_htlc_value:
raise PaymentFailure(f"HTLC value over protocol maximum: {amount_msat} > {LN_MAX_HTLC_VALUE_MSAT} msat") raise PaymentFailure(f"HTLC value over protocol maximum: {amount_msat} > {LN_MAX_HTLC_VALUE_MSAT} msat")
def can_pay(self, amount_msat: int) -> bool: def can_pay(self, amount_msat: int, *, check_frozen=False) -> bool:
"""Returns whether we can initiate a new payment of given value. """Returns whether we can add an HTLC of given value."""
(we are the payer, not just a forwarding node) if check_frozen and self.is_frozen_for_sending():
"""
if self.is_frozen():
return False return False
try: try:
self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=amount_msat) self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=amount_msat)
@ -449,6 +459,16 @@ class Channel(Logger):
return False return False
return True return True
def can_receive(self, amount_msat: int, *, check_frozen=False) -> bool:
"""Returns whether the remote can add an HTLC of given value."""
if check_frozen and self.is_frozen_for_receiving():
return False
try:
self._assert_can_add_htlc(htlc_proposer=REMOTE, amount_msat=amount_msat)
except PaymentFailure:
return False
return True
def should_try_to_reestablish_peer(self) -> bool: def should_try_to_reestablish_peer(self) -> bool:
return channel_states.PREOPENING < self._state < channel_states.FORCE_CLOSING and self.peer_state == peer_states.DISCONNECTED return channel_states.PREOPENING < self._state < channel_states.FORCE_CLOSING and self.peer_state == peer_states.DISCONNECTED

5
electrum/lnrouter.py

@ -205,11 +205,12 @@ class LNPathFinder(Logger):
is_mine = edge_channel_id in my_channels is_mine = edge_channel_id in my_channels
if is_mine: if is_mine:
if edge_startnode == nodeA: # payment outgoing, on our channel if edge_startnode == nodeA: # payment outgoing, on our channel
if not my_channels[edge_channel_id].can_pay(amount_msat): if not my_channels[edge_channel_id].can_pay(amount_msat, check_frozen=True):
return return
else: # payment incoming, on our channel. (funny business, cycle weirdness) else: # payment incoming, on our channel. (funny business, cycle weirdness)
assert edge_endnode == nodeA, (bh2u(edge_startnode), bh2u(edge_endnode)) assert edge_endnode == nodeA, (bh2u(edge_startnode), bh2u(edge_endnode))
pass # TODO? if not my_channels[edge_channel_id].can_receive(amount_msat, check_frozen=True):
return
edge_cost, fee_for_edge_msat = self._edge_cost( edge_cost, fee_for_edge_msat = self._edge_cost(
edge_channel_id, edge_channel_id,
start_node=edge_startnode, start_node=edge_startnode,

7
electrum/lnworker.py

@ -1266,12 +1266,7 @@ class LNWallet(LNWorker):
if chan.short_channel_id is not None} if chan.short_channel_id is not None}
# note: currently we add *all* our channels; but this might be a privacy leak? # note: currently we add *all* our channels; but this might be a privacy leak?
for chan in channels: for chan in channels:
# check channel is open if not chan.can_receive(amount_sat, check_frozen=True):
if chan.get_state() != channel_states.OPEN:
continue
# check channel has sufficient balance
# FIXME because of on-chain fees of ctx, this check is insufficient
if amount_sat and chan.balance(REMOTE) // 1000 < amount_sat:
continue continue
chan_id = chan.short_channel_id chan_id = chan.short_channel_id
assert isinstance(chan_id, bytes), chan_id assert isinstance(chan_id, bytes), chan_id

Loading…
Cancel
Save