electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 3ed6afce64b44431be87409e6ef64de8d29c979f
parent 79d202485e1747ad5d16cecb7c35db64ce502177
Author: SomberNight <somber.night@protonmail.com>
Date:   Thu, 26 Mar 2020 07:09:38 +0100

lnchannel: implement freezing channels (for receiving)

A bit weird, I know... :)
It allows for rebalancing our own channels! :P

Diffstat:
Melectrum/gui/qt/channels_list.py | 39++++++++++++++++++++++++---------------
Melectrum/lnchannel.py | 36++++++++++++++++++++++++++++--------
Melectrum/lnrouter.py | 5+++--
Melectrum/lnworker.py | 7+------
4 files changed, 56 insertions(+), 31 deletions(-)

diff --git a/electrum/gui/qt/channels_list.py b/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(), title=_("Long Channel ID"))) - if not chan.is_frozen(): - menu.addAction(_("Freeze"), lambda: chan.set_frozen(True)) + if not chan.is_frozen_for_sending(): + menu.addAction(_("Freeze (for sending)"), lambda: chan.set_frozen_for_sending(True)) 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) if funding_tx: @@ -212,18 +217,22 @@ class ChannelsList(MyTreeView): def _update_chan_frozen_bg(self, *, chan: Channel, items: Sequence[QStandardItem]): assert self._default_item_bg_brush is not None - for col in [ - self.Columns.LOCAL_BALANCE, - self.Columns.REMOTE_BALANCE, - self.Columns.CHANNEL_STATUS, - ]: - item = items[col] - if chan.is_frozen(): - item.setBackground(ColorScheme.BLUE.as_color(True)) - item.setToolTip(_("This channel is frozen. Frozen channels will not be used for outgoing payments.")) - else: - item.setBackground(self._default_item_bg_brush) - item.setToolTip("") + # frozen for sending + item = items[self.Columns.LOCAL_BALANCE] + if chan.is_frozen_for_sending(): + item.setBackground(ColorScheme.BLUE.as_color(True)) + item.setToolTip(_("This channel is frozen for sending. It will not be used for outgoing payments.")) + else: + item.setBackground(self._default_item_bg_brush) + item.setToolTip("") + # frozen for receiving + item = items[self.Columns.REMOTE_BALANCE] + if chan.is_frozen_for_receiving(): + 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): msg = _('Can send') + ' ' + self.parent.format_amount(lnworker.num_sats_can_send())\ diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py @@ -392,18 +392,30 @@ class Channel(Logger): def is_redeemed(self): return self.get_state() == channel_states.REDEEMED - def is_frozen(self) -> bool: - """Whether the user has marked this channel as frozen. + def is_frozen_for_sending(self) -> bool: + """Whether the user has marked this channel as frozen for sending. Frozen channels are not supposed to be used for new outgoing payments. (note that payment-forwarding ignores this option) """ 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) if self.lnworker: 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: """Raises PaymentFailure if the htlc_proposer cannot add this new HTLC. (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: raise PaymentFailure(f"HTLC value over protocol maximum: {amount_msat} > {LN_MAX_HTLC_VALUE_MSAT} msat") - def can_pay(self, amount_msat: int) -> bool: - """Returns whether we can initiate a new payment of given value. - (we are the payer, not just a forwarding node) - """ - if self.is_frozen(): + def can_pay(self, amount_msat: int, *, check_frozen=False) -> bool: + """Returns whether we can add an HTLC of given value.""" + if check_frozen and self.is_frozen_for_sending(): return False try: self._assert_can_add_htlc(htlc_proposer=LOCAL, amount_msat=amount_msat) @@ -449,6 +459,16 @@ class Channel(Logger): return False 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: return channel_states.PREOPENING < self._state < channel_states.FORCE_CLOSING and self.peer_state == peer_states.DISCONNECTED diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py @@ -205,11 +205,12 @@ class LNPathFinder(Logger): is_mine = edge_channel_id in my_channels if is_mine: 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 else: # payment incoming, on our channel. (funny business, cycle weirdness) 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_channel_id, start_node=edge_startnode, diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -1266,12 +1266,7 @@ class LNWallet(LNWorker): if chan.short_channel_id is not None} # note: currently we add *all* our channels; but this might be a privacy leak? for chan in channels: - # check channel is open - 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: + if not chan.can_receive(amount_sat, check_frozen=True): continue chan_id = chan.short_channel_id assert isinstance(chan_id, bytes), chan_id