Skip to content

Commit

Permalink
Fix script issues, test with CLTV scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
Cryp Toon committed Mar 18, 2024
1 parent 54a2ae1 commit 63d8bcf
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 65 deletions.
2 changes: 2 additions & 0 deletions bitcoinlib/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
'p2tr': ('locking', ['op_n', 'data'], [32]),
'multisig': ('locking', ['op_n', 'key', 'op_n', op.op_checkmultisig], []),
'p2pk': ('locking', ['key', op.op_checksig], []),
'locktime_cltv_script': ('locking', ['locktime_cltv', op.op_checklocktimeverify, op.op_drop, op.op_dup,
op.op_hash160, 'data', op.op_equalverify, op.op_checksig], [20]),
'nulldata': ('locking', [op.op_return, 'data'], [0]),
'nulldata_1': ('locking', [op.op_return, op.op_0], []),
'nulldata_2': ('locking', [op.op_return], []),
Expand Down
68 changes: 35 additions & 33 deletions bitcoinlib/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def get_data_type(data):
class Script(object):

def __init__(self, commands=None, message=None, script_types='', is_locking=True, keys=None, signatures=None,
blueprint=None, tx_data=None, public_hash=b'', sigs_required=None, redeemscript=b'',
blueprint=None, env_data=None, public_hash=b'', sigs_required=None, redeemscript=b'',
hash_type=SIGHASH_ALL):
"""
Create a Script object with specified parameters. Use parse() method to create a Script from raw hex
Expand Down Expand Up @@ -173,8 +173,8 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True
:type signatures: list of Signature
:param blueprint: Simplified version of script, normally generated by Script object
:type blueprint: list of str
:param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts
:type tx_data: dict
: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 public_hash: Public hash of key or redeemscript used to create scripts
:type public_hash: bytes
:param sigs_required: Nubmer of signatures required to create multisig script
Expand All @@ -193,13 +193,13 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True
self.keys = keys if keys else []
self.signatures = signatures if signatures else []
self._blueprint = blueprint if blueprint else []
self.tx_data = {} if not tx_data else tx_data
self.env_data = {} if not env_data else env_data
self.sigs_required = sigs_required if sigs_required else len(self.keys) if len(self.keys) else 1
self.redeemscript = redeemscript
self.public_hash = public_hash
self.hash_type = hash_type

if not self.commands and self.script_types and (self.keys or self.signatures or self.public_hash):
if not self.commands and self.script_types: # and (self.keys or self.signatures or self.public_hash):
for st in self.script_types:
st_values = SCRIPT_TYPES[st]
script_template = st_values[1]
Expand All @@ -217,6 +217,8 @@ def __init__(self, commands=None, message=None, script_types='', is_locking=True
command = [sig_n_and_m.pop() + 80]
elif tc == 'redeemscript':
command = [self.redeemscript]
elif tc in self.env_data:
command = [env_data[tc]]
if not command or command == [b'']:
raise ScriptError("Cannot create script, please supply %s" % (tc if tc != 'data' else
'public key hash'))
Expand All @@ -238,7 +240,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, tx_data=None, strict=True, _level=0):
def parse(cls, script, message=None, env_data=None, strict=True, _level=0):
"""
Parse raw script and return Script object. Extracts script commands, keys, signatures and other data.
Expand All @@ -251,8 +253,8 @@ def parse(cls, script, message=None, tx_data=None, strict=True, _level=0):
:type script: BytesIO, bytes, str
:param message: Signed message to verify, normally a transaction hash
:type message: bytes
:param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts
:type tx_data: dict
: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 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 @@ -267,19 +269,19 @@ def parse(cls, script, message=None, tx_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, tx_data, data_length, strict, _level)
return cls.parse_bytesio(script, message, env_data, data_length, strict, _level)

@classmethod
def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict=True, _level=0):
def parse_bytesio(cls, script, message=None, env_data=None, data_length=0, strict=True, _level=0):
"""
Parse raw script and return Script object. Extracts script commands, keys, signatures and other data.
:param script: Raw script to parse in bytes, BytesIO or hexadecimal string format
:type script: BytesIO
:param message: Signed message to verify, normally a transaction hash
:type message: bytes
:param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts
:type tx_data: dict
: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 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 strict: Raise exception when script is malformed, incomplete or not understood. Default is True
Expand All @@ -297,8 +299,8 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict
sigs_required = None
# hash_type = SIGHASH_ALL # todo: check
hash_type = None
if not tx_data:
tx_data = {}
if not env_data:
env_data = {}

chb = script.read(1)
ch = int.from_bytes(chb, 'big')
Expand Down Expand Up @@ -385,7 +387,7 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict
chb = script.read(1)
ch = int.from_bytes(chb, 'big')

s = cls(commands, message, keys=keys, signatures=signatures, blueprint=blueprint, tx_data=tx_data,
s = cls(commands, message, keys=keys, signatures=signatures, blueprint=blueprint, env_data=env_data,
hash_type=hash_type)
script.seek(0)
s._raw = script.read()
Expand All @@ -412,15 +414,15 @@ def parse_bytesio(cls, script, message=None, tx_data=None, data_length=0, strict
elif st == 'p2pkh' and len(s.commands) > 2:
s.public_hash = s.commands[2]
s.redeemscript = redeemscript if redeemscript else s.redeemscript
if s.redeemscript and 'redeemscript' not in s.tx_data:
s.tx_data['redeemscript'] = s.redeemscript
if s.redeemscript and 'redeemscript' not in s.env_data:
s.env_data['redeemscript'] = s.redeemscript

s.sigs_required = sigs_required if sigs_required else s.sigs_required

return s

@classmethod
def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0):
def parse_hex(cls, script, message=None, env_data=None, strict=True, _level=0):
"""
Parse raw script and return Script object. Extracts script commands, keys, signatures and other data.
Expand All @@ -433,8 +435,8 @@ def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0):
:type script: str
:param message: Signed message to verify, normally a transaction hash
:type message: bytes
:param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts
:type tx_data: dict
: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 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 @@ -443,10 +445,10 @@ def parse_hex(cls, script, message=None, tx_data=None, strict=True, _level=0):
:return Script:
"""
data_length = len(script) // 2
return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, tx_data, data_length, strict, _level)
return cls.parse_bytesio(BytesIO(bytes.fromhex(script)), message, env_data, data_length, strict, _level)

@classmethod
def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0):
def parse_bytes(cls, script, message=None, env_data=None, strict=True, _level=0):
"""
Parse raw script and return Script object. Extracts script commands, keys, signatures and other data.
Expand All @@ -456,8 +458,8 @@ def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0):
:type script: bytes
:param message: Signed message to verify, normally a transaction hash
:type message: bytes
:param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts
:type tx_data: dict
: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 strict: Raise exception when script is malformed or incomplete
:type strict: bool
:param _level: Internal argument used to avoid recursive depth
Expand All @@ -466,7 +468,7 @@ def parse_bytes(cls, script, message=None, tx_data=None, strict=True, _level=0):
:return Script:
"""
data_length = len(script)
return cls.parse_bytesio(BytesIO(script), message, tx_data, data_length, strict, _level)
return cls.parse_bytesio(BytesIO(script), message, env_data, data_length, strict, _level)

def __repr__(self):
s_items = []
Expand Down Expand Up @@ -496,8 +498,8 @@ def __add__(self, other):
self.signatures += other.signatures
self._blueprint += other._blueprint
self.script_types = _get_script_types(self._blueprint)
if other.tx_data and not self.tx_data:
self.tx_data = other.tx_data
if other.env_data and not self.env_data:
self.env_data = other.env_data
if other.redeemscript and not self.redeemscript:
self.redeemscript = other.redeemscript
return self
Expand Down Expand Up @@ -556,7 +558,7 @@ def serialize_list(self):
clist.append(bytes(cmd))
return clist

def evaluate(self, message=None, tx_data=None):
def evaluate(self, message=None, env_data=None):
"""
Evaluate script, run all commands and check if it is valid
Expand All @@ -577,12 +579,12 @@ def evaluate(self, message=None, tx_data=None):
:param message: Signed message to verify, normally a transaction hash. Leave empty to use Script.message. If supplied Script.message will be ignored.
:type message: bytes
:param tx_data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts. Leave emtpy to use Script.tx_data. If supplied Script.tx_data will be ignored
:param data: Dictionary with extra information needed to verify script. Such as 'redeemscript' for multisignature scripts and 'blockcount' for time locked scripts. Leave emtpy to use Script.data. If supplied Script.data will be ignored
:return bool: Valid or not valid
"""
self.message = self.message if message is None else message
self.tx_data = self.tx_data if tx_data is None else tx_data
self.env_data = self.env_data if env_data is None else env_data
self.stack = Stack()

commands = self.commands[:]
Expand All @@ -609,12 +611,12 @@ def evaluate(self, message=None, tx_data=None):
if method_name == 'op_checksig' or method_name == 'op_checksigverify':
res = method(self.message)
elif method_name == 'op_checkmultisig' or method_name == 'op_checkmultisigverify':
res = method(self.message, self.tx_data)
res = method(self.message, self.env_data)
elif method_name == 'op_checklocktimeverify':
res = self.stack.op_checklocktimeverify(
self.tx_data['sequence'], self.tx_data.get('locktime'))
self.env_data['sequence'], self.env_data.get('locktime'))
elif method_name == 'op_checksequenceverify':
res = self.stack.op_checksequenceverify(self.tx_data['sequence'], self.tx_data['version'])
res = self.stack.op_checksequenceverify(self.env_data['sequence'], self.env_data['version'])
else:
res = method()
if res is False:
Expand Down
Loading

0 comments on commit 63d8bcf

Please sign in to comment.