Browse Source

channel close handling: detect situation based on output addresses

WIP...
master
SomberNight 7 years ago committed by ThomasV
parent
commit
930d21c31c
  1. 80
      electrum/lnsweep.py
  2. 8
      electrum/lnwatcher.py
  3. 16
      electrum/lnworker.py

80
electrum/lnsweep.py

@ -2,7 +2,8 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php # file LICENCE or http://www.opensource.org/licenses/mit-license.php
from typing import Optional, Dict, List, Tuple, TYPE_CHECKING from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple
from enum import Enum, auto
from .util import bfh, bh2u from .util import bfh, bh2u
from .bitcoin import TYPE_ADDRESS, redeem_script_to_address, dust_threshold from .bitcoin import TYPE_ADDRESS, redeem_script_to_address, dust_threshold
@ -59,7 +60,7 @@ def create_sweeptxs_for_their_just_revoked_ctx(chan: 'Channel', ctx: Transaction
sweep_address: str) -> Dict[str,Transaction]: sweep_address: str) -> Dict[str,Transaction]:
"""Presign sweeping transactions using the just received revoked pcs. """Presign sweeping transactions using the just received revoked pcs.
These will only be utilised if the remote breaches. These will only be utilised if the remote breaches.
Sweep 'lo_local', and all the HTLCs (two cases: directly from ctx, or from HTLC tx). Sweep 'to_local', and all the HTLCs (two cases: directly from ctx, or from HTLC tx).
""" """
# prep # prep
pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True) pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
@ -128,6 +129,81 @@ def create_sweeptxs_for_their_just_revoked_ctx(chan: 'Channel', ctx: Transaction
return txs return txs
class ChannelClosedBy(Enum):
US = auto()
THEM = auto()
UNKNOWN = auto()
class ChannelCloseSituationReport(NamedTuple):
closed_by: ChannelClosedBy
is_breach: Optional[bool]
def detect_how_channel_was_closed(chan: 'Channel', ctx: Transaction) -> ChannelCloseSituationReport:
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
def get_to_local_and_to_remote_addresses_for_our_ctx():
is_breach = ctn < our_conf.ctn
# to_local
our_per_commitment_secret = get_per_commitment_secret_from_seed(
our_conf.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
our_pcp = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True)
our_delayed_bp_privkey = ecc.ECPrivkey(our_conf.delayed_basepoint.privkey)
our_localdelayed_privkey = derive_privkey(our_delayed_bp_privkey.secret_scalar, our_pcp)
our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey)
their_revocation_pubkey = derive_blinded_pubkey(their_conf.revocation_basepoint.pubkey, our_pcp)
our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)
to_local_witness_script = bh2u(make_commitment_output_to_local_witness_script(
their_revocation_pubkey, their_conf.to_self_delay, our_localdelayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', to_local_witness_script)
# to_remote
their_payment_pubkey = derive_pubkey(their_conf.payment_basepoint.pubkey, our_pcp)
to_remote_address = make_commitment_output_to_remote_address(their_payment_pubkey)
return to_local_address, to_remote_address, is_breach
def get_to_local_and_to_remote_addresses_for_their_ctx():
is_breach = False
if ctn == their_conf.ctn:
their_pcp = their_conf.current_per_commitment_point
elif ctn == their_conf.ctn + 1:
their_pcp = their_conf.next_per_commitment_point
elif ctn < their_conf.ctn: # breach
is_breach = True
try:
per_commitment_secret = their_conf.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
except UnableToDeriveSecret:
return None, None, is_breach
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
else:
return None, None, None
# to_local
our_revocation_pubkey = derive_blinded_pubkey(our_conf.revocation_basepoint.pubkey, their_pcp)
their_delayed_pubkey = derive_pubkey(their_conf.delayed_basepoint.pubkey, their_pcp)
witness_script = bh2u(make_commitment_output_to_local_witness_script(
our_revocation_pubkey, our_conf.to_self_delay, their_delayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', witness_script)
# to_remote
our_payment_pubkey = derive_pubkey(our_conf.payment_basepoint.pubkey, their_pcp)
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
return to_local_address, to_remote_address, is_breach
# our ctx?
to_local_address, to_remote_address, is_breach = get_to_local_and_to_remote_addresses_for_our_ctx()
if (to_local_address and ctx.get_output_idx_from_address(to_local_address) is not None
or to_remote_address and ctx.get_output_idx_from_address(to_remote_address) is not None):
return ChannelCloseSituationReport(closed_by=ChannelClosedBy.US, is_breach=is_breach)
# their ctx?
to_local_address, to_remote_address, is_breach = get_to_local_and_to_remote_addresses_for_their_ctx()
if (to_local_address and ctx.get_output_idx_from_address(to_local_address) is not None
or to_remote_address and ctx.get_output_idx_from_address(to_remote_address) is not None):
return ChannelCloseSituationReport(closed_by=ChannelClosedBy.THEM, is_breach=is_breach)
return ChannelCloseSituationReport(closed_by=ChannelClosedBy.UNKNOWN, is_breach=None)
def create_sweeptxs_for_our_latest_ctx(chan: 'Channel', ctx: Transaction, def create_sweeptxs_for_our_latest_ctx(chan: 'Channel', ctx: Transaction,
sweep_address: str) -> Dict[str,Transaction]: sweep_address: str) -> Dict[str,Transaction]:
"""Handle the case where we force close unilaterally with our latest ctx. """Handle the case where we force close unilaterally with our latest ctx.

8
electrum/lnwatcher.py

@ -214,7 +214,13 @@ class LNWatcher(AddressSynchronizer):
self.network.trigger_callback('channel_open', funding_outpoint, funding_txid, funding_height) self.network.trigger_callback('channel_open', funding_outpoint, funding_txid, funding_height)
else: else:
closing_height = self.get_tx_height(closing_txid) closing_height = self.get_tx_height(closing_txid)
self.network.trigger_callback('channel_closed', funding_outpoint, spenders, funding_txid, funding_height, closing_txid, closing_height) closing_tx = self.db.get_transaction(closing_txid)
if not closing_tx:
self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...")
return
self.network.trigger_callback('channel_closed', funding_outpoint, spenders,
funding_txid, funding_height, closing_txid,
closing_height, closing_tx) # FIXME sooo many args..
await self.do_breach_remedy(funding_outpoint, spenders) await self.do_breach_remedy(funding_outpoint, spenders)
if not keep_watching: if not keep_watching:
self.unwatch_channel(address, funding_outpoint) self.unwatch_channel(address, funding_outpoint)

16
electrum/lnworker.py

@ -44,6 +44,8 @@ from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
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 from .address_synchronizer import TX_HEIGHT_LOCAL
from . import lnsweep
from .lnsweep import ChannelClosedBy
if TYPE_CHECKING: if TYPE_CHECKING:
from .network import Network from .network import Network
@ -495,7 +497,7 @@ class LNWallet(LNWorker):
self.network.trigger_callback('channel', chan) self.network.trigger_callback('channel', chan)
@log_exceptions @log_exceptions
async def on_channel_closed(self, event, funding_outpoint, spenders, funding_txid, funding_height, closing_txid, closing_height): async def on_channel_closed(self, event, funding_outpoint, spenders, funding_txid, funding_height, closing_txid, closing_height, closing_tx):
chan = self.channel_by_txo(funding_outpoint) chan = self.channel_by_txo(funding_outpoint)
if not chan: if not chan:
return return
@ -510,14 +512,16 @@ class LNWallet(LNWorker):
if chan.short_channel_id is not None: if chan.short_channel_id is not None:
self.channel_db.remove_channel(chan.short_channel_id) self.channel_db.remove_channel(chan.short_channel_id)
# detect who closed # detect who closed
if closing_txid == chan.local_commitment.txid(): assert closing_tx, f"no closing tx... {repr(closing_tx)}"
self.logger.info(f'we force closed {funding_outpoint}') sitrep = lnsweep.detect_how_channel_was_closed(chan, closing_tx)
if sitrep.closed_by == ChannelClosedBy.US:
self.logger.info(f'we force closed {funding_outpoint}. sitrep: {repr(sitrep)}')
encumbered_sweeptxs = chan.local_sweeptxs encumbered_sweeptxs = chan.local_sweeptxs
elif closing_txid == chan.remote_commitment.txid(): elif sitrep.closed_by == ChannelClosedBy.THEM and sitrep.is_breach is False:
self.logger.info(f'they force closed {funding_outpoint}') self.logger.info(f'they force closed {funding_outpoint}. sitrep: {repr(sitrep)}')
encumbered_sweeptxs = chan.remote_sweeptxs encumbered_sweeptxs = chan.remote_sweeptxs
else: else:
self.logger.info(f'not sure who closed {funding_outpoint} {closing_txid}') self.logger.info(f'not sure who closed {funding_outpoint} {closing_txid}. sitrep: {repr(sitrep)}')
return return
# sweep # sweep
for prevout, spender in spenders.items(): for prevout, spender in spenders.items():

Loading…
Cancel
Save