Browse Source

detect redeemed channels (fix #5963)

hard-fail-on-bad-server-string
ThomasV 5 years ago
parent
commit
938fab86d1
  1. 4
      electrum/gui/kivy/uix/dialogs/lightning_channels.py
  2. 2
      electrum/gui/qt/channels_list.py
  3. 3
      electrum/lnchannel.py
  4. 44
      electrum/lnwatcher.py
  5. 3
      electrum/lnworker.py

4
electrum/gui/kivy/uix/dialogs/lightning_channels.py

@ -88,6 +88,7 @@ Builder.load_string(r'''
id: popuproot id: popuproot
data: [] data: []
is_closed: False is_closed: False
is_redeemed: False
BoxLayout: BoxLayout:
orientation: 'vertical' orientation: 'vertical'
ScrollView: ScrollView:
@ -115,7 +116,7 @@ Builder.load_string(r'''
height: '48dp' height: '48dp'
text: _('Delete') text: _('Delete')
on_release: root.remove_channel() on_release: root.remove_channel()
disabled: not root.is_closed disabled: not root.is_redeemed
Button: Button:
size_hint: 0.5, None size_hint: 0.5, None
height: '48dp' height: '48dp'
@ -129,6 +130,7 @@ class ChannelDetailsPopup(Popup):
def __init__(self, chan, app, **kwargs): def __init__(self, chan, app, **kwargs):
super(ChannelDetailsPopup,self).__init__(**kwargs) super(ChannelDetailsPopup,self).__init__(**kwargs)
self.is_closed = chan.is_closed() self.is_closed = chan.is_closed()
self.is_redeemed = chan.is_redeemed()
self.app = app self.app = app
self.chan = chan self.chan = chan
self.title = _('Channel details') self.title = _('Channel details')

2
electrum/gui/qt/channels_list.py

@ -112,7 +112,7 @@ class ChannelsList(MyTreeView):
if chan.peer_state == peer_states.GOOD: if chan.peer_state == peer_states.GOOD:
menu.addAction(_("Close channel"), lambda: self.close_channel(channel_id)) menu.addAction(_("Close channel"), lambda: self.close_channel(channel_id))
menu.addAction(_("Force-close channel"), lambda: self.force_close(channel_id)) menu.addAction(_("Force-close channel"), lambda: self.force_close(channel_id))
else: if chan.is_redeemed():
menu.addAction(_("Remove"), lambda: self.remove_channel(channel_id)) menu.addAction(_("Remove"), lambda: self.remove_channel(channel_id))
menu.exec_(self.viewport().mapToGlobal(position)) menu.exec_(self.viewport().mapToGlobal(position))

3
electrum/lnchannel.py

@ -228,6 +228,9 @@ class Channel(Logger):
# the closing txid has been saved # the closing txid has been saved
return self.get_state() >= channel_states.CLOSED return self.get_state() >= channel_states.CLOSED
def is_redeemed(self):
return self.get_state() == channel_states.REDEEMED
def _check_can_pay(self, amount_msat: int) -> None: def _check_can_pay(self, amount_msat: int) -> None:
# TODO check if this method uses correct ctns (should use "latest" + 1) # TODO check if this method uses correct ctns (should use "latest" + 1)
if self.is_closed(): if self.is_closed():

44
electrum/lnwatcher.py

@ -176,21 +176,24 @@ class LNWatcher(AddressSynchronizer):
await self.check_onchain_situation(address, outpoint) await self.check_onchain_situation(address, outpoint)
async def check_onchain_situation(self, address, funding_outpoint): async def check_onchain_situation(self, address, funding_outpoint):
keep_watching, spenders = self.inspect_tx_candidate(funding_outpoint, 0) spenders = self.inspect_tx_candidate(funding_outpoint, 0)
funding_txid = funding_outpoint.split(':')[0] funding_txid = funding_outpoint.split(':')[0]
funding_height = self.get_tx_height(funding_txid) funding_height = self.get_tx_height(funding_txid)
closing_txid = spenders.get(funding_outpoint) closing_txid = spenders.get(funding_outpoint)
closing_height = self.get_tx_height(closing_txid) closing_height = self.get_tx_height(closing_txid)
await self.update_channel_state(
funding_outpoint, funding_txid,
funding_height, closing_txid,
closing_height, keep_watching)
if closing_txid: if closing_txid:
closing_tx = self.db.get_transaction(closing_txid) closing_tx = self.db.get_transaction(closing_txid)
if closing_tx: if closing_tx:
await self.do_breach_remedy(funding_outpoint, closing_tx, spenders) keep_watching = await self.do_breach_remedy(funding_outpoint, closing_tx, spenders)
else: else:
self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...") self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...")
keep_watching = True
else:
keep_watching = True
await self.update_channel_state(
funding_outpoint, funding_txid,
funding_height, closing_txid,
closing_height, keep_watching)
if not keep_watching: if not keep_watching:
await self.unwatch_channel(address, funding_outpoint) await self.unwatch_channel(address, funding_outpoint)
@ -201,32 +204,24 @@ class LNWatcher(AddressSynchronizer):
raise NotImplementedError() # implemented by subclasses raise NotImplementedError() # implemented by subclasses
def inspect_tx_candidate(self, outpoint, n): def inspect_tx_candidate(self, outpoint, n):
# FIXME: instead of stopping recursion at n == 2,
# we should detect which outputs are HTLCs
prev_txid, index = outpoint.split(':') prev_txid, index = outpoint.split(':')
txid = self.db.get_spent_outpoint(prev_txid, int(index)) txid = self.db.get_spent_outpoint(prev_txid, int(index))
result = {outpoint:txid} result = {outpoint:txid}
if txid is None: if txid is None:
self.channel_status[outpoint] = 'open' self.channel_status[outpoint] = 'open'
#self.logger.info('keep watching because outpoint is unspent') return result
return True, result if n == 0 and not self.is_deeply_mined(txid):
keep_watching = (self.get_tx_mined_depth(txid) != TxMinedDepth.DEEP)
if keep_watching:
self.channel_status[outpoint] = 'closed (%d)' % self.get_tx_height(txid).conf self.channel_status[outpoint] = 'closed (%d)' % self.get_tx_height(txid).conf
#self.logger.info('keep watching because spending tx is not deep')
else: else:
self.channel_status[outpoint] = 'closed (deep)' self.channel_status[outpoint] = 'closed (deep)'
tx = self.db.get_transaction(txid) tx = self.db.get_transaction(txid)
for i, o in enumerate(tx.outputs()): for i, o in enumerate(tx.outputs()):
if o.address not in self.get_addresses(): if o.address not in self.get_addresses():
self.add_address(o.address) self.add_address(o.address)
keep_watching = True
elif n < 2: elif n < 2:
k, r = self.inspect_tx_candidate(txid+':%d'%i, n+1) r = self.inspect_tx_candidate(txid+':%d'%i, n+1)
keep_watching |= k
result.update(r) result.update(r)
return keep_watching, result return result
def get_tx_mined_depth(self, txid: str): def get_tx_mined_depth(self, txid: str):
if not txid: if not txid:
@ -247,6 +242,9 @@ class LNWatcher(AddressSynchronizer):
else: else:
raise NotImplementedError() raise NotImplementedError()
def is_deeply_mined(self, txid):
return self.get_tx_mined_depth(txid) == TxMinedDepth.DEEP
class WatchTower(LNWatcher): class WatchTower(LNWatcher):
@ -267,12 +265,16 @@ class WatchTower(LNWatcher):
self.add_channel(outpoint, address) self.add_channel(outpoint, address)
async def do_breach_remedy(self, funding_outpoint, closing_tx, spenders): async def do_breach_remedy(self, funding_outpoint, closing_tx, spenders):
keep_watching = False
for prevout, spender in spenders.items(): for prevout, spender in spenders.items():
if spender is not None: if spender is not None:
keep_watching |= not self.is_deeply_mined(spender)
continue continue
sweep_txns = await self.sweepstore.get_sweep_tx(funding_outpoint, prevout) sweep_txns = await self.sweepstore.get_sweep_tx(funding_outpoint, prevout)
for tx in sweep_txns: for tx in sweep_txns:
await self.broadcast_or_log(funding_outpoint, tx) await self.broadcast_or_log(funding_outpoint, tx)
keep_watching = True
return keep_watching
async def broadcast_or_log(self, funding_outpoint: str, tx: Transaction): async def broadcast_or_log(self, funding_outpoint: str, tx: Transaction):
height = self.get_tx_height(tx.txid()).height height = self.get_tx_height(tx.txid()).height
@ -345,6 +347,7 @@ class LNWalletWatcher(LNWatcher):
return return
# detect who closed and set sweep_info # detect who closed and set sweep_info
sweep_info_dict = chan.sweep_ctx(closing_tx) sweep_info_dict = chan.sweep_ctx(closing_tx)
keep_watching = False
self.logger.info(f'sweep_info_dict length: {len(sweep_info_dict)}') self.logger.info(f'sweep_info_dict length: {len(sweep_info_dict)}')
# create and broadcast transaction # create and broadcast transaction
for prevout, sweep_info in sweep_info_dict.items(): for prevout, sweep_info in sweep_info_dict.items():
@ -360,14 +363,19 @@ class LNWalletWatcher(LNWatcher):
spender2 = spenders.get(spender_txid+':0') spender2 = spenders.get(spender_txid+':0')
if spender2: if spender2:
self.logger.info(f'htlc is already spent {name}: {prevout}') self.logger.info(f'htlc is already spent {name}: {prevout}')
keep_watching |= not self.is_deeply_mined(spender2)
else: else:
self.logger.info(f'trying to redeem htlc {name}: {prevout}') self.logger.info(f'trying to redeem htlc {name}: {prevout}')
await self.try_redeem(spender_txid+':0', e_htlc_tx) await self.try_redeem(spender_txid+':0', e_htlc_tx)
keep_watching = True
else: else:
self.logger.info(f'outpoint already spent {name}: {prevout}') self.logger.info(f'outpoint already spent {name}: {prevout}')
keep_watching |= not self.is_deeply_mined(spender_txid)
else: else:
self.logger.info(f'trying to redeem {name}: {prevout}') self.logger.info(f'trying to redeem {name}: {prevout}')
await self.try_redeem(prevout, sweep_info) await self.try_redeem(prevout, sweep_info)
keep_watching = True
return keep_watching
@log_exceptions @log_exceptions
async def try_redeem(self, prevout: str, sweep_info: 'SweepInfo') -> None: async def try_redeem(self, prevout: str, sweep_info: 'SweepInfo') -> None:

3
electrum/lnworker.py

@ -1192,9 +1192,8 @@ class LNWallet(LNWorker):
return tx.txid() return tx.txid()
def remove_channel(self, chan_id): def remove_channel(self, chan_id):
# TODO: assert that closing tx is deep-mined and htlcs are swept
chan = self.channels[chan_id] chan = self.channels[chan_id]
assert chan.is_closed() assert chan.get_state() == channel_states.REDEEMED
with self.lock: with self.lock:
self.channels.pop(chan_id) self.channels.pop(chan_id)
self.channel_timestamps.pop(chan_id.hex()) self.channel_timestamps.pop(chan_id.hex())

Loading…
Cancel
Save