Skip to content

Commit

Permalink
Add p2sh
Browse files Browse the repository at this point in the history
  • Loading branch information
davidchocholaty committed Nov 6, 2024
1 parent 8c80537 commit 2f9549e
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 250 deletions.
80 changes: 1 addition & 79 deletions src/op_codes.py
Original file line number Diff line number Diff line change
@@ -1,85 +1,7 @@
# The following code is taken from: https://github.com/SummerOfBitcoin/code-challenge-2024-Nesopie/blob/840aeb58dd68cba3fba33f96d71ea0790c1840a8/src/features/script/op_codes.ts

OP_CODES = {
""" 'OP_0': b'\x00',
'OP_PUSHBYTES_1': b'\x01',
'OP_PUSHBYTES_2': b'\x02',
'OP_PUSHBYTES_3': b'\x03',
'OP_PUSHBYTES_4': b'\x04',
'OP_PUSHBYTES_5': b'\x05',
'OP_PUSHBYTES_6': b'\x06',
'OP_PUSHBYTES_7': b'\x07',
'OP_PUSHBYTES_8': b'\x08',
'OP_PUSHBYTES_9': b'\x09',
'OP_PUSHBYTES_10': b'\x0a',
'OP_PUSHBYTES_11': b'\x0b',
'OP_PUSHBYTES_12': b'\x0c',
'OP_PUSHBYTES_13': b'\x0d',
'OP_PUSHBYTES_14': b'\x0e',
'OP_PUSHBYTES_15': b'\x0f',
'OP_PUSHBYTES_16': b'\x10',
'OP_PUSHBYTES_17': b'\x11',
'OP_PUSHBYTES_18': b'\x12',
'OP_PUSHBYTES_19': b'\x13',
'OP_PUSHBYTES_20': b'\x14',
'OP_PUSHBYTES_21': b'\x15',
'OP_PUSHBYTES_22': b'\x16',
'OP_PUSHBYTES_23': b'\x17',
'OP_PUSHBYTES_24': b'\x18',
'OP_PUSHBYTES_25': b'\x19',
'OP_PUSHBYTES_26': b'\x1a',
'OP_PUSHBYTES_27': b'\x1b',
'OP_PUSHBYTES_28': b'\x1c',
'OP_PUSHBYTES_29': b'\x1d',
'OP_PUSHBYTES_30': b'\x1e',
'OP_PUSHBYTES_31': b'\x1f',
'OP_PUSHBYTES_32': b'\x20',
'OP_PUSHBYTES_33': b'\x21',
'OP_PUSHBYTES_34': b'\x22',
'OP_PUSHBYTES_35': b'\x23',
'OP_PUSHBYTES_36': b'\x24',
'OP_PUSHBYTES_37': b'\x25',
'OP_PUSHBYTES_38': b'\x26',
'OP_PUSHBYTES_39': b'\x27',
'OP_PUSHBYTES_40': b'\x28',
'OP_PUSHBYTES_41': b'\x29',
'OP_PUSHBYTES_42': b'\x2a',
'OP_PUSHBYTES_43': b'\x2b',
'OP_PUSHBYTES_44': b'\x2c',
'OP_PUSHBYTES_45': b'\x2d',
'OP_PUSHBYTES_46': b'\x2e',
'OP_PUSHBYTES_47': b'\x2f',
'OP_PUSHBYTES_48': b'\x30',
'OP_PUSHBYTES_49': b'\x31',
'OP_PUSHBYTES_50': b'\x32',
'OP_PUSHBYTES_51': b'\x33',
'OP_PUSHBYTES_52': b'\x34',
'OP_PUSHBYTES_53': b'\x35',
'OP_PUSHBYTES_54': b'\x36',
'OP_PUSHBYTES_55': b'\x37',
'OP_PUSHBYTES_56': b'\x38',
'OP_PUSHBYTES_57': b'\x39',
'OP_PUSHBYTES_58': b'\x3a',
'OP_PUSHBYTES_59': b'\x3b',
'OP_PUSHBYTES_60': b'\x3c',
'OP_PUSHBYTES_61': b'\x3d',
'OP_PUSHBYTES_62': b'\x3e',
'OP_PUSHBYTES_63': b'\x3f',
'OP_PUSHBYTES_64': b'\x40',
'OP_PUSHBYTES_65': b'\x41',
'OP_PUSHBYTES_66': b'\x42',
'OP_PUSHBYTES_67': b'\x43',
'OP_PUSHBYTES_68': b'\x44',
'OP_PUSHBYTES_69': b'\x45',
'OP_PUSHBYTES_70': b'\x46',
'OP_PUSHBYTES_71': b'\x47',
'OP_PUSHBYTES_72': b'\x48',
'OP_PUSHBYTES_73': b'\x49',
'OP_PUSHBYTES_74': b'\x4a',
'OP_PUSHBYTES_75': b'\x4b', """
'OP_PUSHDATA1': b'\x4c',
'OP_PUSHDATA2': b'\x4d',
'OP_PUSHDATA4': b'\x4e',
'OP_0': b'\x00',
'OP_1NEGATE': b'\x4f',
'OP_RESERVED': b'\x50',
'OP_1': b'\x51',
Expand Down
26 changes: 21 additions & 5 deletions src/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,33 @@ def execute(self) -> bool:
op = self.script[i:i+1]

# Handle data push operations
if op not in OP_CODES.values():
length = int.from_bytes(op, 'little')
if length > 75: # Use OP_PUSHDATA operations for larger chunks
raise InvalidScriptException(f"Invalid push operation length: {length}")
if op not in OP_CODES.values():
length = int.from_bytes(op, 'little')
if length > 75: # Use OP_PUSHDATA operations for larger chunks
if length == 76: #'OP_PUSHDATA1'
length = self.script[i+1]
data = self.script[i+2:i+2+length]
self.stack.push(data)
return length + 2
elif length == 77: # 'OP_PUSHDATA2'
length = int.from_bytes(self.script[i+1:i+3], 'little')
data = self.script[i+3:i+3+length]
self.stack.push(data)
return length + 3
elif length == 78: # 'OP_PUSHDATA4'
length = int.from_bytes(self.script[i+1:i+5], 'little')
data = self.script[i+5:i+5+length]
self.stack.push(data)
return length + 5
else:
raise InvalidScriptException(f"Invalid push operation length: {length}")
data = self.script[i+1:i+1+length]
self.stack.push(data)
i += length + 1
continue

# Handle opcodes
op_name = list(OP_CODES.keys())[list(OP_CODES.values()).index(op)]
op_name = list(OP_CODES.keys())[list(OP_CODES.values()).index(op)]
i += self._execute_opcode(op_name)

# Script executed successfully if stack is not empty and top value is true
Expand Down
178 changes: 12 additions & 166 deletions src/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def is_valid(self):
return False

# Check each input validity.
for vin_idx, vin in enumerate(self.vin):
if not self.valid_input(vin_idx, vin):
for vin in self.vin:
if not self.valid_input(vin):
return False

# Check each output validity.
Expand Down Expand Up @@ -94,18 +94,17 @@ def check_input_output_sum(self):

return True

def valid_input(self, vin_idx, vin):
def valid_input(self, vin):
if vin.get("is_coinbase", False):
return False

prevout = vin.get("prevout", {})
scriptpubkey_type = prevout.get("scriptpubkey_type", "")

if scriptpubkey_type == "p2pkh":
return self.validate_p2pkh(vin_idx, vin)
return self.validate_p2pkh(vin)
elif scriptpubkey_type == "p2sh":
pass
#return self.validate_p2sh_p2wpkh(vin_idx, vin)
return self.validate_p2sh_p2wpkh(vin)
elif scriptpubkey_type == "v0_p2wsh":
pass
#return self.validate_p2wsh(vin)
Expand All @@ -123,7 +122,7 @@ def valid_output(self, vout):
scriptpubkey_type = vout.get("scriptpubkey_type", "")
return scriptpubkey_type in ["v0_p2wpkh", "p2sh", "v0_p2wsh", "v1_p2tr", "p2pkh"]

def validate_p2pkh(self, vin_idx, vin):
def validate_p2pkh(self, vin):
#################
# Pubkey script #
#################
Expand All @@ -142,171 +141,18 @@ def validate_p2pkh(self, vin_idx, vin):
#print(is_valid)

return is_valid
"""
#####################################################################
# Extract signature and public key from scriptSig (Parse scriptSig) #
#####################################################################
# https://learnmeabitcoin.com/technical/script/p2pkh/
# Explanation: the scriptSig contains the signature and the public key (including ASM instructions).
signature_len = scriptsig[0] # The first byte represents the length of the DER signature (including hash type)
signature_w_hash_type = scriptsig[1:1+signature_len] # Extract the signature (includes the hash type at the end)
# The last byte of the signature is the hash type (e.g., SIGHASH_ALL = 0x01)
signature = signature_w_hash_type[:-1]
hash_type = signature_w_hash_type[-1]
public_key_idx = 1 + signature_len
public_key_len = scriptsig[public_key_idx]
public_key = scriptsig[public_key_idx+1:public_key_idx+1+public_key_len]
#######################
# Parse DER signature #
#######################
# https://bitcoin.stackexchange.com/questions/92680/what-are-the-der-signature-and-sec-format
# https://learnmeabitcoin.com/technical/keys/signature/
# Remove the hash_type from the DER signature
der_signature = signature_w_hash_type[:-1]
r, s, hash_type = parse_der_signature_bytes(der_signature)
der_len = len(der_signature)
signature_len = len(r + s) + 6
if der_len != signature_len:
return False
signature = r + s
#print(signature)
######################
# Parse scriptPubKey #
######################
# https://learnmeabitcoin.com/technical/script/p2pkh/
# Explanation: the scriptPubKey contains: DUP, HASH160, public key hash (including OP_PUSHBYTES_20), EQUALVERIFY and CHECKSIG.
if scriptpubkey[0:1] != b'\x76' or scriptpubkey[1:2] != b'\xa9' or scriptpubkey[2:3] != b'\x14':
return False # Not a valid P2PKH scriptPubKey (missing OP_DUP, OP_HASH160, or length mismatch)

if scriptpubkey[23:24] != b'\x88' or scriptpubkey[24:25] != b'\xac':
return False # Not a valid P2PKH scriptPubKey (missing OP_EQUALVERIFY or OP_CHECKSIG)
pkh = scriptpubkey[3:23]
# Compute the public key hash (HASH160 of the public key) and compare with scriptPubKey
calc_pkh = hash160(public_key)
if calc_pkh != pkh:
return False # Public key hash does not match
############################################
# Verify the signature with the public key #
############################################
data_signed = serialize_transaction(self.json_transaction, vin_idx, int(hash_type))
data_hash = hashlib.sha256(data_signed).digest()
#print(self.json_transaction)
#print("********************************")
# Verify the signature
verifying_key = VerifyingKey.from_string(public_key, curve=SECP256k1)
try:
verifying_key.verify(signature, data_hash, hashlib.sha256)
except BadSignatureError:
return False
print(public_key)
print("-------------------")
return True
"""
"""
def validate_p2sh_p2wpkh(self, vin_idx, vin):
# Extract scriptSig and witness
def validate_p2sh_p2wpkh(self, vin):
scriptsig = decode_hex(vin.get("scriptsig", ""))
witness = vin.get("witness", [])
if not scriptsig or len(witness) < 2:
if not scriptsig:
return False
print(vin["txid"])
prevout = vin.get("prevout", {})
if not prevout:
return False
scriptpubkey = decode_hex(prevout.get("scriptpubkey", ""))

#############################
# Check if it's a P2SH script #
#############################
if len(scriptpubkey) != 23 or scriptpubkey[0:1] != b'\xa9' or scriptpubkey[-1:] != b'\x87':
return False # Not a valid P2SH scriptPubKey
# Extract the redeem script hash from the scriptPubKey
# Extract redeem script hash from scriptPubKey
if scriptpubkey[0] != 0xa9: # Check for OP_HASH160
return False
length_of_hash = scriptpubkey[1]
if length_of_hash != 0x14: # 20 bytes
return False
expected_redeem_script_hash = scriptpubkey[2:2+length_of_hash]
###########################
# Extract the redeem script #
###########################
# The redeem script is the data in the scriptSig
redeem_script = scriptsig
# Hash the redeem script and compare it with the expected hash in the scriptPubKey
redeem_script_hash = hash160(redeem_script)
#print("rsh: ", redeem_script_hash)
#print("ersh: ", expected_redeem_script_hash)
if redeem_script_hash != expected_redeem_script_hash:
return False # Redeem script hash does not match
##############################
# Parse and execute redeem script #
##############################
# The redeem script should be a P2WPKH script: OP_0 <20-byte-public-key-hash>
if len(redeem_script) != 22 or redeem_script[0:1] != b'\x00' or redeem_script[1:2] != b'\x14':
return False # Not a valid P2WPKH redeem script
# Extract the public key hash from the redeem script
public_key_hash = redeem_script[2:]
######################
# Verify the witness #
######################
# The witness field contains:
# - witness[0] = signature
# - witness[1] = public key
signature = decode_hex(witness[0])
public_key = decode_hex(witness[1])
# Compute the public key hash (HASH160 of the public key) and compare with the public key hash in the redeem script
calc_pkh = hash160(public_key)
if calc_pkh != public_key_hash:
return False # Public key hash does not match
############################################
# Verify the signature with the public key #
############################################
data_signed = serialize_transaction(self.json_transaction, vin_idx, 1) # SIGHASH_ALL is typically 1
data_hash = hashlib.sha256(data_signed).digest()
# Verify the signature
verifying_key = VerifyingKey.from_string(public_key, curve=SECP256k1)
try:
verifying_key.verify(signature[:-1], data_hash, hashlib.sha256) # Remove the last byte (hash type)
except BadSignatureError:
return False
# Combine and verify
script = Script.combine_scripts(scriptsig, scriptpubkey, json_transaction=self.json_transaction)
is_valid = script.execute()

return True """
return is_valid

0 comments on commit 2f9549e

Please sign in to comment.