diff --git a/src/main.py b/src/main.py index f27ea07..060a210 100644 --- a/src/main.py +++ b/src/main.py @@ -28,6 +28,8 @@ def parse_arguments(): mempool = MemPool(args.mempool) + # TODO pokracovani + block_transactions = [COINBASE_TRANSACTION] + mempool.valid_transactions transaction_hashes = [calculate_txid(COINBASE_TRANSACTION)] + [calculate_txid(json_transaction) for json_transaction in block_transactions[1:]] @@ -45,4 +47,4 @@ def parse_arguments(): print(block_hash) print(coinbase_serialized.hex()) for transaction in transaction_hashes: - print(transaction) \ No newline at end of file + print(transaction) diff --git a/src/transaction.py b/src/transaction.py index 39cf28a..9ba027a 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -1,9 +1,11 @@ import hashlib import json +from ecdsa import VerifyingKey, SECP256k1, BadSignatureError + from src.serialize import serialize_transaction -from src.utils import get_filename_without_extension -from src.verify import non_empty_vin_vout, valid_transaction_syntax, verify_p2pkh_transaction +from src.utils import decode_hex, get_filename_without_extension, hash160 +from src.verify import valid_transaction_syntax def calculate_txid(transaction_content, coinbase=False): # Serialize the transaction content @@ -35,12 +37,56 @@ def __init__(self, transaction_json_file): self.vout = json_transaction['vout'] self.json_transaction = json_transaction else: + # TODO jestli nejakej error print('Invalid transaction syntax') def is_valid(self): - if not non_empty_vin_vout(self.vin, self.vout): + # At least one input and one output. + if not self.non_empty_vin_vout(): + return False + + # Basic locktime check. + if not self.valid_locktime(): + return False + + if not self.check_input_output_sum(): return False + # Check each input validity. + for vin_idx, vin in enumerate(self.vin): + if not self.valid_input(vin_idx, vin): + return False + + # Check each output validity. + for vout in self.vout: + if not self.valid_output(vout): + return False + + return True + + def non_empty_vin_vout(self): + # Make sure neither in or out lists are empty + if not self.vin: + #print("vin is empty") + return False + if not self.vout: + #print("vout is empty") + return False + + return True + + def valid_locktime(self): + try: + locktime_int = int(self.locktime) + except ValueError: + return False + + if len(locktime_int) != 1 or self.locktime_int < 0: + return False + + return True + + def check_input_output_sum(self): input_sum = 0 for input in self.vin: input_sum = input_sum + input['prevout']['value'] @@ -48,28 +94,159 @@ def is_valid(self): output_sum = 0 for output in self.vout: output_sum = output_sum + output['value'] - + + # Output sum can't be greater than the input sum. if input_sum < output_sum: return False - input_idx = 0 - for input in self.vin: - if 'scriptsig' in input: - scriptsig = input['scriptsig'] - - scriptpubkey_type = input['prevout']['scriptpubkey_type'] - - if scriptsig == "" or scriptpubkey_type not in ["p2pkh", "p2sh"]: - return False - - if scriptpubkey_type == 'p2pkh': - if not verify_p2pkh_transaction(input_idx, self.json_transaction): - return False - else: - return False - else: + def valid_input(self, vin_idx, vin): + # TODO + 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) + elif scriptpubkey_type == "p2sh": + pass + #return self.validate_p2sh(vin) + elif scriptpubkey_type == "v0_p2wsh": + pass + #return self.validate_p2wsh(vin) + elif scriptpubkey_type == "v1_p2tr": + pass + #return self.validate_p2tr(vin) + elif scriptpubkey_type == "v0_p2wpkh": + pass + #return self.validate_p2wpkh(vin) + + # Unknown script type. + return False + + 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): + # Checking input signatures. + if "scriptsig" in vin: + prevout = vin.get("prevout", {}) + scriptpubkey_type = prevout.get("scriptpubkey_type", "") + + if vin["scriptsig"] == "": return False - input_idx += 1 + #if scriptpubkey_type == 'p2pkh': + # if not verify_p2pkh_transaction(input_idx, self.json_transaction): + # return False + #else: + # return False + + ################# + # Pubkey script # + ################# + + input_tx = vin[vin_idx] + + scriptsig = decode_hex(input_tx.get("scriptsig", "")) + + prevout = input_tx.get("prevout", "") + + if prevout == "": + return False + + scriptpubkey = decode_hex(prevout.get("scriptpubkey", "")) + + ################### + # 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] + signature = scriptsig[1:1+signature_len] + + 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 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 + + ## -------------------------------------- + + """# Extract data from input transaction + script_sig_asm = input_tx["scriptsig_asm"] + + # Parse scriptSig ASM to extract signature and public key + script_parts = script_sig_asm.split(" ") + signature_hex = script_parts[1] + public_key_hex = script_parts[3] + + r, s, hash_type = parse_der_signature(signature_hex) + + r_hex = hex(r)[2:] + s_hex = hex(s)[2:] + + der_len = len(signature_hex[:-2]) + signature_len = len(r_hex + s_hex) + 2 * 6 + + if der_len != signature_len: + return False + + signature = bytes.fromhex(r_hex + s_hex) + + public_key = bytes.fromhex(public_key_hex) + + scriptpubkey = bytes.fromhex(input_tx['prevout']['scriptpubkey']) + pubkey_hash = scriptpubkey[3:23] + + hashed_public_key = hashlib.sha256(public_key).digest() + + ripemd160 = RIPEMD160.new() + ripemd160.update(hashed_public_key) + pubkey_hash_calculated = ripemd160.digest() + + if pubkey_hash != pubkey_hash_calculated: + return False +""" + + ############################################ + # Verify the signature with the public key # + ############################################ + + # Remove the SIGHASH type from the signature. + hash_type = signature[-1] + signature = signature[:-1] + + data_signed = serialize_transaction(self.json_transaction, vin_idx, int(hash_type)) + data_hash = hashlib.sha256(data_signed).digest() + + # 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 + + return True + - return True \ No newline at end of file + return False diff --git a/src/utils.py b/src/utils.py index 32a854e..d01d192 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,3 +1,4 @@ +import hashlib import os def get_filename_without_extension(file_path): @@ -5,4 +6,13 @@ def get_filename_without_extension(file_path): filename = os.path.basename(file_path) # Remove the extension filename_without_extension = os.path.splitext(filename)[0] - return filename_without_extension \ No newline at end of file + return filename_without_extension + +def decode_hex(hex_data): + # Decode a hex-encoded data into its raw bytecode. + return bytes.fromhex(hex_data) + +def hash160(data): + # SHA-256 followed by RIPEMD-160 (Bitcoin's HASH160). + sha256_hash = hashlib.sha256(data).digest() + return hashlib.new('ripemd160', sha256_hash).digest() \ No newline at end of file diff --git a/src/verify.py b/src/verify.py index c568fbc..8a07f8e 100644 --- a/src/verify.py +++ b/src/verify.py @@ -1,67 +1,49 @@ -import ecdsa -import hashlib - -from Crypto.Hash import RIPEMD160 -from src.serialize import serialize_transaction - def valid_transaction_syntax(json_transaction): required = ["version", "locktime", "vin", "vout"] for field in required: if field not in json_transaction: - print('Required field is missing') + #print('Required field is missing') return False if not isinstance(json_transaction["version"], int): - print('Invalid data type') + #print('Invalid data type') return False if not isinstance(json_transaction["locktime"], int): - print('Invalid data type') + #print('Invalid data type') return False if not isinstance(json_transaction["vin"], list): - print('Invalid data type') + #print('Invalid data type') return False if not isinstance(json_transaction["vout"], list): - print('Invalid data type') + #print('Invalid data type') return False # Check inputs for input in json_transaction['vin']: if not isinstance(input, dict): - print('Invalid data type') + #print('Invalid data type') return False if 'txid' not in input or 'vout' not in input: - print('Invalid data type') + #print('Invalid data type') return False # Check outputs - for output in json_transaction['vout']: if not isinstance(output, dict): - print('Invalid data type') + #print('Invalid data type') return False if 'scriptpubkey' not in output or 'value' not in output: - print('Invalid data type') + #print('Invalid data type') return False return True - -def non_empty_vin_vout(vin, vout): - # Make sure neither in or out lists are empty - if not vin: - print("vin is empty") - return False - if not vout: - print("vout is empty") - return False - - return True - +""" def parse_der_signature(der_signature_with_hash_type): # Remove the hash_type from the DER signature der_signature = der_signature_with_hash_type[:-2] @@ -75,63 +57,4 @@ def parse_der_signature(der_signature_with_hash_type): s = int.from_bytes(der_bytes[s_length_index + 1:s_length_index + 1 + s_length], 'big') hash_type = der_bytes[-1] - return r, s, hash_type - -def verify_p2pkh_transaction(input_idx, json_transaction): - ################# - # Pubkey script # - ################# - - input_tx = json_transaction["vin"][input_idx] - - # Extract data from input transaction - script_sig_asm = input_tx["scriptsig_asm"] - - # Parse scriptSig ASM to extract signature and public key - script_parts = script_sig_asm.split(" ") - signature_hex = script_parts[1] - public_key_hex = script_parts[3] - - r, s, hash_type = parse_der_signature(signature_hex) - - r_hex = hex(r)[2:] - s_hex = hex(s)[2:] - - der_len = len(signature_hex[:-2]) - signature_len = len(r_hex + s_hex) + 2 * 6 - - if der_len != signature_len: - return False - - signature = bytes.fromhex(r_hex + s_hex) - - public_key = bytes.fromhex(public_key_hex) - - scriptpubkey = bytes.fromhex(input_tx['prevout']['scriptpubkey']) - pubkey_hash = scriptpubkey[3:23] - - hashed_public_key = hashlib.sha256(public_key).digest() - - ripemd160 = RIPEMD160.new() - ripemd160.update(hashed_public_key) - pubkey_hash_calculated = ripemd160.digest() - - if pubkey_hash != pubkey_hash_calculated: - return False - - - #################### - # Signature script # - #################### - - data_signed = serialize_transaction(json_transaction, input_idx, int(hash_type)) - data_hash = hashlib.sha256(data_signed).digest() - - # Verify the signature - verifying_key = ecdsa.VerifyingKey.from_string(public_key, curve=ecdsa.SECP256k1) - try: - verifying_key.verify(signature, data_hash, hashlib.sha256) - except ecdsa.BadSignatureError: - return False - - return True + return r, s, hash_type"""