URI: 
       tlnpeer: cooperative close: verify scriptpubkey matches templates - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit aaf174ef3efb7744a48bcacd805618c1523766f3
   DIR parent c1212307063723e3fa0d13c6d05798e9a5d1a111
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Tue, 25 Feb 2020 17:54:49 +0100
       
       lnpeer: cooperative close: verify scriptpubkey matches templates
       
       Diffstat:
         M electrum/lnpeer.py                  |      12 ++++++++----
         M electrum/transaction.py             |      48 ++++++++++++++++++-------------
       
       2 files changed, 36 insertions(+), 24 deletions(-)
       ---
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -25,7 +25,8 @@ from . import ecc
        from .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string, der_sig_from_sig_string
        from . import constants
        from .util import bh2u, bfh, log_exceptions, ignore_exceptions, chunks, SilentTaskGroup
       -from .transaction import Transaction, TxOutput, PartialTxOutput
       +from . import transaction
       +from .transaction import Transaction, TxOutput, PartialTxOutput, match_script_against_template
        from .logging import Logger
        from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
                              process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage,
       t@@ -1357,9 +1358,12 @@ class Peer(Logger):
        
            @log_exceptions
            async def on_shutdown(self, payload):
       -        # length of scripts allowed in BOLT-02
       -        if int.from_bytes(payload['len'], 'big') not in (3+20+2, 2+20+1, 2+20, 2+32):
       -            raise Exception('scriptpubkey length in received shutdown message invalid: ' + str(payload['len']))
       +        their_scriptpubkey = payload['scriptpubkey']
       +        # 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)
       +                or match_script_against_template(their_scriptpubkey, transaction.SCRIPTPUBKEY_TEMPLATE_P2PKH)):
       +            raise Exception(f'scriptpubkey in received shutdown message does not conform to any template: {their_scriptpubkey.hex()}')
                chan_id = payload['channel_id']
                if chan_id in self.shutdown_received:
                    self.shutdown_received[chan_id].set_result(payload)
   DIR diff --git a/electrum/transaction.py b/electrum/transaction.py
       t@@ -390,20 +390,32 @@ class OPPushDataGeneric:
        
        
        OPPushDataPubkey = OPPushDataGeneric(lambda x: x in (33, 65))
       -# note that this does not include x_pubkeys !
        
       +SCRIPTPUBKEY_TEMPLATE_P2PKH = [opcodes.OP_DUP, opcodes.OP_HASH160,
       +                               OPPushDataGeneric(lambda x: x == 20),
       +                               opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG]
       +SCRIPTPUBKEY_TEMPLATE_P2SH = [opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUAL]
       +SCRIPTPUBKEY_TEMPLATE_WITNESS_V0 = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))]
        
       -def match_decoded(decoded, to_match):
       -    if decoded is None:
       +
       +def match_script_against_template(script, template) -> bool:
       +    """Returns whether 'script' matches 'template'."""
       +    if script is None:
                return False
       -    if len(decoded) != len(to_match):
       +    # optionally decode script now:
       +    if isinstance(script, (bytes, bytearray)):
       +        try:
       +            script = [x for x in script_GetOp(script)]
       +        except MalformedBitcoinScript:
       +            return False
       +    if len(script) != len(template):
                return False
       -    for i in range(len(decoded)):
       -        to_match_item = to_match[i]
       -        decoded_item = decoded[i]
       -        if OPPushDataGeneric.is_instance(to_match_item) and to_match_item.check_data_len(decoded_item[0]):
       +    for i in range(len(script)):
       +        template_item = template[i]
       +        script_item = script[i]
       +        if OPPushDataGeneric.is_instance(template_item) and template_item.check_data_len(script_item[0]):
                    continue
       -        if to_match_item != decoded_item[0]:
       +        if template_item != script_item[0]:
                    return False
            return True
        
       t@@ -412,28 +424,25 @@ def get_address_from_output_script(_bytes: bytes, *, net=None) -> Optional[str]:
            try:
                decoded = [x for x in script_GetOp(_bytes)]
            except MalformedBitcoinScript:
       -        decoded = None
       +        return None
        
            # p2pkh
       -    match = [opcodes.OP_DUP, opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG]
       -    if match_decoded(decoded, match):
       +    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2PKH):
                return hash160_to_p2pkh(decoded[2][1], net=net)
        
            # p2sh
       -    match = [opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUAL]
       -    if match_decoded(decoded, match):
       +    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_P2SH):
                return hash160_to_p2sh(decoded[1][1], net=net)
        
            # segwit address (version 0)
       -    match = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))]
       -    if match_decoded(decoded, match):
       +    if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_WITNESS_V0):
                return hash_to_segwit_addr(decoded[1][1], witver=0, net=net)
        
            # segwit address (version 1-16)
            future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1))
            for witver, opcode in enumerate(future_witness_versions, start=1):
                match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)]
       -        if match_decoded(decoded, match):
       +        if match_script_against_template(decoded, match):
                    return hash_to_segwit_addr(decoded[1][1], witver=witver, net=net)
        
            return None
       t@@ -1348,14 +1357,13 @@ class PartialTxInput(TxInput, PSBTSection):
                        except MalformedBitcoinScript:
                            decoded = None
                        # witness version 0
       -                match = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))]
       -                if match_decoded(decoded, match):
       +                if match_script_against_template(decoded, SCRIPTPUBKEY_TEMPLATE_WITNESS_V0):
                            return True
                        # witness version 1-16
                        future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1))
                        for witver, opcode in enumerate(future_witness_versions, start=1):
                            match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)]
       -                    if match_decoded(decoded, match):
       +                    if match_script_against_template(decoded, match):
                                return True
                        return False