From 4ebef602f0f0fccc9b9cc50f676ce5acab5be353 Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Tue, 5 Nov 2024 22:55:41 +0100 Subject: [PATCH 01/14] Fix ecdsa error --- requirements.txt | 1 - run.sh | 6 +++++- src/script.py | 6 ++++-- src/transaction.py | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) 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/script.py b/src/script.py index 471a34e..f2304a0 100644 --- a/src/script.py +++ b/src/script.py @@ -3,6 +3,7 @@ import hashlib import ecdsa from src.op_codes import OP_CODES +from Crypto.Hash import RIPEMD160 class InvalidScriptException(Exception): """Custom exception for Script execution errors""" @@ -301,8 +302,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""" diff --git a/src/transaction.py b/src/transaction.py index cb905f1..616d1e2 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -1,7 +1,7 @@ import hashlib import json -from ecdsa import VerifyingKey, SECP256k1, BadSignatureError +#from ecdsa import VerifyingKey, SECP256k1, BadSignatureError from src.script import Script, InvalidScriptException from src.serialize import serialize_transaction @@ -307,4 +307,4 @@ def validate_p2sh_p2wpkh(self, vin_idx, vin): except BadSignatureError: return False - return True """ \ No newline at end of file + return True """ From 8c8053765242965737d82995a5be88c28c89f0cd Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Wed, 6 Nov 2024 00:02:27 +0100 Subject: [PATCH 02/14] Fix checksig --- src/script.py | 50 +++++++++++++++++++++++++++++++++------------- src/transaction.py | 24 ++++++++++++---------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/script.py b/src/script.py index f2304a0..8c34db3 100644 --- a/src/script.py +++ b/src/script.py @@ -1,9 +1,11 @@ from dataclasses import dataclass from typing import List, Any, Union +from Crypto.Hash import RIPEMD160 import hashlib import ecdsa from src.op_codes import OP_CODES -from Crypto.Hash import RIPEMD160 +from src.verify import parse_der_signature_bytes +from src.serialize import serialize_transaction class InvalidScriptException(Exception): """Custom exception for Script execution errors""" @@ -42,10 +44,12 @@ def __init__(self, script: bytes, json_transaction: dict = None, input_index: in self.input_index = input_index def create_signature_hash(self, hash_type: int) -> bytes: + data_signed = serialize_transaction(self.transaction, self.input_index, int(hash_type)) + return hashlib.sha256(data_signed).digest() """ 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") @@ -97,7 +101,7 @@ def create_signature_hash(self, hash_type: int) -> bytes: # 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() @@ -177,6 +181,7 @@ def execute(self) -> bool: # Script executed successfully if stack is not empty and top value is true if self.stack.is_empty(): return False + return self.stack.pop() != b'\x00' except Exception as e: @@ -186,7 +191,7 @@ def _execute_opcode(self, op_name: str) -> int: """Execute a single opcode and return how many bytes to advance""" # Constants - if op_name == 'OP_0': + if op_name == 'OP_0': self.stack.push(b'\x00') return 1 elif op_name == 'OP_1NEGATE': @@ -202,7 +207,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': @@ -284,7 +289,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 @@ -328,12 +333,26 @@ def op_checksig(self) -> int: try: # Extract DER signature and hash type - if len(signature) < 1: + if len(signature) < 1: raise InvalidScriptException("Empty signature") - der_sig = signature[:-1] # Remove hash type byte - hash_type = signature[-1] + #der_sig = signature[:-1] # Remove hash type byte + #hash_type = signature[-1] + + der_sig = signature[:-1] + r, s, hash_type = parse_der_signature_bytes(der_sig) + + der_len = len(der_sig) + signature_len = len(r + s) + 6 + + if der_len != signature_len: + self.stack.push(b'\x00') + return 1 + sig = r + s + + #print(pubkey) + # Create verifying key from public key bytes try: vk = ecdsa.VerifyingKey.from_string( @@ -341,7 +360,7 @@ def op_checksig(self) -> int: curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256 ) - except Exception as e: + except Exception as e: raise InvalidScriptException(f"Invalid public key: {str(e)}") # Create signature hash based on hash type @@ -349,11 +368,14 @@ def op_checksig(self) -> int: # Verify the signature try: - verified = vk.verify(der_sig, sig_hash) + verified = vk.verify(sig, sig_hash) except Exception: verified = False - + self.stack.push(b'\x01' if verified else b'\x00') + + #print(verified) + return 1 except Exception as e: @@ -412,7 +434,7 @@ def op_checkmultisig(self) -> int: return 1 @staticmethod - def combine_scripts(*scripts: Union[bytes, 'Script']) -> 'Script': + def combine_scripts(*scripts: Union[bytes, 'Script'], json_transaction: dict) -> 'Script': """ Combine multiple scripts into a single script. Accepts both bytes and Script objects. @@ -425,5 +447,5 @@ def combine_scripts(*scripts: Union[bytes, 'Script']) -> 'Script': combined.extend(script) else: raise InvalidScriptException(f"Invalid script type: {type(script)}") - return Script(bytes(combined)) + return Script(bytes(combined), json_transaction) \ No newline at end of file diff --git a/src/transaction.py b/src/transaction.py index 616d1e2..ce0ba23 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -1,7 +1,7 @@ import hashlib import json -#from ecdsa import VerifyingKey, SECP256k1, BadSignatureError +from ecdsa import VerifyingKey, SECP256k1, BadSignatureError from src.script import Script, InvalidScriptException from src.serialize import serialize_transaction @@ -128,20 +128,19 @@ 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() + + #print(is_valid) + return is_valid """ ##################################################################### @@ -179,7 +178,8 @@ def validate_p2pkh(self, vin_idx, vin): return False signature = r + s - + #print(signature) + ###################### # Parse scriptPubKey # ###################### @@ -206,8 +206,8 @@ def validate_p2pkh(self, vin_idx, vin): data_signed = serialize_transaction(self.json_transaction, vin_idx, int(hash_type)) data_hash = hashlib.sha256(data_signed).digest() - print(self.json_transaction) - print("********************************") + #print(self.json_transaction) + #print("********************************") # Verify the signature verifying_key = VerifyingKey.from_string(public_key, curve=SECP256k1) @@ -216,8 +216,10 @@ def validate_p2pkh(self, vin_idx, vin): except BadSignatureError: return False - return True""" - + print(public_key) + print("-------------------") + return True +""" """ def validate_p2sh_p2wpkh(self, vin_idx, vin): # Extract scriptSig and witness From a1f079241ecff5e8ccf7c1e4d35f565fe3e67feb Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Wed, 6 Nov 2024 22:50:47 +0100 Subject: [PATCH 03/14] Update coinbase --- src/coinbase_transaction.py | 71 ++++++++------- src/transaction.py | 175 +----------------------------------- 2 files changed, 43 insertions(+), 203 deletions(-) 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/transaction.py b/src/transaction.py index ce0ba23..45d13ba 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -105,16 +105,12 @@ def valid_input(self, vin_idx, vin): return self.validate_p2pkh(vin_idx, vin) 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) + return self.validate_p2wpkh(vin) # Unknown script type. return False @@ -142,171 +138,6 @@ 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 - scriptsig = decode_hex(vin.get("scriptsig", "")) - witness = vin.get("witness", []) - - if not scriptsig or len(witness) < 2: - 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 - return True """ + def validate_p2wpkh(self, vin): + return False From d6f45c351c875cc6c8b07016caf9953645b307d5 Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Wed, 6 Nov 2024 23:05:47 +0100 Subject: [PATCH 04/14] Add p2wpkh --- src/script.py | 9 +++--- src/transaction.py | 69 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/script.py b/src/script.py index 8c34db3..b62cc79 100644 --- a/src/script.py +++ b/src/script.py @@ -35,16 +35,17 @@ 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: - data_signed = serialize_transaction(self.transaction, self.input_index, int(hash_type)) + data_signed = serialize_transaction(self.transaction, self.input_index, int(hash_type), self.segwit) return hashlib.sha256(data_signed).digest() """ Create the signature hash for the transaction based on the hash type. @@ -179,7 +180,7 @@ 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' @@ -191,7 +192,7 @@ def _execute_opcode(self, op_name: str) -> int: """Execute a single opcode and return how many bytes to advance""" # Constants - if op_name == 'OP_0': + if op_name == 'OP_0': self.stack.push(b'\x00') return 1 elif op_name == 'OP_1NEGATE': diff --git a/src/transaction.py b/src/transaction.py index 45d13ba..661fedd 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -110,7 +110,7 @@ def valid_input(self, vin_idx, vin): elif scriptpubkey_type == "v1_p2tr": pass elif scriptpubkey_type == "v0_p2wpkh": - return self.validate_p2wpkh(vin) + return self.validate_p2wpkh(vin_idx, vin) # Unknown script type. return False @@ -139,5 +139,68 @@ def validate_p2pkh(self, vin_idx, vin): return is_valid - def validate_p2wpkh(self, vin): - return False + def validate_p2wpkh(self, vin_idx, vin): + """ + Validate a Pay-to-Witness-Public-Key-Hash (P2WPKH) transaction input + + 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 + witness = vin.get("witness", []) + if len(witness) != 2: # P2WPKH requires exactly 2 witness items (signature and pubkey) + return False + + # Get witness components + signature = bytes.fromhex(witness[0]) + pubkey = bytes.fromhex(witness[1]) + + # Get previous output's scriptPubKey + prevout = vin.get("prevout", {}) + if not prevout: + return False + + scriptpubkey = bytes.fromhex(prevout.get("scriptpubkey", "")) + + # Verify the script is proper P2WPKH format (OP_0 <20-byte-key-hash>) + if len(scriptpubkey) != 22 or scriptpubkey[0] != 0x00 or scriptpubkey[1] != 0x14: + return False + + # Create P2PKH-style script from witness data + # P2WPKH witness program is executed as if it were P2PKH scriptPubKey + p2pkh_script = ( + bytes([len(signature)]) + # Push signature + signature + + bytes([len(pubkey)]) + # Push pubkey + pubkey + + # Standard P2PKH script operations + bytes([ + 0x76, # OP_DUP + 0xa9, # OP_HASH160 + 0x14 # Push 20 bytes + ]) + + scriptpubkey[2:22] + # 20-byte pubkey hash from witness program + bytes([ + 0x88, # OP_EQUALVERIFY + 0xac # OP_CHECKSIG + ]) + ) + + # Create script object with witness-specific transaction data + script = Script( + p2pkh_script, + json_transaction=self.json_transaction, + input_index=vin_idx, + segwit=True + ) + + # Execute the script + try: + return script.execute() + except Exception as e: + print(f"P2WPKH validation error: {str(e)}") + return False From 8e1b4e0fa1a6a1151194d8c1a09216edf0880bbd Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Thu, 7 Nov 2024 16:18:38 +0100 Subject: [PATCH 05/14] Add p2sh --- src/main.py | 11 +++++----- src/script.py | 6 +++--- src/transaction.py | 50 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/main.py b/src/main.py index 060a210..3cbd532 100644 --- a/src/main.py +++ b/src/main.py @@ -29,15 +29,16 @@ 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:]] + transaction_hashes = [calculate_txid(COINBASE_TRANSACTION)] + [calculate_txid(json_transaction) for json_transaction in mempool.valid_transactions] block_hash = block_mining(transaction_hashes).hex() - wtxids = ["0000000000000000000000000000000000000000000000000000000000000000"] + transaction_hashes[1:] + #wtxids = ["0000000000000000000000000000000000000000000000000000000000000000"] + transaction_hashes[1:] + #wtxids = ["0000000000000000000000000000000000000000000000000000000000000000"] + transaction_hashes + wtxids = transaction_hashes + + witness_commitment = calculate_witness_commitment(wtxids) - witness_commitment = calculate_witness_commitment(wtxids) scriptpubkey_wc = '6a24aa21a9ed' + witness_commitment COINBASE_TRANSACTION["vout"][1]["scriptpubkey"] = scriptpubkey_wc diff --git a/src/script.py b/src/script.py index b62cc79..4b6de34 100644 --- a/src/script.py +++ b/src/script.py @@ -430,12 +430,12 @@ def op_checkmultisig(self) -> int: # 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'], json_transaction: dict) -> '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. @@ -448,5 +448,5 @@ def combine_scripts(*scripts: Union[bytes, 'Script'], json_transaction: dict) -> combined.extend(script) else: raise InvalidScriptException(f"Invalid script type: {type(script)}") - return Script(bytes(combined), json_transaction) + return Script(bytes(combined), json_transaction, segwit=segwit) \ No newline at end of file diff --git a/src/transaction.py b/src/transaction.py index 661fedd..4f24bbf 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -2,12 +2,14 @@ import json from ecdsa import VerifyingKey, SECP256k1, BadSignatureError +from Crypto.Hash import RIPEMD160 from src.script import Script, InvalidScriptException 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 + def calculate_txid(transaction_content, coinbase=False): # Serialize the transaction content if coinbase: @@ -104,13 +106,15 @@ def valid_input(self, vin_idx, vin): if scriptpubkey_type == "p2pkh": return self.validate_p2pkh(vin_idx, vin) elif scriptpubkey_type == "p2sh": + #return self.validate_p2sh(vin_idx, vin) pass elif scriptpubkey_type == "v0_p2wsh": pass elif scriptpubkey_type == "v1_p2tr": pass elif scriptpubkey_type == "v0_p2wpkh": - return self.validate_p2wpkh(vin_idx, vin) + #return self.validate_p2wpkh(vin_idx, vin) + pass # Unknown script type. return False @@ -139,6 +143,49 @@ def validate_p2pkh(self, vin_idx, vin): return is_valid + 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", "")) + + # 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) + + # Extract the script hash from the scriptpubkey + script_hash = scriptpubkey[2:-1] + + # 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] + + # Create the combined script + script = Script.combine_scripts(redeem_script, json_transaction=self.json_transaction) + + # 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() + + # Compare with the provided script hash + if computed_script_hash != script_hash: + return False + + # Execute the redeem script + is_valid = script.execute() + + return is_valid + def validate_p2wpkh(self, vin_idx, vin): """ Validate a Pay-to-Witness-Public-Key-Hash (P2WPKH) transaction input @@ -200,6 +247,7 @@ def validate_p2wpkh(self, vin_idx, vin): # Execute the script try: + #return script.execute() return script.execute() except Exception as e: print(f"P2WPKH validation error: {str(e)}") From a94f8d2deaefd3068ad20291f720a74ea3cb5cf6 Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Fri, 8 Nov 2024 10:53:13 +0100 Subject: [PATCH 06/14] Get back witness calculation --- src/main.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main.py b/src/main.py index 3cbd532..3ed909f 100644 --- a/src/main.py +++ b/src/main.py @@ -30,15 +30,14 @@ def parse_arguments(): # TODO pokracovani - transaction_hashes = [calculate_txid(COINBASE_TRANSACTION)] + [calculate_txid(json_transaction) for json_transaction in mempool.valid_transactions] + block_transactions = [COINBASE_TRANSACTION] + mempool.valid_transactions + + transaction_hashes = [calculate_txid(COINBASE_TRANSACTION)] + [calculate_txid(json_transaction) for json_transaction in block_transactions[1:]] block_hash = block_mining(transaction_hashes).hex() - #wtxids = ["0000000000000000000000000000000000000000000000000000000000000000"] + transaction_hashes[1:] - #wtxids = ["0000000000000000000000000000000000000000000000000000000000000000"] + transaction_hashes - wtxids = transaction_hashes - - witness_commitment = calculate_witness_commitment(wtxids) + wtxids = ["0000000000000000000000000000000000000000000000000000000000000000"] + transaction_hashes[1:] + witness_commitment = calculate_witness_commitment(wtxids) scriptpubkey_wc = '6a24aa21a9ed' + witness_commitment COINBASE_TRANSACTION["vout"][1]["scriptpubkey"] = scriptpubkey_wc From 6e43f6ee695d768e0a571bedfdbe9c8304afbffe Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Fri, 8 Nov 2024 11:31:23 +0100 Subject: [PATCH 07/14] Add fee calculation --- src/main.py | 28 +++++++++++++++++++++++++--- src/mempool.py | 2 +- src/serialize.py | 3 +++ src/transaction.py | 27 ++++++++++++++++++--------- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/main.py b/src/main.py index 3ed909f..fa6ed5e 100644 --- a/src/main.py +++ b/src/main.py @@ -30,9 +30,31 @@ def parse_arguments(): # 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:]] + + #block_transactions = [COINBASE_TRANSACTION] + mempool.valid_transactions + + # Initialize an empty list for transactions + block_transactions = [COINBASE_TRANSACTION] + + # Initialize total weight and total fees + total_weight = 0 + total_fees = 0 + + # Set the maximum block weight + 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[1:]] 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/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 4f24bbf..28b2761 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -10,12 +10,9 @@ from src.verify import parse_der_signature_bytes, 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() @@ -39,6 +36,8 @@ 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') @@ -90,12 +89,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 @@ -105,16 +112,18 @@ def valid_input(self, vin_idx, vin): if scriptpubkey_type == "p2pkh": return self.validate_p2pkh(vin_idx, vin) - elif scriptpubkey_type == "p2sh": - #return self.validate_p2sh(vin_idx, vin) - pass + elif scriptpubkey_type == "p2sh": + return self.validate_p2sh(vin_idx, vin) elif scriptpubkey_type == "v0_p2wsh": + self.has_witness = True pass elif scriptpubkey_type == "v1_p2tr": pass elif scriptpubkey_type == "v0_p2wpkh": - #return self.validate_p2wpkh(vin_idx, vin) pass + #self.has_witness = True + #return self.validate_p2wpkh(vin_idx, vin) + # Unknown script type. return False From fbd804fddbcca33aefec3765e3795c4ce201b1ed Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Fri, 8 Nov 2024 14:11:35 +0100 Subject: [PATCH 08/14] P2PKH only --- src/transaction.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/transaction.py b/src/transaction.py index 28b2761..713b04e 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -113,9 +113,10 @@ def valid_input(self, vin_idx, vin): if scriptpubkey_type == "p2pkh": return self.validate_p2pkh(vin_idx, vin) elif scriptpubkey_type == "p2sh": - return self.validate_p2sh(vin_idx, vin) + #return self.validate_p2sh(vin_idx, vin) + pass elif scriptpubkey_type == "v0_p2wsh": - self.has_witness = True + #self.has_witness = True pass elif scriptpubkey_type == "v1_p2tr": pass From 40aa7222613430d9da654e816d0c0dd437d8d7ce Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Fri, 8 Nov 2024 14:56:44 +0100 Subject: [PATCH 09/14] Replace DER Encoding --- src/script.py | 53 ++++++++++++---------------------------------- src/transaction.py | 50 ++++++++++++++++++++++++------------------- src/verify.py | 30 ++++++++++++++++++++------ 3 files changed, 66 insertions(+), 67 deletions(-) diff --git a/src/script.py b/src/script.py index 4b6de34..9422b4d 100644 --- a/src/script.py +++ b/src/script.py @@ -328,59 +328,34 @@ 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: + if len(signature) < 1: raise InvalidScriptException("Empty signature") - - #der_sig = signature[:-1] # Remove hash type byte - #hash_type = signature[-1] - - der_sig = signature[:-1] - r, s, hash_type = parse_der_signature_bytes(der_sig) - - der_len = len(der_sig) - signature_len = len(r + s) + 6 - - if der_len != signature_len: - self.stack.push(b'\x00') - return 1 - - sig = r + s - - #print(pubkey) + 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(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') - - #print(verified) - 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: diff --git a/src/transaction.py b/src/transaction.py index 713b04e..874612a 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -112,6 +112,7 @@ def valid_input(self, vin_idx, vin): if scriptpubkey_type == "p2pkh": return self.validate_p2pkh(vin_idx, vin) + #pass elif scriptpubkey_type == "p2sh": #return self.validate_p2sh(vin_idx, vin) pass @@ -121,9 +122,9 @@ def valid_input(self, vin_idx, vin): elif scriptpubkey_type == "v1_p2tr": pass elif scriptpubkey_type == "v0_p2wpkh": - pass - #self.has_witness = True - #return self.validate_p2wpkh(vin_idx, vin) + #pass + self.has_witness = True + return self.validate_p2wpkh(vin_idx, vin) # Unknown script type. @@ -207,47 +208,53 @@ def validate_p2wpkh(self, vin_idx, vin): Returns: bool: True if the P2WPKH input is valid, False otherwise """ - # Check for witness data + # Check for witness data (P2WPKH requires exactly 2 items: signature and pubkey) witness = vin.get("witness", []) - if len(witness) != 2: # P2WPKH requires exactly 2 witness items (signature and pubkey) + if len(witness) != 2: + return False + + # 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 - - # Get witness components - signature = bytes.fromhex(witness[0]) - pubkey = bytes.fromhex(witness[1]) - # Get previous output's scriptPubKey + # 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", "")) - # Verify the script is proper P2WPKH format (OP_0 <20-byte-key-hash>) + # 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 - # Create P2PKH-style script from witness data - # P2WPKH witness program is executed as if it were P2PKH scriptPubKey + # Create the equivalent P2PKH scriptPubKey from the witness data p2pkh_script = ( - bytes([len(signature)]) + # Push signature + bytes([len(signature)]) + # Push the signature signature + - bytes([len(pubkey)]) + # Push pubkey + bytes([len(pubkey)]) + # Push the public key pubkey + - # Standard P2PKH script operations bytes([ 0x76, # OP_DUP 0xa9, # OP_HASH160 - 0x14 # Push 20 bytes + 0x14 # Push 20 bytes (20-byte hash follows) ]) + - scriptpubkey[2:22] + # 20-byte pubkey hash from witness program + scriptpubkey[2:22] + # Extract 20-byte public key hash from scriptPubKey bytes([ 0x88, # OP_EQUALVERIFY 0xac # OP_CHECKSIG ]) ) - # Create script object with witness-specific transaction data + # Create a script object with the generated P2PKH script and transaction context script = Script( p2pkh_script, json_transaction=self.json_transaction, @@ -255,9 +262,8 @@ def validate_p2wpkh(self, vin_idx, vin): segwit=True ) - # Execute the script + # Execute the script and catch any errors during the process try: - #return script.execute() return script.execute() except Exception as e: print(f"P2WPKH validation error: {str(e)}") diff --git a/src/verify.py b/src/verify.py index 7844b78..916149b 100644 --- a/src/verify.py +++ b/src/verify.py @@ -46,12 +46,30 @@ def valid_transaction_syntax(json_transaction): def parse_der_signature_bytes(der_signature): - # Parse the DER signature + # Parse the DER signature + if der_signature[0] != 0x30: + raise ValueError("Invalid DER signature format") + + length = der_signature[1] + if length + 2 != len(der_signature): + raise ValueError("Invalid DER signature length") + + if der_signature[2] != 0x02: + raise ValueError("Invalid DER signature format") + 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] - + + if der_signature[4 + r_length] != 0x02: + raise ValueError("Invalid DER signature format") + + s_length = der_signature[5 + r_length] + s = der_signature[6 + r_length:6 + r_length + s_length] + + # Determine the hash type + if len(der_signature) > 6 + r_length + s_length: + hash_type = der_signature[-1] + else: + hash_type = 0x01 # Default to SIGHASH_ALL + return r, s, hash_type From 0497eb8f375e795c81f233cadd81cc0d41a6042b Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Fri, 8 Nov 2024 14:58:45 +0100 Subject: [PATCH 10/14] Try add p2sh --- src/transaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transaction.py b/src/transaction.py index 874612a..3278dbb 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -114,8 +114,8 @@ def valid_input(self, vin_idx, vin): return self.validate_p2pkh(vin_idx, vin) #pass elif scriptpubkey_type == "p2sh": - #return self.validate_p2sh(vin_idx, vin) - pass + return self.validate_p2sh(vin_idx, vin) + #pass elif scriptpubkey_type == "v0_p2wsh": #self.has_witness = True pass From 0e0b23d28c7996cba6aa395df729d6182f3a61b8 Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Fri, 8 Nov 2024 15:20:38 +0100 Subject: [PATCH 11/14] Add p2wpkh --- src/script.py | 1 - src/transaction.py | 8 ++------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/script.py b/src/script.py index 9422b4d..d468196 100644 --- a/src/script.py +++ b/src/script.py @@ -4,7 +4,6 @@ import hashlib import ecdsa from src.op_codes import OP_CODES -from src.verify import parse_der_signature_bytes from src.serialize import serialize_transaction class InvalidScriptException(Exception): diff --git a/src/transaction.py b/src/transaction.py index 3278dbb..c75b49d 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -112,17 +112,13 @@ def valid_input(self, vin_idx, vin): if scriptpubkey_type == "p2pkh": return self.validate_p2pkh(vin_idx, vin) - #pass elif scriptpubkey_type == "p2sh": - return self.validate_p2sh(vin_idx, vin) - #pass + pass elif scriptpubkey_type == "v0_p2wsh": - #self.has_witness = True pass elif scriptpubkey_type == "v1_p2tr": pass elif scriptpubkey_type == "v0_p2wpkh": - #pass self.has_witness = True return self.validate_p2wpkh(vin_idx, vin) @@ -231,7 +227,7 @@ def validate_p2wpkh(self, vin_idx, vin): return False scriptpubkey = bytes.fromhex(prevout.get("scriptpubkey", "")) - + # 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 From df0439bc23f51eb9c7696e880d40a6df4e17fac6 Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Fri, 8 Nov 2024 15:42:12 +0100 Subject: [PATCH 12/14] Add final version --- src/script.py | 108 +-------------------------------------------- src/transaction.py | 2 +- 2 files changed, 2 insertions(+), 108 deletions(-) diff --git a/src/script.py b/src/script.py index d468196..f3790b8 100644 --- a/src/script.py +++ b/src/script.py @@ -46,62 +46,7 @@ def __init__(self, script: bytes, json_transaction: dict = None, input_index: in def create_signature_hash(self, hash_type: int) -> bytes: data_signed = serialize_transaction(self.transaction, self.input_index, int(hash_type), self.segwit) return hashlib.sha256(data_signed).digest() - """ - 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() @@ -357,57 +302,6 @@ def op_checksig(self) -> int: 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'], json_transaction: dict, segwit: bool = False) -> 'Script': """ diff --git a/src/transaction.py b/src/transaction.py index c75b49d..e432cde 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -44,7 +44,7 @@ def __init__(self, transaction_json_file): 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. From 3e3d01fff89126d8b06f4ff3f471b22d632934c6 Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Fri, 8 Nov 2024 15:56:42 +0100 Subject: [PATCH 13/14] Removed the unused functions and methods --- src/main.py | 13 ++----------- src/script.py | 46 ---------------------------------------------- src/transaction.py | 5 ++--- src/verify.py | 30 ------------------------------ 4 files changed, 4 insertions(+), 90 deletions(-) diff --git a/src/main.py b/src/main.py index fa6ed5e..d4fda4b 100644 --- a/src/main.py +++ b/src/main.py @@ -28,19 +28,10 @@ def parse_arguments(): mempool = MemPool(args.mempool) - # TODO pokracovani - + block_transactions = [] - #block_transactions = [COINBASE_TRANSACTION] + mempool.valid_transactions - - # Initialize an empty list for transactions - block_transactions = [COINBASE_TRANSACTION] - - # Initialize total weight and total fees total_weight = 0 total_fees = 0 - - # Set the maximum block weight max_block_weight = 4000000 # Sort the transactions by the fee in descending order @@ -54,7 +45,7 @@ def parse_arguments(): 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[1:]] + 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/script.py b/src/script.py index f3790b8..c0725b4 100644 --- a/src/script.py +++ b/src/script.py @@ -46,51 +46,6 @@ def __init__(self, script: bytes, json_transaction: dict = None, input_index: in def create_signature_hash(self, hash_type: int) -> bytes: data_signed = serialize_transaction(self.transaction, self.input_index, int(hash_type), self.segwit) return hashlib.sha256(data_signed).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) def execute(self) -> bool: """Execute the script and return True if it executed successfully""" @@ -317,4 +272,3 @@ def combine_scripts(*scripts: Union[bytes, 'Script'], json_transaction: dict, se else: raise InvalidScriptException(f"Invalid script type: {type(script)}") return Script(bytes(combined), json_transaction, segwit=segwit) - \ No newline at end of file diff --git a/src/transaction.py b/src/transaction.py index e432cde..42dd820 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -1,13 +1,12 @@ 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.verify import valid_transaction_syntax def calculate_txid(transaction_content, segwit=False): diff --git a/src/verify.py b/src/verify.py index 916149b..ad89e54 100644 --- a/src/verify.py +++ b/src/verify.py @@ -43,33 +43,3 @@ def valid_transaction_syntax(json_transaction): return False return True - - -def parse_der_signature_bytes(der_signature): - # Parse the DER signature - if der_signature[0] != 0x30: - raise ValueError("Invalid DER signature format") - - length = der_signature[1] - if length + 2 != len(der_signature): - raise ValueError("Invalid DER signature length") - - if der_signature[2] != 0x02: - raise ValueError("Invalid DER signature format") - - r_length = der_signature[3] - r = der_signature[4:4 + r_length] - - if der_signature[4 + r_length] != 0x02: - raise ValueError("Invalid DER signature format") - - s_length = der_signature[5 + r_length] - s = der_signature[6 + r_length:6 + r_length + s_length] - - # Determine the hash type - if len(der_signature) > 6 + r_length + s_length: - hash_type = der_signature[-1] - else: - hash_type = 0x01 # Default to SIGHASH_ALL - - return r, s, hash_type From e1ff1fc4576d5079f209950b1b65c5760dd3a9e7 Mon Sep 17 00:00:00 2001 From: davidchocholaty Date: Sat, 9 Nov 2024 15:02:49 +0100 Subject: [PATCH 14/14] Add double spending check --- src/main.py | 7 +++++++ src/script.py | 3 +-- src/transaction.py | 2 +- src/utils.py | 9 +++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main.py b/src/main.py index d4fda4b..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,6 +29,12 @@ def parse_arguments(): mempool = MemPool(args.mempool) + # 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)] + + mempool.valid_transactions = non_double_spend + block_transactions = [] total_weight = 0 diff --git a/src/script.py b/src/script.py index c0725b4..fea6aa9 100644 --- a/src/script.py +++ b/src/script.py @@ -1,5 +1,4 @@ -from dataclasses import dataclass -from typing import List, Any, Union +from typing import List, Union from Crypto.Hash import RIPEMD160 import hashlib import ecdsa diff --git a/src/transaction.py b/src/transaction.py index 42dd820..1607c2a 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -5,7 +5,7 @@ from src.script import Script from src.serialize import serialize_transaction -from src.utils import decode_hex, get_filename_without_extension, hash160 +from src.utils import decode_hex, get_filename_without_extension, double_spending from src.verify import valid_transaction_syntax 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