From 1a1b612edfcccd92e4821de712ca6af2f08654b5 Mon Sep 17 00:00:00 2001 From: David Chocholaty Date: Sun, 28 Apr 2024 14:33:01 +0200 Subject: [PATCH] Add p2pkh verification This commit adds the implementation until the p2pkh verification. --- main.py | 459 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + run.sh | 12 +- 3 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 main.py create mode 100644 requirements.txt mode change 100644 => 100755 run.sh diff --git a/main.py b/main.py new file mode 100644 index 00000000..dfcbdced --- /dev/null +++ b/main.py @@ -0,0 +1,459 @@ +import argparse +import ecdsa +import hashlib +import os +import json +import copy +import binascii + +from sha3 import sha3_224 +from Crypto.Hash import RIPEMD160 + +#TARGET = "0000ffff00000000000000000000000000000000000000000000000000000000" +TARGET = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + +class MemPool: + def __init__(self, root_dir): + self.root_dir = root_dir + #self.transactions = [os.path.join(self.root_dir, file) for file in os.listdir(self.root_dir) if file.endswith('.json')] + 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 = [] + + +# TODO prejmenovat promennou input na neco jineho + +# scriptpubkey_type can be: p2sh, p2pkh, v0_p2wsh, v1_p2tr, v0_p2wpkh. + +def get_filename_without_extension(file_path): + # Get the base filename from the path + filename = os.path.basename(file_path) + # Remove the extension + filename_without_extension = os.path.splitext(filename)[0] + return filename_without_extension + +def valid_transaction_syntax(json_transaction): + required = ["version", "locktime", "vin", "vout"] + + for field in required: + if field not in json_transaction: + print('Required field is missing') + return False + + if not isinstance(json_transaction["version"], int): + print('Invalid data type') + return False + + if not isinstance(json_transaction["locktime"], int): + print('Invalid data type') + return False + + if not isinstance(json_transaction["vin"], list): + print('Invalid data type') + return False + + if not isinstance(json_transaction["vout"], list): + print('Invalid data type') + return False + + # Check inputs + for input in json_transaction['vin']: + if not isinstance(input, dict): + print('Invalid data type') + return False + + if 'txid' not in input or 'vout' not in input: + print('Invalid data type') + return False + + # Check outputs + + for output in json_transaction['vout']: + if not isinstance(output, dict): + print('Invalid data type') + return False + + if 'scriptpubkey' not in output or 'value' not in output: + print('Invalid data type') + return False + + return True + +def non_empty_vin_vout(vin, vout): + # Make sure neither in or out lists are empty + if not vin: + print("vin is empty") + return False + if not vout: + print("vout is empty") + return False + + return True + +def serialize_input(tx_input, override=None): + serialized_input = [] + serialized_input += [bytes.fromhex(tx_input["txid"])[::-1]] # Reversed txid + serialized_input += [tx_input["vout"].to_bytes(4, byteorder="little")] + + if override is None: + serialized_input += [serialize_script(bytes.fromhex(tx_input["scriptsig"]))] + elif override is True: + serialized_input += [serialize_script(bytes.fromhex(tx_input["prevout"]["scriptpubkey"]))] + elif override is False: + serialized_input += [serialize_script(bytes.fromhex(""))] + + # Serialize the transaction input + #serialized_input = ( + # bytes.fromhex(tx_input["txid"])[::-1] # Reversed txid + # + tx_input["vout"].to_bytes(4, byteorder="little") + # + serialize_script(bytes.fromhex(tx_input["scriptsig"])) + # + tx_input["sequence"].to_bytes(4, byteorder="little") + #) + serialized_input += [tx_input["sequence"].to_bytes(4, byteorder="little")] + + return b''.join(serialized_input) + +def serialize_script(script): + # Serialize the script + #print(len(script).to_bytes(1, byteorder="big")) + #print(script) + return len(script).to_bytes(1, byteorder="big") + script + +def serialize_output(output): + serialized_output = [] + + serialized_output += [output["value"].to_bytes(8, byteorder="little")] + serialized_output += [serialize_script(bytes.fromhex(output["scriptpubkey"]))] + # Serialize the output + #return ( + # output["value"].to_bytes(8, byteorder="little") + # + serialize_script(bytes.fromhex(output["scriptpubkey"])) + #) + return b''.join(serialized_output) + +def encode_varint(i): + if i < 0xfd: + return bytes([i]) + elif i < 0x10000: + return b'\xfd' + i.to_bytes(2, 'little') + elif i < 0x100000000: + return b'\xfe' + i.to_bytes(4, 'little') + elif i < 0x10000000000000000: + return b'\xff' + i.to_bytes(8, 'little') + else: + raise ValueError("integer too large: %d" % (i, )) + +def serialize_transaction(transaction, index=-1, sighash_type=1): + # Zatim pro p2pkh + message = [] + message += [transaction["version"].to_bytes(4, byteorder="little")] + + # encode inputs + message += [encode_varint(len(transaction["vin"]))] + + inputs = transaction["vin"] + outputs = transaction["vout"] + + if index == -1: + message += [serialize_input(tx_in) for tx_in in inputs] + else: + message += [serialize_input(tx_in, index == i) for i, tx_in in enumerate(inputs)] + + # encode outputs + message += [encode_varint(len(transaction["vout"]))] + message += [serialize_output(tx_out) for tx_out in outputs] + + # encode rest of data + message += [transaction["locktime"].to_bytes(4, byteorder="little")] + hash_type = 1 + message += [hash_type.to_bytes(4, 'little') if index != -1 else b''] # 1 = SIGHASH_ALL + + return b''.join(message) + +def parse_der_signature(der_signature_with_hash_type): + # Remove the hash_type from the DER signature + der_signature = der_signature_with_hash_type[:-2] + + # Parse the DER signature + der_bytes = bytes.fromhex(der_signature) + r_length = der_bytes[3] + r = int.from_bytes(der_bytes[4:4 + r_length], 'big') + s_length_index = 4 + r_length + 1 + s_length = der_bytes[s_length_index] + s = int.from_bytes(der_bytes[s_length_index + 1:s_length_index + 1 + s_length], 'big') + hash_type = der_bytes[-1] + + return r, s, hash_type + +def verify_p2pkh_transaction(input_idx, json_transaction): + ################# + # Pubkey script # + ################# + + input_tx = json_transaction["vin"][input_idx] + + # Extract data from input transaction + script_sig_asm = input_tx["scriptsig_asm"] + # prevout_scriptpubkey_asm = input_tx["prevout"]["scriptpubkey_asm"] + + # Parse scriptSig ASM to extract signature and public key + script_parts = script_sig_asm.split(" ") + signature_hex = script_parts[1] + public_key_hex = script_parts[3] + + r, s, hash_type = parse_der_signature(signature_hex) + + # Decode hex strings to bytes + #hash_type = signature_hex[-2:] + # Remove the last byte + #signature = bytes.fromhex(signature_hex[:-2]) + #print(hex(r) + hex(s)) + r_hex = hex(r)[2:] + s_hex = hex(s)[2:] + + #print(signature_hex[:-2]) + #print(r_hex + s_hex) + der_len = len(signature_hex[:-2]) + signature_len = len(r_hex + s_hex) + 2 * 6 + + if der_len != signature_len: + #print("Signature verification failed.") + return False + + #print(json_transaction["vin"][input_idx]["scriptsig_asm"]) + #print(r_hex + s_hex) + signature = bytes.fromhex(r_hex + s_hex) + + public_key = bytes.fromhex(public_key_hex) + + scriptpubkey = bytes.fromhex(input_tx['prevout']['scriptpubkey']) + pubkey_hash = scriptpubkey[3:23] # Moje pozn. : stejna hodnota je v scriptpubkey_asm (to jedine cislo) + + hashed_public_key = hashlib.sha256(public_key).digest() + + ripemd160 = RIPEMD160.new() + ripemd160.update(hashed_public_key) + pubkey_hash_calculated = ripemd160.digest() + + if pubkey_hash != pubkey_hash_calculated: + return False + + + #################### + # Signature script # + #################### + + data_signed = serialize_transaction(json_transaction, input_idx, int(hash_type)) + data_hash = hashlib.sha256(data_signed).digest() + + # Verify the signature + verifying_key = ecdsa.VerifyingKey.from_string(public_key, curve=ecdsa.SECP256k1) + try: + verifying_key.verify(signature, data_hash, hashlib.sha256) + #print("Signature verification successful.") + except ecdsa.BadSignatureError: + #print("Signature verification failed.") + return False + + return True + + +class Transaction: + def __init__(self, transaction_json_file): + # Parse transaction. + with open(transaction_json_file) as transaction: + json_transaction = json.load(transaction) + + # check jestli je valid + + if valid_transaction_syntax(json_transaction): + # TODO osetreni error, pokud by neexistovalo nektere pole + self.transaction_name = get_filename_without_extension(transaction_json_file) + self.version = json_transaction['version'] + self.locktime = json_transaction['locktime'] + self.vin = json_transaction['vin'] + self.vout = json_transaction['vout'] + #self.txid = calculate_txid(json_transaction) + self.json_transaction = json_transaction + else: + # TODO zatim jen print + print('Invalid transaction syntax') + + #def valid_syntax(): + + def is_valid(self): + if not non_empty_vin_vout(self.vin, self.vout): + # TODO print error + return False + + input_sum = 0 + # TODO osetreni error, pokud by neexistovalo nektere pole + for input in self.vin: + #txid = input['txid'] + #vout = input['vout'] + + input_sum = input_sum + input['prevout']['value'] + + #if not valid_signature(input): + # # TODO mozna error message debugovaci + # return False + + output_sum = 0 + for output in self.vout: + output_sum = output_sum + output['value'] + + #print("input: ", input_sum, ", output: ", output_sum) + + if input_sum < output_sum: + # TODO mozna error message debugovaci + return False + + # ----------------------------------------- + # ----------------------------------------- + + input_idx = 0 + for input in self.vin: + if self.version == 1 and 'scriptsig' in input: + scriptsig = input['scriptsig'] + + scriptpubkey_type = input['prevout']['scriptpubkey_type'] + + if scriptsig == "" or scriptpubkey_type not in ["p2pkh", "p2sh"]: + return False + + if scriptpubkey_type == 'p2pkh': + #scriptsig = bytes.fromhex(scriptsig) + #scriptpubkey = bytes.fromhex(input['prevout']['scriptpubkey']) + #public_key = bytes.fromhex(input["scriptsig_asm"].split(" ")[-1]) + #txid = input["txid"] + #vout = input["vout"] + + #if not verify_p2pkh(input): + # return False + if not verify_p2pkh_transaction(input_idx, self.json_transaction): + return False + + #if not verify_signature(public_key, signature): + # return False + else: + # p2sh + #print("p2sh") + pass + + + # print(scriptpubkey_type) + + # TODO pokracovani + #elif self.version == 2 and 'witness' in input: + # print("witness") + + return True + +def is_valid_block_hash(block_hash, target): + if block_hash == "": + return False + # Check if the block hash meets the difficulty target + block_hash_int = int(block_hash, 16) + return block_hash_int < target + +def block_mining(transactions): + # Transactions je list jsons + nonce = 0 + block_hash = "" + target_int = int(TARGET, 16) + + # Calculate Merkle root hash of transactions + merkle_root = calculate_merkle_root(transactions) + + while not is_valid_block_hash(block_hash, target_int): + # Construct block header + block_header = f"{merkle_root}-{nonce}" + + #block_str = json.dumps(transactions, sort_keys=True) + str(nonce) + #block_hash = hashlib.sha256(block_str.encode()).hexdigest() + block_hash = hashlib.sha256(block_header.encode()).hexdigest() + nonce += 1 + + return block_hash + +def calculate_txid(transaction_content): + # Serialize the transaction content + serialized_transaction = json.dumps(transaction_content, sort_keys=True).encode() + + # Calculate double SHA-256 hash + hash_result = hashlib.sha256(hashlib.sha256(serialized_transaction).digest()).digest() + + # Reverse byte order to obtain txid + txid = hash_result[::-1].hex() + + return txid + +def parse_arguments(): + parser = argparse.ArgumentParser(description='Simulation of the mining process of a block') + parser.add_argument('--mempool', type=str, required=True, help='Path to the directory containing the JSON files with transactions.') + return parser.parse_args() + +def calculate_merkle_root(transactions): + # Calculate Merkle root hash from transactions + transaction_hashes = [hashlib.sha256(json.dumps(tx, sort_keys=True).encode()).hexdigest() for tx in transactions] + while len(transaction_hashes) > 1: + if len(transaction_hashes) % 2 != 0: + transaction_hashes.append(transaction_hashes[-1]) # Duplicate last hash if odd number of hashes + new_hashes = [] + for i in range(0, len(transaction_hashes), 2): + new_hash = hashlib.sha256((transaction_hashes[i] + transaction_hashes[i + 1]).encode()).hexdigest() + new_hashes.append(new_hash) + transaction_hashes = new_hashes + return transaction_hashes[0] + +if __name__ == '__main__': + args = parse_arguments() + + if args.mempool is None: + # TODO error + pass + + mempool = MemPool(args.mempool) + + #counter = 0 + + #for transaction in mempool.transactions: + # #print(transaction.transaction_name) + # if not transaction.is_valid(): + # counter = counter + 1 + # # print("Invalid transaction\n") + + # TODO coinbase + coinbase_transaction = { + "version": 1, + "locktime": 0, + "vin": [], + "vout": [] + } + + block_transactions = [coinbase_transaction] + mempool.valid_transactions + + block_hash = block_mining(block_transactions) + + # TODO vypis transakci v poradi (asi podle txid) + + coinbase_serialized = serialize_transaction(coinbase_transaction) + + print(block_hash) + print(coinbase_serialized.hex()) + for json_transaction in block_transactions: + print(calculate_txid(json_transaction)) + # #print(transaction.txid) + + #counter = 0 + + #for transaction in mempool.transactions: + # #print(transaction.transaction_name) + # if not transaction.is_valid(): + # counter = counter + 1 + # # print("Invalid transaction\n") + + #print("counter: ", counter) + #print(mempool.transactions) + #print("count: ", (len(mempool.transactions) - len(mempool.valid_transactions))) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..5dbfbe23 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +ecdsa==0.19.0 +pycryptodome==3.20.0 +sha3==0.2.1 diff --git a/run.sh b/run.sh old mode 100644 new mode 100755 index 721aeb29..dc1f0b31 --- a/run.sh +++ b/run.sh @@ -1 +1,11 @@ -# Update this file to run your own code \ No newline at end of file +#!/bin/bash + +#python3 -m venv venv +#venv/bin/pip install --upgrade pip +#venv/bin/pip install -r requirements.txt +#source /venv/bin/activate +python3 -m venv venv +venv/bin/pip install --upgrade pip +venv/bin/pip install -r requirements.txt +source venv/bin/activate +python3 main.py --mempool=mempool > output.txt