Browse Source

avoid leaving FORCE_CLOSING state, rebroadcast closing tx if reorged out

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 6 years ago
committed by ThomasV
parent
commit
c570bc5fb1
  1. 4
      electrum/gui/qt/channels_list.py
  2. 22
      electrum/lnbase.py
  3. 23
      electrum/lnchan.py
  4. 15
      electrum/lnworker.py

4
electrum/gui/qt/channels_list.py

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import traceback
import asyncio import asyncio
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import * from PyQt5.QtWidgets import *
@ -48,7 +49,8 @@ class ChannelsList(MyTreeWidget):
def on_success(txid): def on_success(txid):
self.main_window.show_error('Channel closed' + '\n' + txid) self.main_window.show_error('Channel closed' + '\n' + txid)
def on_failure(exc_info): 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))) self.main_window.show_error('Failed to close channel:\n{}'.format(repr(e)))
def close(): def close():
def task(): def task():

22
electrum/lnbase.py

@ -377,6 +377,7 @@ class Peer(PrintError):
except: except:
pass pass
for chan in self.channels.values(): for chan in self.channels.values():
if chan.get_state() != 'FORCE_CLOSING':
chan.set_state('DISCONNECTED') chan.set_state('DISCONNECTED')
self.network.trigger_callback('channel', chan) self.network.trigger_callback('channel', chan)
@ -442,7 +443,7 @@ class Peer(PrintError):
) )
payload = await self.channel_accepted[temp_channel_id].get() payload = await self.channel_accepted[temp_channel_id].get()
if payload.get('error'): 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'] remote_per_commitment_point = payload['first_per_commitment_point']
funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big') funding_txn_minimum_depth = int.from_bytes(payload['minimum_depth'], 'big')
remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='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) self.print_error('Channel closed', txid)
return 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 @log_exceptions
async def on_shutdown(self, payload): async def on_shutdown(self, payload):
# length of scripts allowed in BOLT-02 # length of scripts allowed in BOLT-02

23
electrum/lnchan.py

@ -187,6 +187,10 @@ class Channel(PrintError):
self.remote_commitment = self.pending_remote_commitment self.remote_commitment = self.pending_remote_commitment
self._is_funding_txo_spent = None # "don't know" self._is_funding_txo_spent = None # "don't know"
self._state = None
if state.get('force_closed', False):
self.set_state('FORCE_CLOSING')
else:
self.set_state('DISCONNECTED') self.set_state('DISCONNECTED')
self.lnwatcher = None self.lnwatcher = None
@ -197,6 +201,8 @@ class Channel(PrintError):
self.log[sub].locked_in.update(self.log[sub].adds.keys()) self.log[sub].locked_in.update(self.log[sub].adds.keys())
def set_state(self, state: str): 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 self._state = state
def get_state(self): def get_state(self):
@ -713,6 +719,7 @@ class Channel(PrintError):
"onion_keys": str_bytes_dict_to_save(self.onion_keys), "onion_keys": str_bytes_dict_to_save(self.onion_keys),
"settled_local": self.settled[LOCAL], "settled_local": self.settled[LOCAL],
"settled_remote": self.settled[REMOTE], "settled_remote": self.settled[REMOTE],
"force_closed": self.get_state() == 'FORCE_CLOSING',
} }
# htlcs number must be monotonically increasing, # htlcs number must be monotonically increasing,
@ -806,6 +813,7 @@ class Channel(PrintError):
def make_closing_tx(self, local_script: bytes, remote_script: bytes, def make_closing_tx(self, local_script: bytes, remote_script: bytes,
fee_sat: Optional[int]=None) -> Tuple[bytes, int, str]: fee_sat: Optional[int]=None) -> Tuple[bytes, int, str]:
""" cooperative close """
if fee_sat is None: if fee_sat is None:
fee_sat = self.pending_local_fee fee_sat = self.pending_local_fee
@ -830,6 +838,21 @@ class Channel(PrintError):
sig = ecc.sig_string_from_der_sig(der_sig[:-1]) sig = ecc.sig_string_from_der_sig(der_sig[:-1])
return sig, fee_sat, closing_tx.txid() 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, def maybe_create_sweeptx_for_their_ctx_to_remote(chan, ctx, their_pcp: bytes,
sweep_address) -> Optional[EncumberedTransaction]: sweep_address) -> Optional[EncumberedTransaction]:
assert isinstance(their_pcp, bytes) assert isinstance(their_pcp, bytes)

15
electrum/lnworker.py

@ -36,6 +36,7 @@ from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
NUM_MAX_EDGES_IN_PAYMENT_PATH) NUM_MAX_EDGES_IN_PAYMENT_PATH)
from .i18n import _ from .i18n import _
from .lnrouter import RouteEdge, is_route_sane_to_use from .lnrouter import RouteEdge, is_route_sane_to_use
from .address_synchronizer import TX_HEIGHT_LOCAL
if TYPE_CHECKING: if TYPE_CHECKING:
from .network import Network from .network import Network
@ -173,6 +174,7 @@ class LNWorker(PrintError):
return return
chan.set_funding_txo_spentness(is_spent) chan.set_funding_txo_spentness(is_spent)
if is_spent: if is_spent:
if chan.get_state() != 'FORCE_CLOSING':
chan.set_state("CLOSED") chan.set_state("CLOSED")
self.channel_db.remove_channel(chan.short_channel_id) self.channel_db.remove_channel(chan.short_channel_id)
self.network.trigger_callback('channel', chan) self.network.trigger_callback('channel', chan)
@ -207,6 +209,13 @@ class LNWorker(PrintError):
await peer.bitcoin_fee_update(chan) await peer.bitcoin_fee_update(chan)
conf = addr_sync.get_tx_height(chan.funding_outpoint.txid).conf conf = addr_sync.get_tx_height(chan.funding_outpoint.txid).conf
peer.on_network_update(chan, 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): async def _open_channel_coroutine(self, peer, local_amount_sat, push_sat, password):
# peer might just have been connected to # peer might just have been connected to
@ -450,8 +459,10 @@ class LNWorker(PrintError):
async def force_close_channel(self, chan_id): async def force_close_channel(self, chan_id):
chan = self.channels[chan_id] chan = self.channels[chan_id]
peer = self.peers[chan.node_id] tx = chan.force_close_tx()
return await peer.force_close_channel(chan_id) 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]: def _get_next_peers_to_try(self) -> Sequence[LNPeerAddr]:
now = time.time() now = time.time()

Loading…
Cancel
Save