diff --git a/bitcoinlib/config/config.py b/bitcoinlib/config/config.py index 12f8e4bc..9dbd1fcd 100644 --- a/bitcoinlib/config/config.py +++ b/bitcoinlib/config/config.py @@ -75,11 +75,14 @@ 'sig_pubkey': ('unlocking', ['signature', 'key'], []), # 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'op_n', 'key', 'op_n', op.op_checkmultisig], []), 'p2sh_multisig': ('unlocking', [op.op_0, 'signature', 'redeemscript'], []), + 'multisig_redeemscript': ('unlocking', ['op_n', 'key', 'op_n', op.op_checkmultisig], []), 'p2tr_unlock': ('unlocking', ['data'], [64]), 'p2sh_multisig_2?': ('unlocking', [op.op_0, 'signature', op.op_verify, 'redeemscript'], []), 'p2sh_multisig_3?': ('unlocking', [op.op_0, 'signature', op.op_1add, 'redeemscript'], []), - 'p2sh_p2wpkh': ('unlocking', [op.op_0, op.op_hash160, 'redeemscript', op.op_equal], []), - 'p2sh_p2wsh': ('unlocking', [op.op_0, 'redeemscript'], []), + # 'p2sh_p2wpkh': ('unlocking', [op.op_0, op.op_hash160, 'redeemscript', op.op_equal], []), + # 'p2sh_p2wsh': ('unlocking', [op.op_0, 'redeemscript'], []), + 'p2sh_p2wpkh': ('unlocking', [op.op_0, 'data'], [20]), + 'p2sh_p2wsh': ('unlocking', [op.op_0, 'data'], [32]), 'signature': ('unlocking', ['signature'], []), 'signature_multisig': ('unlocking', [op.op_0, 'signature'], []), 'locktime_cltv': ('unlocking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop], []), diff --git a/bitcoinlib/scripts.py b/bitcoinlib/scripts.py index aea34b95..1ab9525d 100644 --- a/bitcoinlib/scripts.py +++ b/bitcoinlib/scripts.py @@ -41,7 +41,7 @@ def __str__(self): return self.msg -def _get_script_types(blueprint): +def _get_script_types(blueprint, is_locking=None): # Convert blueprint to more generic format bp = [] for item in blueprint: @@ -56,7 +56,14 @@ def _get_script_types(blueprint): else: bp.append(item) - script_types = [key for key, values in SCRIPT_TYPES.items() if values[1] == bp] + if is_locking is None: + locktype = ['locking', 'unlocking'] + elif is_locking: + locktype = ['locking'] + else: + locktype = ['unlocking'] + + script_types = [key for key, values in SCRIPT_TYPES.items() if values[1] == bp and values[0] in locktype] if len(script_types) == 1: return script_types @@ -65,7 +72,7 @@ def _get_script_types(blueprint): while len(bp): # Find all possible matches with blueprint matches = [(key, len(values[1]), values[2]) for key, values in SCRIPT_TYPES.items() if - values[1] == bp[:len(values[1])]] + values[1] == bp[:len(values[1])] and values[0] in locktype] if not matches: script_types.append('unknown') break @@ -80,9 +87,10 @@ def _get_script_types(blueprint): match_id = matches.index(match) break - # Add script type to list + # Add script type to list, if script_type = matches[match_id][0] - if script_type == 'multisig' and script_types[-1:] == ['signature_multisig']: + if (script_type == 'multisig' or script_type == 'multisig_redeemscript') \ + and script_types[-1:] == ['signature_multisig']: script_types.pop() script_type = 'p2sh_multisig' script_types.append(script_type) @@ -240,7 +248,7 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True self._blueprint.append('data-%d' % len(c)) @classmethod - def parse(cls, script, message=None, env_data=None, strict=True, _level=0): + def parse(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -255,6 +263,8 @@ def parse(cls, script, message=None, env_data=None, strict=True, _level=0): :type message: bytes :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -269,10 +279,10 @@ def parse(cls, script, message=None, env_data=None, strict=True, _level=0): elif isinstance(script, str): data_length = len(script) script = BytesIO(bytes.fromhex(script)) - return cls.parse_bytesio(script, message, env_data, data_length, strict, _level) + return cls.parse_bytesio(script, message, env_data, data_length, is_locking, strict, _level) @classmethod - def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, strict=True, _level=0): + def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -284,6 +294,8 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, stric :type env_data: dict :param data_length: Length of script data if known. Supply if you can to increase efficiency and lower change of incorrect parsing :type data_length: int + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -392,7 +404,7 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, stric script.seek(0) s._raw = script.read() - s.script_types = _get_script_types(blueprint) + s.script_types = _get_script_types(blueprint, is_locking=is_locking) if 'unknown' in s.script_types: s.script_types = ['unknown'] @@ -407,7 +419,7 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, stric if len(s.keys) != s.commands[-2] - 80: raise ScriptError("%d keys found but %d keys expected" % (len(s.keys), s.commands[-2] - 80)) - elif st in ['p2wpkh', 'p2wsh', 'p2sh', 'p2tr'] and len(s.commands) > 1: + elif st in ['p2wpkh', 'p2wsh', 'p2sh', 'p2tr', 'p2sh_p2wpkh', 'p2sh_p2wsh'] and len(s.commands) > 1: s.public_hash = s.commands[1] elif st == 'p2tr_unlock': s.public_hash = s.commands[0] @@ -422,7 +434,7 @@ def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, stric return s @classmethod - def parse_hex(cls, script, message=None, env_data=None, strict=True, _level=0): + def parse_hex(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -437,6 +449,8 @@ def parse_hex(cls, script, message=None, env_data=None, strict=True, _level=0): :type message: bytes :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed, incomplete or not understood. Default is True :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -445,10 +459,11 @@ def parse_hex(cls, script, message=None, env_data=None, strict=True, _level=0): :return Script: """ data_length = len(script) // 2 - return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, env_data, data_length, strict, _level) + return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, env_data, data_length, is_locking, strict, + _level) @classmethod - def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0): + def parse_bytes(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse raw script and return Script object. Extracts script commands, keys, signatures and other data. @@ -460,6 +475,8 @@ def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0) :type message: bytes :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed or incomplete :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -468,10 +485,10 @@ def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0) :return Script: """ data_length = len(script) - return cls.parse_bytesio(BytesIO(script), message, env_data, data_length, strict, _level) + return cls.parse_bytesio(BytesIO(script), message, env_data, data_length, is_locking, strict, _level) @classmethod - def parse_str(cls, script, message=None, env_data=None, strict=True, _level=0): + def parse_str(cls, script, message=None, env_data=None, is_locking=None, strict=True, _level=0): """ Parse script in string format and return Script object. Extracts script commands, keys, signatures and other data. @@ -488,6 +505,8 @@ def parse_str(cls, script, message=None, env_data=None, strict=True, _level=0): :type message: bytes :param env_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts :type env_data: dict + :param is_locking: Is this a locking script or not, use None if not known and derive from script. + :param is_locking: bool, None :param strict: Raise exception when script is malformed or incomplete :type strict: bool :param _level: Internal argument used to avoid recursive depth @@ -508,7 +527,7 @@ def parse_str(cls, script, message=None, env_data=None, strict=True, _level=0): s_items.append(getattr(op, item.lower(), 'unknown-command-%s' % item)) else: s_items.append(bytes.fromhex(item)) - return cls(s_items, message, env_data, strict, _level) + return cls(s_items, message, env_data, is_locking=is_locking) def __repr__(self): s_items = self.view(blueprint=True, as_list=True) diff --git a/bitcoinlib/transactions.py b/bitcoinlib/transactions.py index b945edf4..528be404 100644 --- a/bitcoinlib/transactions.py +++ b/bitcoinlib/transactions.py @@ -275,7 +275,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= # If unlocking script is specified extract keys, signatures, type from script if self.unlocking_script and self.script_type != 'coinbase' and not (signatures and keys): - script = Script.parse_bytes(self.unlocking_script, strict=strict) + script = Script.parse_bytes(self.unlocking_script, is_locking=False, strict=strict) self.keys = script.keys self.signatures = script.signatures if len(self.signatures): @@ -283,24 +283,8 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= sigs_required = script.sigs_required if len(script.script_types) == 1 and not self.script_type: self.script_type = script.script_types[0] - # elif script.script_types == ['signature_multisig', 'multisig']: - # self.script_type = 'p2sh_multisig' - # If unlocking script type is an embedded p2sh script - if self.script_type == 'p2wpkh': - self.script_type = 'p2sh_p2wpkh' - self.witness_type = 'p2sh-segwit' - elif self.script_type == 'p2wsh': - self.script_type = 'p2sh_p2wsh' - self.witness_type = 'p2sh-segwit' - # TODO: Check if this if is necessary - # if 'p2wpkh' in script.script_types: - # self.script_type = 'p2sh_p2wpkh' - # self.witness_type = 'p2sh-segwit' - # elif 'p2wsh' in script.script_types: - # self.script_type = 'p2sh_p2wsh' - # self.witness_type = 'p2sh-segwit' if self.locking_script and not self.signatures: - ls = Script.parse_bytes(self.locking_script, strict=strict) + ls = Script.parse_bytes(self.locking_script, is_locking=True, strict=strict) self.public_hash = self.public_hash if not ls.public_hash else ls.public_hash if ls.script_types[0] in ['p2wpkh', 'p2wsh']: self.witness_type = 'segwit' @@ -309,7 +293,6 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash= if self.script_type is None and self.witness_type is None and self.witnesses: self.witness_type = 'segwit' if self.witness_type is None or self.witness_type == 'legacy': - # if self.script_type in ['p2wpkh', 'p2wsh', 'p2sh_p2wpkh', 'p2sh_p2wsh']: if self.script_type in ['p2wpkh', 'p2wsh']: self.witness_type = 'segwit' elif self.script_type in ['p2sh_p2wpkh', 'p2sh_p2wsh']: @@ -673,7 +656,7 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri self.encoding = 'base58' self.spent = spent self.output_n = output_n - self.script = Script.parse_bytes(self.lock_script, strict=strict) + self.script = Script.parse_bytes(self.lock_script, strict=strict, is_locking=True) self.witver = witver if self._address_obj: @@ -968,7 +951,7 @@ def parse_bytesio(cls, rawtx, strict=True, network=DEFAULT_NETWORK, index=None, witness = rawtx.read(item_size) inputs[n].witnesses.append(witness) if not is_taproot: - s = Script.parse_bytes(witness, strict=strict) + s = Script.parse_bytes(witness, strict=strict, is_locking=False) if s.script_types == ['p2tr_unlock']: # FIXME: Support Taproot unlocking scripts _logger.warning("Taproot is not supported at the moment, rest of parsing input transaction "