Skip to content

Commit

Permalink
Cleanup Script and script_types
Browse files Browse the repository at this point in the history
  • Loading branch information
Cryp Toon committed May 22, 2024
1 parent a98dfdb commit f39ccc0
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 39 deletions.
7 changes: 5 additions & 2 deletions bitcoinlib/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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], []),
Expand Down
51 changes: 35 additions & 16 deletions bitcoinlib/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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']

Expand All @@ -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]
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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)
Expand Down
25 changes: 4 additions & 21 deletions bitcoinlib/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,32 +275,16 @@ 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):
self.hash_type = self.signatures[0].hash_type
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'
Expand All @@ -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']:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 "
Expand Down

0 comments on commit f39ccc0

Please sign in to comment.