electrum

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

commit c570bc5fb1a9479264d8c36816349b0398d52991
parent 0ea87278fb18a535c5ba35eb542ea7dfd5672f18
Author: Janus <ysangkok@gmail.com>
Date:   Mon,  5 Nov 2018 17:23:49 +0100

avoid leaving FORCE_CLOSING state, rebroadcast closing tx if reorged out

Diffstat:
Melectrum/gui/qt/channels_list.py | 4+++-
Melectrum/lnbase.py | 24+++---------------------
Melectrum/lnchan.py | 25++++++++++++++++++++++++-
Melectrum/lnworker.py | 17++++++++++++++---
4 files changed, 44 insertions(+), 26 deletions(-)

diff --git a/electrum/gui/qt/channels_list.py b/electrum/gui/qt/channels_list.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import traceback import asyncio from PyQt5 import QtCore, QtWidgets from PyQt5.QtWidgets import * @@ -48,7 +49,8 @@ class ChannelsList(MyTreeWidget): def on_success(txid): self.main_window.show_error('Channel closed' + '\n' + txid) def on_failure(exc_info): - type_, e, traceback = exc_info + type_, e, tb = exc_info + traceback.print_tb(tb) self.main_window.show_error('Failed to close channel:\n{}'.format(repr(e))) def close(): def task(): diff --git a/electrum/lnbase.py b/electrum/lnbase.py @@ -377,7 +377,8 @@ class Peer(PrintError): except: pass for chan in self.channels.values(): - chan.set_state('DISCONNECTED') + if chan.get_state() != 'FORCE_CLOSING': + chan.set_state('DISCONNECTED') self.network.trigger_callback('channel', chan) def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> Tuple[ChannelConfig, bytes]: @@ -442,7 +443,7 @@ class Peer(PrintError): ) payload = await self.channel_accepted[temp_channel_id].get() if payload.get('error'): - raise Exception(payload.get('error')) + raise Exception('Remote Lightning peer reported error: ' + repr(payload.get('error'))) remote_per_commitment_point = payload['first_per_commitment_point'] funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big') remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') @@ -1158,25 +1159,6 @@ class Peer(PrintError): self.print_error('Channel closed', txid) return txid - async def force_close_channel(self, chan_id): - chan = self.channels[chan_id] - # local_commitment always gives back the next expected local_commitment, - # but in this case, we want the current one. So substract one ctn number - old_local_state = chan.config[LOCAL] - chan.config[LOCAL]=chan.config[LOCAL]._replace(ctn=chan.config[LOCAL].ctn - 1) - tx = chan.pending_local_commitment - chan.config[LOCAL] = old_local_state - tx.sign({bh2u(chan.config[LOCAL].multisig_key.pubkey): (chan.config[LOCAL].multisig_key.privkey, True)}) - remote_sig = chan.config[LOCAL].current_commitment_signature - remote_sig = der_sig_from_sig_string(remote_sig) + b"\x01" - none_idx = tx._inputs[0]["signatures"].index(None) - tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig)) - assert tx.is_complete() - # TODO persist FORCE_CLOSING state to disk - chan.set_state('FORCE_CLOSING') - self.lnworker.save_channel(chan) - return await self.network.broadcast_transaction(tx) - @log_exceptions async def on_shutdown(self, payload): # length of scripts allowed in BOLT-02 diff --git a/electrum/lnchan.py b/electrum/lnchan.py @@ -187,7 +187,11 @@ class Channel(PrintError): self.remote_commitment = self.pending_remote_commitment self._is_funding_txo_spent = None # "don't know" - self.set_state('DISCONNECTED') + self._state = None + if state.get('force_closed', False): + self.set_state('FORCE_CLOSING') + else: + self.set_state('DISCONNECTED') self.lnwatcher = None @@ -197,6 +201,8 @@ class Channel(PrintError): self.log[sub].locked_in.update(self.log[sub].adds.keys()) def set_state(self, state: str): + if self._state == 'FORCE_CLOSING': + assert state == 'FORCE_CLOSING', 'new state was not FORCE_CLOSING: ' + state self._state = state def get_state(self): @@ -713,6 +719,7 @@ class Channel(PrintError): "onion_keys": str_bytes_dict_to_save(self.onion_keys), "settled_local": self.settled[LOCAL], "settled_remote": self.settled[REMOTE], + "force_closed": self.get_state() == 'FORCE_CLOSING', } # htlcs number must be monotonically increasing, @@ -806,6 +813,7 @@ class Channel(PrintError): def make_closing_tx(self, local_script: bytes, remote_script: bytes, fee_sat: Optional[int]=None) -> Tuple[bytes, int, str]: + """ cooperative close """ if fee_sat is None: fee_sat = self.pending_local_fee @@ -830,6 +838,21 @@ class Channel(PrintError): sig = ecc.sig_string_from_der_sig(der_sig[:-1]) return sig, fee_sat, closing_tx.txid() + def force_close_tx(self): + # local_commitment always gives back the next expected local_commitment, + # but in this case, we want the current one. So substract one ctn number + old_local_state = self.config[LOCAL] + self.config[LOCAL]=self.config[LOCAL]._replace(ctn=self.config[LOCAL].ctn - 1) + tx = self.pending_local_commitment + self.config[LOCAL] = old_local_state + tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (self.config[LOCAL].multisig_key.privkey, True)}) + remote_sig = self.config[LOCAL].current_commitment_signature + remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01" + none_idx = tx._inputs[0]["signatures"].index(None) + tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig)) + assert tx.is_complete() + return tx + def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes, sweep_address) -> Optional[EncumberedTransaction]: assert isinstance(their_pcp, bytes) diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -36,6 +36,7 @@ from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, NUM_MAX_EDGES_IN_PAYMENT_PATH) from .i18n import _ from .lnrouter import RouteEdge, is_route_sane_to_use +from .address_synchronizer import TX_HEIGHT_LOCAL if TYPE_CHECKING: from .network import Network @@ -173,7 +174,8 @@ class LNWorker(PrintError): return chan.set_funding_txo_spentness(is_spent) if is_spent: - chan.set_state("CLOSED") + if chan.get_state() != 'FORCE_CLOSING': + chan.set_state("CLOSED") self.channel_db.remove_channel(chan.short_channel_id) self.network.trigger_callback('channel', chan) @@ -207,6 +209,13 @@ class LNWorker(PrintError): await peer.bitcoin_fee_update(chan) conf = addr_sync.get_tx_height(chan.funding_outpoint.txid).conf peer.on_network_update(chan, conf) + elif chan.get_state() == 'FORCE_CLOSING': + txid = chan.force_close_tx().txid() + height = addr_sync.get_tx_height(txid).height + self.print_error("force closing tx", txid, "height", height) + if height == TX_HEIGHT_LOCAL: + self.print_error('REBROADCASTING CLOSING TX') + await self.force_close_channel(chan.channel_id) async def _open_channel_coroutine(self, peer, local_amount_sat, push_sat, password): # peer might just have been connected to @@ -450,8 +459,10 @@ class LNWorker(PrintError): async def force_close_channel(self, chan_id): chan = self.channels[chan_id] - peer = self.peers[chan.node_id] - return await peer.force_close_channel(chan_id) + tx = chan.force_close_tx() + chan.set_state('FORCE_CLOSING') + self.save_channel(chan) + return await self.network.broadcast_transaction(tx) def _get_next_peers_to_try(self) -> Sequence[LNPeerAddr]: now = time.time()