URI: 
       tlnpeer: implement upfront shutdown script logic - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 6b90a2d36c52c577a786e8080e3c09531795b16d
   DIR parent e5458bb5348be6d73ff834f7db3cd4a993c7ed6a
  HTML Author: bitromortac <bitromortac@protonmail.com>
       Date:   Fri, 18 Dec 2020 11:06:42 +0100
       
       lnpeer: implement upfront shutdown script logic
       
       Upfront shutdown script is a script provided on channel opening,
       which will be used by the peer to enforce us closing to this script
       on collaborative channel close.
       
       Diffstat:
         M electrum/lnchannel.py               |       3 +++
         M electrum/lnpeer.py                  |     110 +++++++++++++++++++++++++++++--
         M electrum/lnutil.py                  |       2 ++
         M electrum/tests/test_lnchannel.py    |       2 ++
       
       4 files changed, 110 insertions(+), 7 deletions(-)
       ---
   DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
       t@@ -649,6 +649,9 @@ class Channel(AbstractChannel):
            def is_static_remotekey_enabled(self) -> bool:
                return bool(self.storage.get('static_remotekey_enabled'))
        
       +    def is_upfront_shutdown_script_enabled(self) -> bool:
       +        return bool(self.storage.get('upfront_shutdown_script_enabled'))
       +
            def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:
                ret = []
                if self.is_static_remotekey_enabled():
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -44,7 +44,8 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc,
                             RemoteMisbehaving,
                             NBLOCK_OUR_CLTV_EXPIRY_DELTA, format_short_channel_id, ShortChannelID,
                             IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage,
       -                     LN_MAX_FUNDING_SAT, calc_fees_for_commitment_tx)
       +                     LN_MAX_FUNDING_SAT, calc_fees_for_commitment_tx,
       +                     UpfrontShutdownScriptViolation)
        from .lnutil import FeeUpdate, channel_id_from_funding_tx
        from .lntransport import LNTransport, LNTransportBase
        from .lnmsg import encode_msg, decode_msg
       t@@ -486,12 +487,33 @@ class Peer(Logger):
            def is_static_remotekey(self):
                return bool(self.features & LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
        
       +    def is_upfront_shutdown_script(self):
       +        return bool(self.features & LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT)
       +
       +    def upfront_shutdown_script_from_payload(self, payload, msg_identifier: str) -> Optional[bytes]:
       +        if msg_identifier not in ['accept', 'open']:
       +            raise ValueError("msg_identifier must be either 'accept' or 'open'")
       +
       +        uss_tlv = payload[msg_identifier + '_channel_tlvs'].get(
       +            'upfront_shutdown_script')
       +
       +        if uss_tlv and self.is_upfront_shutdown_script():
       +            upfront_shutdown_script = uss_tlv['shutdown_scriptpubkey']
       +        else:
       +            upfront_shutdown_script = b''
       +        self.logger.info(f"upfront shutdown script received: {upfront_shutdown_script}")
       +        return upfront_shutdown_script
       +
            def make_local_config(self, funding_sat: int, push_msat: int, initiator: HTLCOwner) -> LocalConfig:
                channel_seed = os.urandom(32)
                initial_msat = funding_sat * 1000 - push_msat if initiator == LOCAL else push_msat
       +
       +        static_remotekey = None
       +        # sending empty bytes as the upfront_shutdown_script will give us the
       +        # flexibility to decide an address at closing time
       +        upfront_shutdown_script = b''
       +
                if self.is_static_remotekey():
       -            # Note: in the future, if a CSV delay is added,
       -            # we will want to derive that key
                    wallet = self.lnworker.wallet
                    assert wallet.txin_type == 'p2wpkh'
                    addr = wallet.get_new_sweep_address_for_channel()
       t@@ -503,6 +525,7 @@ class Peer(Logger):
                local_config = LocalConfig.from_seed(
                    channel_seed=channel_seed,
                    static_remotekey=static_remotekey,
       +            upfront_shutdown_script=upfront_shutdown_script,
                    to_self_delay=self.network.config.get('lightning_to_self_delay', 7 * 144),
                    dust_limit_sat=dust_limit_sat,
                    max_htlc_value_in_flight_msat=funding_sat * 1000,
       t@@ -546,15 +569,27 @@ class Peer(Logger):
                    push_msat: int,
                    temp_channel_id: bytes
            ) -> Tuple[Channel, 'PartialTransaction']:
       +        """Implements the channel opening flow.
       +
       +        -> open_channel message
       +        <- accept_channel message
       +        -> funding_created message
       +        <- funding_signed message
       +
       +        Channel configurations are initialized in this method.
       +        """
                await asyncio.wait_for(self.initialized, LN_P2P_NETWORK_TIMEOUT)
       +
                feerate = self.lnworker.current_feerate_per_kw()
                local_config = self.make_local_config(funding_sat, push_msat, LOCAL)
       +
                if funding_sat > LN_MAX_FUNDING_SAT:
                    raise Exception(f"MUST set funding_satoshis to less than 2^24 satoshi. {funding_sat} sat > {LN_MAX_FUNDING_SAT}")
                if push_msat > 1000 * funding_sat:
                    raise Exception(f"MUST set push_msat to equal or less than 1000 * funding_satoshis: {push_msat} msat > {1000 * funding_sat} msat")
                if funding_sat < lnutil.MIN_FUNDING_SAT:
                    raise Exception(f"funding_sat too low: {funding_sat} < {lnutil.MIN_FUNDING_SAT}")
       +
                # for the first commitment transaction
                per_commitment_secret_first = get_per_commitment_secret_from_seed(local_config.per_commitment_secret_seed,
                                                                                  RevocationStore.START_INDEX)
       t@@ -579,7 +614,13 @@ class Peer(Logger):
                    channel_flags=0x00,  # not willing to announce channel
                    channel_reserve_satoshis=local_config.reserve_sat,
                    htlc_minimum_msat=local_config.htlc_minimum_msat,
       +            open_channel_tlvs={
       +                'upfront_shutdown_script':
       +                    {'shutdown_scriptpubkey': local_config.upfront_shutdown_script}
       +            }
                )
       +
       +        # <- accept_channel
                payload = await self.wait_for_message('accept_channel', temp_channel_id)
                remote_per_commitment_point = payload['first_per_commitment_point']
                funding_txn_minimum_depth = payload['minimum_depth']
       t@@ -587,6 +628,10 @@ class Peer(Logger):
                    raise Exception(f"minimum depth too low, {funding_txn_minimum_depth}")
                if funding_txn_minimum_depth > 30:
                    raise Exception(f"minimum depth too high, {funding_txn_minimum_depth}")
       +
       +        upfront_shutdown_script = self.upfront_shutdown_script_from_payload(
       +            payload, 'accept')
       +
                remote_config = RemoteConfig(
                    payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
                    multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]),
       t@@ -602,6 +647,7 @@ class Peer(Logger):
                    htlc_minimum_msat=payload['htlc_minimum_msat'],
                    next_per_commitment_point=remote_per_commitment_point,
                    current_per_commitment_point=None,
       +            upfront_shutdown_script=upfront_shutdown_script
                )
                remote_config.validate_params(funding_sat=funding_sat)
                # if channel_reserve_satoshis is less than dust_limit_satoshis within the open_channel message:
       t@@ -612,6 +658,8 @@ class Peer(Logger):
                #     MUST reject the channel.
                if local_config.reserve_sat < remote_config.dust_limit_sat:
                    raise Exception("violated constraint: local_config.reserve_sat < remote_config.dust_limit_sat")
       +
       +        # -> funding created
                # replace dummy output in funding tx
                redeem_script = funding_output_script(local_config, remote_config)
                funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
       t@@ -626,7 +674,7 @@ class Peer(Logger):
                funding_txid = funding_tx.txid()
                assert funding_txid
                funding_index = funding_tx.outputs().index(funding_output)
       -        # remote commitment transaction
       +        # build remote commitment transaction
                channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_index)
                outpoint = Outpoint(funding_txid, funding_index)
                constraints = ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth)
       t@@ -640,12 +688,15 @@ class Peer(Logger):
                    chan.add_or_update_peer_addr(self.transport.peer_addr)
                sig_64, _ = chan.sign_next_commitment()
                self.temp_id_to_id[temp_channel_id] = channel_id
       +
                self.send_message("funding_created",
                    temporary_channel_id=temp_channel_id,
                    funding_txid=funding_txid_bytes,
                    funding_output_index=funding_index,
                    signature=sig_64)
                self.funding_created_sent.add(channel_id)
       +
       +        # <- funding signed
                payload = await self.wait_for_message('funding_signed', channel_id)
                self.logger.info('received funding_signed')
                remote_sig = payload['signature']
       t@@ -675,6 +726,16 @@ class Peer(Logger):
                return StoredDict(chan_dict, None, [])
        
            async def on_open_channel(self, payload):
       +        """Implements the channel acceptance flow.
       +
       +        <- open_channel message
       +        -> accept_channel message
       +        <- funding_created message
       +        -> funding_signed message
       +
       +        Channel configurations are initialized in this method.
       +        """
       +        # <- open_channel
                if payload['chain_hash'] != constants.net.rev_genesis_bytes():
                    raise Exception('wrong chain_hash')
                funding_sat = payload['funding_satoshis']
       t@@ -688,6 +749,10 @@ class Peer(Logger):
                    raise Exception(f"MUST set push_msat to equal or less than 1000 * funding_satoshis: {push_msat} msat > {1000 * funding_sat} msat")
                if funding_sat < lnutil.MIN_FUNDING_SAT:
                    raise Exception(f"funding_sat too low: {funding_sat} < {lnutil.MIN_FUNDING_SAT}")
       +
       +        upfront_shutdown_script = self.upfront_shutdown_script_from_payload(
       +            payload, 'open')
       +
                remote_config = RemoteConfig(
                    payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
                    multisig_key=OnlyPubkeyKeypair(payload['funding_pubkey']),
       t@@ -703,7 +768,9 @@ class Peer(Logger):
                    htlc_minimum_msat=payload['htlc_minimum_msat'],
                    next_per_commitment_point=payload['first_per_commitment_point'],
                    current_per_commitment_point=None,
       +            upfront_shutdown_script=upfront_shutdown_script,
                )
       +
                remote_config.validate_params(funding_sat=funding_sat)
                # The receiving node MUST fail the channel if:
                #     the funder's amount for the initial commitment transaction is not sufficient for full fee payment.
       t@@ -720,12 +787,15 @@ class Peer(Logger):
                # note: we ignore payload['channel_flags'],  which e.g. contains 'announce_channel'.
                #       Notably if the remote sets 'announce_channel' to True, we will ignore that too,
                #       but we will not play along with actually announcing the channel (so we keep it private).
       +
       +        # -> accept channel
                # for the first commitment transaction
                per_commitment_secret_first = get_per_commitment_secret_from_seed(local_config.per_commitment_secret_seed,
                                                                                  RevocationStore.START_INDEX)
                per_commitment_point_first = secret_to_pubkey(int.from_bytes(per_commitment_secret_first, 'big'))
                min_depth = 3
       -        self.send_message('accept_channel',
       +        self.send_message(
       +            'accept_channel',
                    temporary_channel_id=temp_chan_id,
                    dust_limit_satoshis=local_config.dust_limit_sat,
                    max_htlc_value_in_flight_msat=local_config.max_htlc_value_in_flight_msat,
       t@@ -740,8 +810,16 @@ class Peer(Logger):
                    delayed_payment_basepoint=local_config.delayed_basepoint.pubkey,
                    htlc_basepoint=local_config.htlc_basepoint.pubkey,
                    first_per_commitment_point=per_commitment_point_first,
       +            accept_channel_tlvs={
       +                'upfront_shutdown_script':
       +                    {'shutdown_scriptpubkey': local_config.upfront_shutdown_script}
       +            }
                )
       +
       +        # <- funding created
                funding_created = await self.wait_for_message('funding_created', temp_chan_id)
       +
       +        # -> funding signed
                funding_idx = funding_created['funding_output_index']
                funding_txid = bh2u(funding_created['funding_txid'][::-1])
                channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx)
       t@@ -1407,6 +1485,13 @@ class Peer(Logger):
        
            async def on_shutdown(self, chan: Channel, payload):
                their_scriptpubkey = payload['scriptpubkey']
       +        their_upfront_scriptpubkey = chan.config[REMOTE].upfront_shutdown_script
       +
       +        # BOLT-02 check if they use the upfront shutdown script they advertized
       +        if their_upfront_scriptpubkey:
       +            if not (their_scriptpubkey == their_upfront_scriptpubkey):
       +                raise UpfrontShutdownScriptViolation("remote didn't use upfront shutdown script it commited to in channel opening")
       +
                # BOLT-02 restrict the scriptpubkey to some templates:
                if not (match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_WITNESS_V0)
                        or match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_P2SH)
       t@@ -1433,7 +1518,13 @@ class Peer(Logger):
            async def send_shutdown(self, chan: Channel):
                if not self.can_send_shutdown(chan):
                    raise Exception('cannot send shutdown')
       -        scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address))
       +
       +        if chan.config[LOCAL].upfront_shutdown_script:
       +            scriptpubkey = chan.config[LOCAL].upfront_shutdown_script
       +        else:
       +            scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address))
       +        assert scriptpubkey
       +
                # wait until no more pending updates (bolt2)
                chan.set_can_send_ctx_updates(False)
                while chan.has_pending_changes(REMOTE):
       t@@ -1452,7 +1543,12 @@ class Peer(Logger):
                # if no HTLCs remain, we must not send updates
                chan.set_can_send_ctx_updates(False)
                their_scriptpubkey = payload['scriptpubkey']
       -        our_scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address))
       +        if chan.config[LOCAL].upfront_shutdown_script:
       +            our_scriptpubkey = chan.config[LOCAL].upfront_shutdown_script
       +        else:
       +            our_scriptpubkey = bfh(bitcoin.address_to_script(chan.sweep_address))
       +        assert our_scriptpubkey
       +
                # estimate fee of closing tx
                our_sig, closing_tx = chan.make_closing_tx(our_scriptpubkey, their_scriptpubkey, fee_sat=0)
                fee_rate = self.network.config.fee_per_kb()
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -81,6 +81,7 @@ class Config(StoredObject):
            initial_msat = attr.ib(type=int)
            reserve_sat = attr.ib(type=int)  # applies to OTHER ctx
            htlc_minimum_msat = attr.ib(type=int)  # smallest value for INCOMING htlc
       +    upfront_shutdown_script = attr.ib(type=bytes, converter=hex_to_bytes)
        
            def validate_params(self, *, funding_sat: int) -> None:
                conf_name = type(self).__name__
       t@@ -300,6 +301,7 @@ class UnableToDeriveSecret(LightningError): pass
        class HandshakeFailed(LightningError): pass
        class ConnStringFormatError(LightningError): pass
        class RemoteMisbehaving(LightningError): pass
       +class UpfrontShutdownScriptViolation(RemoteMisbehaving): pass
        
        class NotFoundChanAnnouncementForUpdate(Exception): pass
        
   DIR diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py
       t@@ -69,6 +69,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
                        htlc_minimum_msat=1,
                        next_per_commitment_point=nex,
                        current_per_commitment_point=cur,
       +                upfront_shutdown_script=b'',
                    ),
                    "local_config":lnpeer.LocalConfig(
                        channel_seed = None,
       t@@ -89,6 +90,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
                        current_commitment_signature=None,
                        current_htlc_signatures=None,
                        htlc_minimum_msat=1,
       +                upfront_shutdown_script=b'',
                    ),
                    "constraints":lnpeer.ChannelConstraints(
                        capacity=funding_sat,