diff --git a/src/coinbase_transaction.py b/src/coinbase_transaction.py index af2a18c..3c6e0d8 100644 --- a/src/coinbase_transaction.py +++ b/src/coinbase_transaction.py @@ -1,31 +1,34 @@ -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", +BLOCKREWARD = 1250000000 + +def create_coinbase(total_fee): + return { + "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": BLOCKREWARD + total_fee + }, + { + "scriptpubkey": "", + "scriptpubkey_type": "op_return", + "value": 0 + } ] } - ], - "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 diff --git a/src/main.py b/src/main.py index 060a210..dc19915 100644 --- a/src/main.py +++ b/src/main.py @@ -8,11 +8,12 @@ if project_root_directory not in sys.path: sys.path.append(project_root_directory) -from src.coinbase_transaction import COINBASE_TRANSACTION +from src.coinbase_transaction import create_coinbase from src.mempool import MemPool 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 total_fee def parse_arguments(): parser = argparse.ArgumentParser(description='Simulation of the mining process of a block') @@ -30,7 +31,8 @@ def parse_arguments(): # TODO pokracovani - block_transactions = [COINBASE_TRANSACTION] + mempool.valid_transactions + COINBASE_TRANSACTION = create_coinbase(total_fee(mempool.valid_transactions)) + block_transactions = [COINBASE_TRANSACTION] + mempool.valid_transactions_json transaction_hashes = [calculate_txid(COINBASE_TRANSACTION)] + [calculate_txid(json_transaction) for json_transaction in block_transactions[1:]] block_hash = block_mining(transaction_hashes).hex() diff --git a/src/mempool.py b/src/mempool.py index 1231f62..a999d4d 100644 --- a/src/mempool.py +++ b/src/mempool.py @@ -7,4 +7,5 @@ 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()] + self.valid_transactions_json = [transaction.json_transaction for transaction in self.valid_transactions] diff --git a/src/op_codes.py b/src/op_codes.py index 46570cc..7c1c8b6 100644 --- a/src/op_codes.py +++ b/src/op_codes.py @@ -1,85 +1,7 @@ # The following code is taken from: https://github.com/SummerOfBitcoin/code-challenge-2024-Nesopie/blob/840aeb58dd68cba3fba33f96d71ea0790c1840a8/src/features/script/op_codes.ts OP_CODES = { - """ 'OP_0': b'\x00', - 'OP_PUSHBYTES_1': b'\x01', - 'OP_PUSHBYTES_2': b'\x02', - 'OP_PUSHBYTES_3': b'\x03', - 'OP_PUSHBYTES_4': b'\x04', - 'OP_PUSHBYTES_5': b'\x05', - 'OP_PUSHBYTES_6': b'\x06', - 'OP_PUSHBYTES_7': b'\x07', - 'OP_PUSHBYTES_8': b'\x08', - 'OP_PUSHBYTES_9': b'\x09', - 'OP_PUSHBYTES_10': b'\x0a', - 'OP_PUSHBYTES_11': b'\x0b', - 'OP_PUSHBYTES_12': b'\x0c', - 'OP_PUSHBYTES_13': b'\x0d', - 'OP_PUSHBYTES_14': b'\x0e', - 'OP_PUSHBYTES_15': b'\x0f', - 'OP_PUSHBYTES_16': b'\x10', - 'OP_PUSHBYTES_17': b'\x11', - 'OP_PUSHBYTES_18': b'\x12', - 'OP_PUSHBYTES_19': b'\x13', - 'OP_PUSHBYTES_20': b'\x14', - 'OP_PUSHBYTES_21': b'\x15', - 'OP_PUSHBYTES_22': b'\x16', - 'OP_PUSHBYTES_23': b'\x17', - 'OP_PUSHBYTES_24': b'\x18', - 'OP_PUSHBYTES_25': b'\x19', - 'OP_PUSHBYTES_26': b'\x1a', - 'OP_PUSHBYTES_27': b'\x1b', - 'OP_PUSHBYTES_28': b'\x1c', - 'OP_PUSHBYTES_29': b'\x1d', - 'OP_PUSHBYTES_30': b'\x1e', - 'OP_PUSHBYTES_31': b'\x1f', - 'OP_PUSHBYTES_32': b'\x20', - 'OP_PUSHBYTES_33': b'\x21', - 'OP_PUSHBYTES_34': b'\x22', - 'OP_PUSHBYTES_35': b'\x23', - 'OP_PUSHBYTES_36': b'\x24', - 'OP_PUSHBYTES_37': b'\x25', - 'OP_PUSHBYTES_38': b'\x26', - 'OP_PUSHBYTES_39': b'\x27', - 'OP_PUSHBYTES_40': b'\x28', - 'OP_PUSHBYTES_41': b'\x29', - 'OP_PUSHBYTES_42': b'\x2a', - 'OP_PUSHBYTES_43': b'\x2b', - 'OP_PUSHBYTES_44': b'\x2c', - 'OP_PUSHBYTES_45': b'\x2d', - 'OP_PUSHBYTES_46': b'\x2e', - 'OP_PUSHBYTES_47': b'\x2f', - 'OP_PUSHBYTES_48': b'\x30', - 'OP_PUSHBYTES_49': b'\x31', - 'OP_PUSHBYTES_50': b'\x32', - 'OP_PUSHBYTES_51': b'\x33', - 'OP_PUSHBYTES_52': b'\x34', - 'OP_PUSHBYTES_53': b'\x35', - 'OP_PUSHBYTES_54': b'\x36', - 'OP_PUSHBYTES_55': b'\x37', - 'OP_PUSHBYTES_56': b'\x38', - 'OP_PUSHBYTES_57': b'\x39', - 'OP_PUSHBYTES_58': b'\x3a', - 'OP_PUSHBYTES_59': b'\x3b', - 'OP_PUSHBYTES_60': b'\x3c', - 'OP_PUSHBYTES_61': b'\x3d', - 'OP_PUSHBYTES_62': b'\x3e', - 'OP_PUSHBYTES_63': b'\x3f', - 'OP_PUSHBYTES_64': b'\x40', - 'OP_PUSHBYTES_65': b'\x41', - 'OP_PUSHBYTES_66': b'\x42', - 'OP_PUSHBYTES_67': b'\x43', - 'OP_PUSHBYTES_68': b'\x44', - 'OP_PUSHBYTES_69': b'\x45', - 'OP_PUSHBYTES_70': b'\x46', - 'OP_PUSHBYTES_71': b'\x47', - 'OP_PUSHBYTES_72': b'\x48', - 'OP_PUSHBYTES_73': b'\x49', - 'OP_PUSHBYTES_74': b'\x4a', - 'OP_PUSHBYTES_75': b'\x4b', """ - 'OP_PUSHDATA1': b'\x4c', - 'OP_PUSHDATA2': b'\x4d', - 'OP_PUSHDATA4': b'\x4e', + 'OP_0': b'\x00', 'OP_1NEGATE': b'\x4f', 'OP_RESERVED': b'\x50', 'OP_1': b'\x51', diff --git a/src/script.py b/src/script.py index 8c34db3..e3c5e67 100644 --- a/src/script.py +++ b/src/script.py @@ -165,17 +165,33 @@ def execute(self) -> bool: op = self.script[i:i+1] # Handle data push operations - if op not in OP_CODES.values(): - length = int.from_bytes(op, 'little') - if length > 75: # Use OP_PUSHDATA operations for larger chunks - raise InvalidScriptException(f"Invalid push operation length: {length}") + if op not in OP_CODES.values(): + length = int.from_bytes(op, 'little') + if length > 75: # Use OP_PUSHDATA operations for larger chunks + if length == 76: #'OP_PUSHDATA1' + length = self.script[i+1] + data = self.script[i+2:i+2+length] + self.stack.push(data) + return length + 2 + elif length == 77: # 'OP_PUSHDATA2' + length = int.from_bytes(self.script[i+1:i+3], 'little') + data = self.script[i+3:i+3+length] + self.stack.push(data) + return length + 3 + elif length == 78: # 'OP_PUSHDATA4' + length = int.from_bytes(self.script[i+1:i+5], 'little') + data = self.script[i+5:i+5+length] + self.stack.push(data) + return length + 5 + else: + raise InvalidScriptException(f"Invalid push operation length: {length}") data = self.script[i+1:i+1+length] self.stack.push(data) i += length + 1 continue # Handle opcodes - op_name = list(OP_CODES.keys())[list(OP_CODES.values()).index(op)] + op_name = list(OP_CODES.keys())[list(OP_CODES.values()).index(op)] i += self._execute_opcode(op_name) # Script executed successfully if stack is not empty and top value is true diff --git a/src/transaction.py b/src/transaction.py index ce0ba23..a4095cc 100644 --- a/src/transaction.py +++ b/src/transaction.py @@ -54,8 +54,8 @@ def is_valid(self): return False # Check each input validity. - for vin_idx, vin in enumerate(self.vin): - if not self.valid_input(vin_idx, vin): + for vin in self.vin: + if not self.valid_input(vin): return False # Check each output validity. @@ -94,7 +94,7 @@ def check_input_output_sum(self): return True - def valid_input(self, vin_idx, vin): + def valid_input(self, vin): if vin.get("is_coinbase", False): return False @@ -102,10 +102,10 @@ def valid_input(self, vin_idx, vin): scriptpubkey_type = prevout.get("scriptpubkey_type", "") if scriptpubkey_type == "p2pkh": - return self.validate_p2pkh(vin_idx, vin) + return self.validate_p2pkh(vin) elif scriptpubkey_type == "p2sh": + #return self.validate_p2sh_p2wpkh(vin) pass - #return self.validate_p2sh_p2wpkh(vin_idx, vin) elif scriptpubkey_type == "v0_p2wsh": pass #return self.validate_p2wsh(vin) @@ -123,7 +123,7 @@ def valid_output(self, vout): scriptpubkey_type = vout.get("scriptpubkey_type", "") return scriptpubkey_type in ["v0_p2wpkh", "p2sh", "v0_p2wsh", "v1_p2tr", "p2pkh"] - def validate_p2pkh(self, vin_idx, vin): + def validate_p2pkh(self, vin): ################# # Pubkey script # ################# @@ -142,171 +142,18 @@ def validate_p2pkh(self, vin_idx, vin): #print(is_valid) return is_valid -""" - ##################################################################### - # Extract signature and public key from scriptSig (Parse scriptSig) # - ##################################################################### - # https://learnmeabitcoin.com/technical/script/p2pkh/ - # Explanation: the scriptSig contains the signature and the public key (including ASM instructions). - - signature_len = scriptsig[0] # The first byte represents the length of the DER signature (including hash type) - signature_w_hash_type = scriptsig[1:1+signature_len] # Extract the signature (includes the hash type at the end) - - # The last byte of the signature is the hash type (e.g., SIGHASH_ALL = 0x01) - signature = signature_w_hash_type[:-1] - hash_type = signature_w_hash_type[-1] - - public_key_idx = 1 + signature_len - public_key_len = scriptsig[public_key_idx] - public_key = scriptsig[public_key_idx+1:public_key_idx+1+public_key_len] - - ####################### - # Parse DER signature # - ####################### - # https://bitcoin.stackexchange.com/questions/92680/what-are-the-der-signature-and-sec-format - # https://learnmeabitcoin.com/technical/keys/signature/ - - # Remove the hash_type from the DER signature - der_signature = signature_w_hash_type[:-1] - - r, s, hash_type = parse_der_signature_bytes(der_signature) - - der_len = len(der_signature) - signature_len = len(r + s) + 6 - - if der_len != signature_len: - return False - - signature = r + s - #print(signature) - - ###################### - # Parse scriptPubKey # - ###################### - # https://learnmeabitcoin.com/technical/script/p2pkh/ - # Explanation: the scriptPubKey contains: DUP, HASH160, public key hash (including OP_PUSHBYTES_20), EQUALVERIFY and CHECKSIG. - - if scriptpubkey[0:1] != b'\x76' or scriptpubkey[1:2] != b'\xa9' or scriptpubkey[2:3] != b'\x14': - return False # Not a valid P2PKH scriptPubKey (missing OP_DUP, OP_HASH160, or length mismatch) - - if scriptpubkey[23:24] != b'\x88' or scriptpubkey[24:25] != b'\xac': - return False # Not a valid P2PKH scriptPubKey (missing OP_EQUALVERIFY or OP_CHECKSIG) - pkh = scriptpubkey[3:23] - - # Compute the public key hash (HASH160 of the public key) and compare with scriptPubKey - calc_pkh = hash160(public_key) - if calc_pkh != pkh: - return False # Public key hash does not match - - ############################################ - # Verify the signature with the public key # - ############################################ - - data_signed = serialize_transaction(self.json_transaction, vin_idx, int(hash_type)) - data_hash = hashlib.sha256(data_signed).digest() - - #print(self.json_transaction) - #print("********************************") - - # Verify the signature - verifying_key = VerifyingKey.from_string(public_key, curve=SECP256k1) - try: - verifying_key.verify(signature, data_hash, hashlib.sha256) - except BadSignatureError: - return False - - print(public_key) - print("-------------------") - return True -""" -""" - def validate_p2sh_p2wpkh(self, vin_idx, vin): - # Extract scriptSig and witness + def validate_p2sh_p2wpkh(self, vin): scriptsig = decode_hex(vin.get("scriptsig", "")) - witness = vin.get("witness", []) - - if not scriptsig or len(witness) < 2: + if not scriptsig: return False - - print(vin["txid"]) - prevout = vin.get("prevout", {}) - if not prevout: return False - scriptpubkey = decode_hex(prevout.get("scriptpubkey", "")) - ############################# - # Check if it's a P2SH script # - ############################# - if len(scriptpubkey) != 23 or scriptpubkey[0:1] != b'\xa9' or scriptpubkey[-1:] != b'\x87': - return False # Not a valid P2SH scriptPubKey - - # Extract the redeem script hash from the scriptPubKey - # Extract redeem script hash from scriptPubKey - if scriptpubkey[0] != 0xa9: # Check for OP_HASH160 - return False - - length_of_hash = scriptpubkey[1] - if length_of_hash != 0x14: # 20 bytes - return False - - expected_redeem_script_hash = scriptpubkey[2:2+length_of_hash] - - ########################### - # Extract the redeem script # - ########################### - # The redeem script is the data in the scriptSig - redeem_script = scriptsig - - # Hash the redeem script and compare it with the expected hash in the scriptPubKey - redeem_script_hash = hash160(redeem_script) - - #print("rsh: ", redeem_script_hash) - #print("ersh: ", expected_redeem_script_hash) - - if redeem_script_hash != expected_redeem_script_hash: - return False # Redeem script hash does not match - - ############################## - # Parse and execute redeem script # - ############################## - # The redeem script should be a P2WPKH script: OP_0 <20-byte-public-key-hash> - if len(redeem_script) != 22 or redeem_script[0:1] != b'\x00' or redeem_script[1:2] != b'\x14': - return False # Not a valid P2WPKH redeem script - - # Extract the public key hash from the redeem script - public_key_hash = redeem_script[2:] - - ###################### - # Verify the witness # - ###################### - # The witness field contains: - # - witness[0] = signature - # - witness[1] = public key - - signature = decode_hex(witness[0]) - public_key = decode_hex(witness[1]) - - # Compute the public key hash (HASH160 of the public key) and compare with the public key hash in the redeem script - calc_pkh = hash160(public_key) - if calc_pkh != public_key_hash: - return False # Public key hash does not match - - ############################################ - # Verify the signature with the public key # - ############################################ - - data_signed = serialize_transaction(self.json_transaction, vin_idx, 1) # SIGHASH_ALL is typically 1 - data_hash = hashlib.sha256(data_signed).digest() - - # Verify the signature - verifying_key = VerifyingKey.from_string(public_key, curve=SECP256k1) - try: - verifying_key.verify(signature[:-1], data_hash, hashlib.sha256) # Remove the last byte (hash type) - except BadSignatureError: - return False + # Combine and verify + script = Script.combine_scripts(scriptsig, scriptpubkey, json_transaction=self.json_transaction) + is_valid = script.execute() - return True """ + return is_valid diff --git a/src/utils.py b/src/utils.py index 32ad658..5779bb0 100644 --- a/src/utils.py +++ b/src/utils.py @@ -22,3 +22,17 @@ def hash160(data): ripemd160.update(sha256_hash) return ripemd160.digest() + +def total_fee(transactions): + input_sum = 0 + output_sum = 0 + + for tx in transactions: + for input in tx.vin: + prevout = input.get("prevout", {}) + if prevout: + input_sum = input_sum + prevout["value"] + for output in tx.vout: + output_sum = output_sum + output["value"] + + return input_sum - output_sum