URI: 
       tchannel close handling: detect situation based on output addresses - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 930d21c31c5240fe57ed5f53335637e4f3ac5335
   DIR parent acbb458ef7012b04a1df671fcd9c350a6cb76c75
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Thu, 23 May 2019 16:13:28 +0200
       
       channel close handling: detect situation based on output addresses
       
       WIP...
       
       Diffstat:
         M electrum/lnsweep.py                 |      80 ++++++++++++++++++++++++++++++-
         M electrum/lnwatcher.py               |       8 +++++++-
         M electrum/lnworker.py                |      16 ++++++++++------
       
       3 files changed, 95 insertions(+), 9 deletions(-)
       ---
   DIR diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py
       t@@ -2,7 +2,8 @@
        # Distributed under the MIT software license, see the accompanying
        # 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 .bitcoin import TYPE_ADDRESS, redeem_script_to_address, dust_threshold
       t@@ -59,7 +60,7 @@ def create_sweeptxs_for_their_just_revoked_ctx(chan: 'Channel', ctx: Transaction
                                                       sweep_address: str) -> Dict[str,Transaction]:
            """Presign sweeping transactions using the just received revoked pcs.
            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
            pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
       t@@ -128,6 +129,81 @@ def create_sweeptxs_for_their_just_revoked_ctx(chan: 'Channel', ctx: Transaction
            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,
                                               sweep_address: str) -> Dict[str,Transaction]:
            """Handle the case where we force close unilaterally with our latest ctx.
   DIR diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
       t@@ -214,7 +214,13 @@ class LNWatcher(AddressSynchronizer):
                    self.network.trigger_callback('channel_open', funding_outpoint, funding_txid, funding_height)
                else:
                    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)
                if not keep_watching:
                    self.unwatch_channel(address, funding_outpoint)
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -44,6 +44,8 @@ from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr,
        from .i18n import _
        from .lnrouter import RouteEdge, is_route_sane_to_use
        from .address_synchronizer import TX_HEIGHT_LOCAL
       +from . import lnsweep
       +from .lnsweep import ChannelClosedBy
        
        if TYPE_CHECKING:
            from .network import Network
       t@@ -495,7 +497,7 @@ class LNWallet(LNWorker):
                self.network.trigger_callback('channel', chan)
        
            @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)
                if not chan:
                    return
       t@@ -510,14 +512,16 @@ class LNWallet(LNWorker):
                if chan.short_channel_id is not None:
                    self.channel_db.remove_channel(chan.short_channel_id)
                # detect who closed
       -        if closing_txid == chan.local_commitment.txid():
       -            self.logger.info(f'we force closed {funding_outpoint}')
       +        assert closing_tx, f"no closing tx... {repr(closing_tx)}"
       +        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
       -        elif closing_txid == chan.remote_commitment.txid():
       -            self.logger.info(f'they force closed {funding_outpoint}')
       +        elif sitrep.closed_by == ChannelClosedBy.THEM and sitrep.is_breach is False:
       +            self.logger.info(f'they force closed {funding_outpoint}. sitrep: {repr(sitrep)}')
                    encumbered_sweeptxs = chan.remote_sweeptxs
                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
                # sweep
                for prevout, spender in spenders.items():