diff --git a/requirements.txt b/requirements.txt index ed2a25e..746c8a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ ecdsa==0.19.0 pycryptodome==3.20.0 -#sha3==0.2.1 \ No newline at end of file diff --git a/run.sh b/run.sh index f0c97c6..da26132 100755 --- a/run.sh +++ b/run.sh @@ -4,4 +4,8 @@ #venv/bin/pip install --upgrade pip #venv/bin/pip install -r requirements.txt #source /venv/bin/activate -python3 src/main.py --mempool=mempool > output.txt \ No newline at end of file +python3 -m venv venv +venv/bin/pip install --upgrade pip +venv/bin/pip install -r requirements.txt +source venv/bin/activate +python3 src/main.py --mempool=mempool > output.txt diff --git a/src/coinbase_transaction.py b/src/coinbase_transaction.py index af2a18c..95c13dc 100644 --- a/src/coinbase_transaction.py +++ b/src/coinbase_transaction.py @@ -1,31 +1,40 @@ -COINBASE_TRANSACTION = { - "version": 2, - "locktime": 0xffffffff, - "vin": [ - { - "txid": "0000000000000000000000000000000000000000000000000000000000000000", - "vout": 0xffffffff, - "sequence": 0xffffffff, - "is_coinbase": True, - "scriptsig": "160014fd91039e25b0827748473fce351afd8ead4ecdce", - "scriptsig_asm": "OP_PUSHBYTES_22 0014fd91039e25b0827748473fce351afd8ead4ecdce", - "witness": [ - "0000000000000000000000000000000000000000000000000000000000000000", - ] - } - ], - "vout": [ - { - "scriptpubkey": "0014ad4cc1cc859c57477bf90d0f944360d90a3998bf", - "scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 ad4cc1cc859c57477bf90d0f944360d90a3998bf", - "scriptpubkey_type": "v0_p2wpkh", - "scriptpubkey_address": "bc1q44xvrny9n3t5w7lep58egsmqmy9rnx9lt6u0tc", - "value": 100000 - }, - { - "scriptpubkey": "", - "scriptpubkey_type": "op_return", - "value": 0 - } - ] - } \ No newline at end of file +COINBASE_TRANSACTION = { + "version": 1, + "locktime": 0, + "vin": [ + { + "txid": "0000000000000000000000000000000000000000000000000000000000000000", + "vout": 4294967295, + "prevout": { + "scriptpubkey": "41047eda6bd04fb27cab6e7c28c99b94977f073e912f25d1ff7165d9c95cd9bbe6da7e7ad7f2acb09e0ced91705f7616af53bee51a238b7dc527f2be0aa60469d140ac", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 423877331b30a905240c7e1f2adee4ebaa47c5f6 OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "14gnf7L2DjBYKFuWb6iftBoWE9hmAoFbcF", + "value": 2504928, + }, + "scriptsig": "03233708184d696e656420627920416e74506f6f6c373946205b8160a4256c0000946e0100", + "scriptsig_asm": "OP_PUSHBYTES_22 001415ff0337937ecadd10ce56ffdfd4674817613223", + "witness": [ + "0000000000000000000000000000000000000000000000000000000000000000", + ], + "is_coinbase": True, + "sequence": 4294967295, + } + ], + "vout": [ + { + "scriptpubkey": "76a914edf10a7fac6b32e24daa5305c723f3de58db1bc888ac", + "scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 71a3d2f54b0917dc9d2c877b2861ac52967dec7f OP_EQUALVERIFY OP_CHECKSIG", + "scriptpubkey_type": "p2pkh", + "scriptpubkey_address": "1BMscNZbFKdUDYi3bnF5XEmkWT3WPmRBDJ", + "value": 28278016, + }, + { + "scriptpubkey": "", + "scriptpubkey_asm": "OP_HASH160 OP_PUSHBYTES_20 423877331b30a905240c7e1f2adee4ebaa47c5f6 OP_EQUAL", + "scriptpubkey_type": "p2sh", + "scriptpubkey_address": "37jAAWEdJ9D9mXybRobcveioxSkt7Lkwog", + "value": 0000000000000000, + }, + ], + } diff --git a/src/main.py b/src/main.py index 060a210..26ff335 100644 --- a/src/main.py +++ b/src/main.py @@ -13,6 +13,7 @@ from src.mining import calculate_witness_commitment, block_mining from src.serialize import serialize_transaction from src.transaction import calculate_txid +from src.utils import double_spending def parse_arguments(): parser = argparse.ArgumentParser(description='Simulation of the mining process of a block') @@ -28,11 +29,30 @@ def parse_arguments(): mempool = MemPool(args.mempool) - # TODO pokracovani + # Check double spending + non_double_spend = [] + non_double_spend = [tx for i, tx in enumerate(mempool.valid_transactions) if not double_spending(non_double_spend[:i], tx)] - block_transactions = [COINBASE_TRANSACTION] + mempool.valid_transactions - - transaction_hashes = [calculate_txid(COINBASE_TRANSACTION)] + [calculate_txid(json_transaction) for json_transaction in block_transactions[1:]] + mempool.valid_transactions = non_double_spend + + block_transactions = [] + + total_weight = 0 + total_fees = 0 + max_block_weight = 4000000 + + # Sort the transactions by the fee in descending order + transactions_sorted_by_fee = sorted(mempool.valid_transactions, key=lambda tx: tx.fee, reverse=True) + + for tx in transactions_sorted_by_fee: + tx_weight = tx.calculate_weight() + if total_weight + tx_weight > max_block_weight: + break + block_transactions.append(tx) + total_weight = total_weight + tx_weight + total_fees = total_fees + tx.fee + + transaction_hashes = [calculate_txid(COINBASE_TRANSACTION, True)] + [calculate_txid(transaction.json_transaction, transaction.has_witness) for transaction in block_transactions] block_hash = block_mining(transaction_hashes).hex() wtxids = ["0000000000000000000000000000000000000000000000000000000000000000"] + transaction_hashes[1:] diff --git a/src/mempool.py b/src/mempool.py index 1231f62..fa11785 100644 --- a/src/mempool.py +++ b/src/mempool.py @@ -7,4 +7,4 @@ def __init__(self, root_dir): self.root_dir = root_dir self.transaction_files = [os.path.join(self.root_dir, file) for file in os.listdir(self.root_dir) if file.endswith('.json')] self.transactions = [Transaction(file) for file in self.transaction_files] - self.valid_transactions = [transaction.json_transaction for transaction in self.transactions if transaction.is_valid()] + self.valid_transactions = [transaction for transaction in self.transactions if transaction.is_valid()] diff --git a/src/script.py b/src/script.py index 471a34e..fea6aa9 100644 --- a/src/script.py +++ b/src/script.py @@ -1,8 +1,9 @@ -from dataclasses import dataclass -from typing import List, Any, Union +from typing import List, Union +from Crypto.Hash import RIPEMD160 import hashlib import ecdsa from src.op_codes import OP_CODES +from src.serialize import serialize_transaction class InvalidScriptException(Exception): """Custom exception for Script execution errors""" @@ -32,115 +33,18 @@ def is_empty(self) -> bool: return len(self._items) == 0 class Script: - def __init__(self, script: bytes, json_transaction: dict = None, input_index: int = 0): + def __init__(self, script: bytes, json_transaction: dict = None, input_index: int = 0, segwit: bool = False): self.script = script self.stack = Stack() self.alt_stack = Stack() self.if_stack: List[bool] = [] self.transaction = json_transaction # Store JSON transaction self.input_index = input_index + self.segwit = segwit def create_signature_hash(self, hash_type: int) -> bytes: - """ - Create the signature hash for the transaction based on the hash type. - This is what gets signed/verified in OP_CHECKSIG. - """ - if not self.transaction: - raise InvalidScriptException("No transaction context provided for signature verification") - - # Create a copy of the transaction - tx_copy = self.transaction.copy() - - # Clear all input scripts - for inp in tx_copy['vin']: - inp['scriptsig'] = '' - - # Handle different hash types - if hash_type & 0x1F == 0x01: # SIGHASH_ALL - # Most common, signs all inputs and outputs - # Current input gets the subscript - tx_copy['vin'][self.input_index]['scriptsig'] = self.script.hex() - - elif hash_type & 0x1F == 0x02: # SIGHASH_NONE - # Signs all inputs, but no outputs - tx_copy['vout'] = [] - # Zero out sequence numbers of other inputs - for i in range(len(tx_copy['vin'])): - if i != self.input_index: - tx_copy['vin'][i]['sequence'] = 0 - - elif hash_type & 0x1F == 0x03: # SIGHASH_SINGLE - # Signs all inputs and only the output with same index - if self.input_index >= len(tx_copy['vout']): - raise InvalidScriptException("SIGHASH_SINGLE invalid output index") - # Keep only the output at the same index - output = tx_copy['vout'][self.input_index] - tx_copy['vout'] = [{'value': -1, 'scriptpubkey': ''}] * self.input_index - tx_copy['vout'].append(output) - # Zero out sequence numbers of other inputs - for i in range(len(tx_copy['vin'])): - if i != self.input_index: - tx_copy['vin'][i]['sequence'] = 0 - - if hash_type & 0x80: # SIGHASH_ANYONECANPAY - # Only sign the current input - current_input = tx_copy['vin'][self.input_index] - tx_copy['vin'] = [current_input] - self.input_index = 0 - - # Serialize the modified transaction - serialized = self.serialize_transaction(tx_copy) - - # Add hash type - serialized += hash_type.to_bytes(4, 'little') - - # Double SHA256 - return hashlib.sha256(hashlib.sha256(serialized).digest()).digest() - - def serialize_transaction(self, tx: dict) -> bytes: - """Serialize a transaction for signing/verification""" - result = bytearray() - - # Version - result.extend(tx['version'].to_bytes(4, 'little')) - - # Number of inputs - result.extend(len(tx['vin']).to_bytes(1, 'little')) - - # Inputs - for inp in tx['vin']: - # Previous transaction hash (reverse byte order) - prev_tx = bytes.fromhex(inp['txid'])[::-1] - result.extend(prev_tx) - - # Previous output index - result.extend(inp['vout'].to_bytes(4, 'little')) - - # Script - script_sig = bytes.fromhex(inp['scriptsig']) if inp['scriptsig'] else b'' - result.extend(len(script_sig).to_bytes(1, 'little')) - result.extend(script_sig) - - # Sequence - result.extend(inp['sequence'].to_bytes(4, 'little')) - - # Number of outputs - result.extend(len(tx['vout']).to_bytes(1, 'little')) - - # Outputs - for out in tx['vout']: - # Amount in satoshis - result.extend(out['value'].to_bytes(8, 'little')) - - # Script - script_pubkey = bytes.fromhex(out['scriptpubkey']) - result.extend(len(script_pubkey).to_bytes(1, 'little')) - result.extend(script_pubkey) - - # Locktime - result.extend(tx['locktime'].to_bytes(4, 'little')) - - return bytes(result) + data_signed = serialize_transaction(self.transaction, self.input_index, int(hash_type), self.segwit) + return hashlib.sha256(data_signed).digest() def execute(self) -> bool: """Execute the script and return True if it executed successfully""" @@ -174,8 +78,9 @@ def execute(self) -> bool: i += self._execute_opcode(op_name) # Script executed successfully if stack is not empty and top value is true - if self.stack.is_empty(): + if self.stack.is_empty(): return False + return self.stack.pop() != b'\x00' except Exception as e: @@ -201,7 +106,7 @@ def _execute_opcode(self, op_name: str) -> int: if self.stack.is_empty(): self.if_stack.append(False) else: - value = self.stack.pop() + value = self.stack.pop() self.if_stack.append(value != b'\x00') return 1 elif op_name == 'OP_NOTIF': @@ -283,7 +188,7 @@ def _execute_opcode(self, op_name: str) -> int: if self.stack.size() < 2: raise InvalidScriptException("Stack too small for OP_EQUAL") a = self.stack.pop() - b = self.stack.pop() + b = self.stack.pop() self.stack.push(b'\x01' if a == b else b'\x00') return 1 @@ -301,8 +206,9 @@ def op_hash160(self) -> None: raise InvalidScriptException("Cannot HASH160 empty stack") value = self.stack.pop() sha256 = hashlib.sha256(value).digest() - ripemd160 = hashlib.new('ripemd160', sha256).digest() - self.stack.push(ripemd160) + ripemd160 = RIPEMD160.new() + ripemd160.update(sha256) + self.stack.push(ripemd160.digest()) def op_equalverify(self) -> None: """Verify top two stack items are equal""" @@ -320,97 +226,38 @@ def op_checksig(self) -> int: """ if self.stack.size() < 2: raise InvalidScriptException("Stack too small for CHECKSIG") - pubkey = self.stack.pop() signature = self.stack.pop() - try: # Extract DER signature and hash type if len(signature) < 1: raise InvalidScriptException("Empty signature") - - der_sig = signature[:-1] # Remove hash type byte - hash_type = signature[-1] - + der_sig, hash_type = signature[:-1], signature[-1] + # Create verifying key from public key bytes - try: - vk = ecdsa.VerifyingKey.from_string( - pubkey, - curve=ecdsa.SECP256k1, - hashfunc=hashlib.sha256 - ) - except Exception as e: - raise InvalidScriptException(f"Invalid public key: {str(e)}") - + vk = ecdsa.VerifyingKey.from_string( + pubkey, + curve=ecdsa.SECP256k1, + hashfunc=hashlib.sha256 + ) + # Create signature hash based on hash type sig_hash = self.create_signature_hash(hash_type) - + # Verify the signature - try: - verified = vk.verify(der_sig, sig_hash) - except Exception: - verified = False - + verified = vk.verify(der_sig, sig_hash, sigdecode=ecdsa.util.sigdecode_der) self.stack.push(b'\x01' if verified else b'\x00') return 1 - + except ecdsa.BadSignatureError: + self.stack.push(b'\x00') + return 1 except Exception as e: self.stack.push(b'\x00') + print(f"Unexpected exception: {e}") return 1 - def op_checkmultisig(self) -> int: - """ - Verify multiple signatures against multiple public keys - Returns number of bytes consumed - """ - if self.stack.size() < 1: - raise InvalidScriptException("Stack too small for CHECKMULTISIG") - - # Get number of public keys - n = int.from_bytes(self.stack.pop(), 'little') - if n < 0 or n > 20: - raise InvalidScriptException("Invalid number of public keys") - - if self.stack.size() < n + 1: - raise InvalidScriptException("Stack too small for public keys") - - # Get public keys - pubkeys = [] - for _ in range(n): - pubkeys.append(self.stack.pop()) - - # Get number of signatures - m = int.from_bytes(self.stack.pop(), 'little') - if m < 0 or m > n: - raise InvalidScriptException("Invalid number of signatures") - - if self.stack.size() < m: - raise InvalidScriptException("Stack too small for signatures") - - # Get signatures - signatures = [] - for _ in range(m): - signatures.append(self.stack.pop()) - - # Remove the extra null byte (Bitcoin protocol quirk) - if self.stack.size() < 1: - raise InvalidScriptException("No extra null byte for CHECKMULTISIG") - self.stack.pop() - - # TODO: Implement proper multisig verification - # This is a simplified version that always returns true - # In a real implementation, you would: - # 1. Verify each signature against public keys in order - # 2. Ensure all signatures are valid - # 3. Handle proper error cases - - verified = True # Replace with actual verification - - self.stack.push(b'\x01' if verified else b'\x00') - return 1 - @staticmethod - def combine_scripts(*scripts: Union[bytes, 'Script']) -> 'Script': + def combine_scripts(*scripts: Union[bytes, 'Script'], json_transaction: dict, segwit: bool = False) -> 'Script': """ Combine multiple scripts into a single script. Accepts both bytes and Script objects. @@ -423,5 +270,4 @@ def combine_scripts(*scripts: Union[bytes, 'Script']) -> 'Script': combined.extend(script) else: raise InvalidScriptException(f"Invalid script type: {type(script)}") - return Script(bytes(combined)) - \ No newline at end of file + return Script(bytes(combined), json_transaction, segwit=segwit) diff --git a/src/serialize.py b/src/serialize.py index db84e33..d3bac3c 100644 --- a/src/serialize.py +++ b/src/serialize.py @@ -82,6 +82,9 @@ def serialize_transaction(transaction, index=-1, sighash_type=1, segwit=False): # witness if segwit: for tx_in in inputs: + if "witness" not in tx_in: + break + out += [encode_varint(len(tx_in["witness"]))] for item in tx_in["witness"]: diff --git a/src/transaction.py b/src/transaction.py index cb905f1..1607c2a 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -1,19 +1,17 @@ import hashlib import json -from ecdsa import VerifyingKey, SECP256k1, BadSignatureError +from Crypto.Hash import RIPEMD160 -from src.script import Script, InvalidScriptException +from src.script import Script from src.serialize import serialize_transaction -from src.utils import decode_hex, get_filename_without_extension, hash160 -from src.verify import parse_der_signature_bytes, valid_transaction_syntax +from src.utils import decode_hex, get_filename_without_extension, double_spending +from src.verify import valid_transaction_syntax -def calculate_txid(transaction_content, coinbase=False): + +def calculate_txid(transaction_content, segwit=False): # Serialize the transaction content - if coinbase: - serialized_transaction = serialize_transaction(transaction_content, segwit=True) #json.dumps(transaction_content, sort_keys=True).encode() - else: - serialized_transaction = serialize_transaction(transaction_content) #json.dumps(transaction_content, sort_keys=True).encode() + serialized_transaction = serialize_transaction(transaction_content, segwit=segwit) # Calculate double SHA-256 hash hash_result = hashlib.sha256(hashlib.sha256(serialized_transaction).digest()).digest() @@ -37,13 +35,15 @@ def __init__(self, transaction_json_file): self.vin = json_transaction['vin'] self.vout = json_transaction['vout'] self.json_transaction = json_transaction + self.fee = 0 + self.has_witness = False else: # TODO jestli nejakej error print('Invalid transaction syntax') def is_valid(self): # At least one input and one output. - if not self.non_empty_vin_vout(): + if not self.non_empty_vin_vout(): return False # Basic locktime check. @@ -88,12 +88,20 @@ def check_input_output_sum(self): for output in self.vout: output_sum = output_sum + output['value'] + self.fee = input_sum - output_sum + # Output sum can't be greater than the input sum. if input_sum < output_sum: return False return True + def calculate_weight(self): + base_size = len(serialize_transaction(self.json_transaction)) + total_size = len(serialize_transaction(self.json_transaction, segwit=self.has_witness)) + + return int(base_size * 3 + total_size) + def valid_input(self, vin_idx, vin): if vin.get("is_coinbase", False): return False @@ -103,18 +111,16 @@ def valid_input(self, vin_idx, vin): if scriptpubkey_type == "p2pkh": return self.validate_p2pkh(vin_idx, vin) - elif scriptpubkey_type == "p2sh": + elif scriptpubkey_type == "p2sh": pass - #return self.validate_p2sh_p2wpkh(vin_idx, 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) + self.has_witness = True + return self.validate_p2wpkh(vin_idx, vin) + # Unknown script type. return False @@ -128,183 +134,132 @@ def validate_p2pkh(self, vin_idx, vin): # Pubkey script # ################# scriptsig = decode_hex(vin.get("scriptsig", "")) - if not scriptsig: return False - prevout = vin.get("prevout", {}) - if not prevout: return False - scriptpubkey = decode_hex(prevout.get("scriptpubkey", "")) # Combine and verify - script = Script.combine_scripts(scriptsig, scriptpubkey) + script = Script.combine_scripts(scriptsig, scriptpubkey, json_transaction=self.json_transaction) is_valid = script.execute() - 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] + #print(is_valid) - ####################### - # 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 + return is_valid - if der_len != signature_len: + def validate_p2sh(self, vin_idx, vin): + ################# + # Pubkey script # + ################# + scriptsig = decode_hex(vin.get("scriptsig", "")) + if not scriptsig: return False + prevout = vin.get("prevout", {}) + if not prevout: + return False + scriptpubkey = decode_hex(prevout.get("scriptpubkey", "")) - signature = r + s - - ###################### - # Parse scriptPubKey # - ###################### - # https://learnmeabitcoin.com/technical/script/p2pkh/ - # Explanation: the scriptPubKey contains: DUP, HASH160, public key hash (including OP_PUSHBYTES_20), EQUALVERIFY and CHECKSIG. + # Check if the scriptpubkey is a P2SH script + if scriptpubkey[:2] != b'\xa9\x14' or scriptpubkey[-1:] != b'\x87': + # Not a P2SH script, fallback to P2PKH validation + return self.validate_p2pkh(vin_idx, vin) - 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) + # Extract the script hash from the scriptpubkey + script_hash = scriptpubkey[2:-1] - 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) + # Find the redeem script in the scriptsig + redeem_script_len = int.from_bytes(scriptsig[0:1], byteorder='little') + redeem_script = scriptsig[1:1+redeem_script_len] - pkh = scriptpubkey[3:23] + # Create the combined script + script = Script.combine_scripts(redeem_script, json_transaction=self.json_transaction) - # 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 + # Hash the redeem script and compare with the script hash + # Compute the HASH160 (RIPEMD-160 of SHA-256) of the redeem script + sha256_hash = hashlib.sha256(redeem_script).digest() + ripemd160 = RIPEMD160.new() + ripemd160.update(sha256_hash) + computed_script_hash = ripemd160.digest() - ############################################ - # Verify the signature with the public key # - ############################################ + # Compare with the provided script hash + if computed_script_hash != script_hash: + return False - data_signed = serialize_transaction(self.json_transaction, vin_idx, int(hash_type)) - data_hash = hashlib.sha256(data_signed).digest() + # Execute the redeem script + is_valid = script.execute() - print(self.json_transaction) - print("********************************") + return is_valid - # 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 + def validate_p2wpkh(self, vin_idx, vin): + """ + Validate a Pay-to-Witness-Public-Key-Hash (P2WPKH) transaction input - return True""" - -""" - def validate_p2sh_p2wpkh(self, vin_idx, vin): - # Extract scriptSig and witness - scriptsig = decode_hex(vin.get("scriptsig", "")) + Args: + vin_idx (int): Index of the input being validated + vin (dict): Input data containing witness information + + Returns: + bool: True if the P2WPKH input is valid, False otherwise + """ + # Check for witness data (P2WPKH requires exactly 2 items: signature and pubkey) witness = vin.get("witness", []) - - if not scriptsig or len(witness) < 2: + if len(witness) != 2: return False - print(vin["txid"]) + # Extract the signature and public key from the witness data + try: + signature = bytes.fromhex(witness[0]) + pubkey = bytes.fromhex(witness[1]) + except ValueError: + # Handle invalid hex data + return False + + # Check the length of the public key (33 bytes for compressed, 65 for uncompressed) + if len(pubkey) not in {33, 65}: + return False + # Extract the previous output's scriptPubKey prevout = vin.get("prevout", {}) - if not prevout: return False + + scriptpubkey = bytes.fromhex(prevout.get("scriptpubkey", "")) - 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 + # Verify that the scriptPubKey matches the P2WPKH format (OP_0 <20-byte-key-hash>) + if len(scriptpubkey) != 22 or scriptpubkey[0] != 0x00 or scriptpubkey[1] != 0x14: 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) + # Create the equivalent P2PKH scriptPubKey from the witness data + p2pkh_script = ( + bytes([len(signature)]) + # Push the signature + signature + + bytes([len(pubkey)]) + # Push the public key + pubkey + + bytes([ + 0x76, # OP_DUP + 0xa9, # OP_HASH160 + 0x14 # Push 20 bytes (20-byte hash follows) + ]) + + scriptpubkey[2:22] + # Extract 20-byte public key hash from scriptPubKey + bytes([ + 0x88, # OP_EQUALVERIFY + 0xac # OP_CHECKSIG + ]) + ) + + # Create a script object with the generated P2PKH script and transaction context + script = Script( + p2pkh_script, + json_transaction=self.json_transaction, + input_index=vin_idx, + segwit=True + ) + + # Execute the script and catch any errors during the process try: - verifying_key.verify(signature[:-1], data_hash, hashlib.sha256) # Remove the last byte (hash type) - except BadSignatureError: + return script.execute() + except Exception as e: + print(f"P2WPKH validation error: {str(e)}") return False - - return True """ \ No newline at end of file diff --git a/src/utils.py b/src/utils.py index 32ad658..702c03e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -22,3 +22,12 @@ def hash160(data): ripemd160.update(sha256_hash) return ripemd160.digest() + +def double_spending(valid_transactions, checked_transaction): + for tx in valid_transactions: + for vin in tx["vin"]: + for vin_checked in checked_transaction["vin"]: + if vin["txid"] == vin_checked["txid"] and vin["vout"] == vin_checked["vout"]: + return True + + return False diff --git a/src/verify.py b/src/verify.py index 7844b78..ad89e54 100644 --- a/src/verify.py +++ b/src/verify.py @@ -43,15 +43,3 @@ def valid_transaction_syntax(json_transaction): return False return True - - -def parse_der_signature_bytes(der_signature): - # Parse the DER signature - r_length = der_signature[3] - r = der_signature[4:4 + r_length] - s_length_index = 4 + r_length + 1 - s_length = der_signature[s_length_index] - s = der_signature[s_length_index + 1:s_length_index + 1 + s_length] - hash_type = der_signature[-1] - - return r, s, hash_type