From a8ad18c26a3826377ddaccad6955385c5b5296d6 Mon Sep 17 00:00:00 2001 From: obamwonyi Date: Mon, 6 May 2024 12:35:39 +0100 Subject: [PATCH] reimplemented the transaction verification --- main.py | 21 +- src/__pycache__/validation.cpython-310.pyc | Bin 4652 -> 4870 bytes src/validation.py | 301 +++++++++++---------- 3 files changed, 174 insertions(+), 148 deletions(-) diff --git a/main.py b/main.py index 6086010..ceafc5d 100644 --- a/main.py +++ b/main.py @@ -3,9 +3,12 @@ # TODO: replace json with a serializer import json from src.transaction import Transaction, TransactionSchema -from src.validation import validate_transactions +from src.validation import ValidateTransaction from marshmallow import ValidationError from src.mine import mine_block +from collections import defaultdict + +TRANSACTION_BY_ID = defaultdict(dict) # TODO: Remove async def main(): @@ -19,8 +22,13 @@ async def main(): transaction_schema = TransactionSchema() try: loaded_data = json.loads(json_data) - transaction = transaction_schema.load(loaded_data) - transactions.append(transaction) + # transaction = transaction_schema.load(loaded_data) + for tx_input in loaded_data.get("vin", []): + txid = tx_input.get("txid") + if txid: + TRANSACTION_BY_ID[txid] = loaded_data + + transactions.append(loaded_data) # print(f"Deserialized transaction from {filename}") except ValidationError as e: # errors = errors + 1 @@ -31,9 +39,14 @@ async def main(): # print(f"Total failed transactions:{errors}") # Step 2: Validate transactions asynchronously - valid_transactions = await validate_transactions(transactions) + validate_transaction = ValidateTransaction(TRANSACTION_BY_ID) + valid_transactions = await validate_transaction.validate_transactions(transactions) + + # implement an initial transaction validation process. print(valid_transactions) + + # print(valid_transactions) # Step 3: Mine the block block_data = mine_block(valid_transactions) diff --git a/src/__pycache__/validation.cpython-310.pyc b/src/__pycache__/validation.cpython-310.pyc index ab51b4de5636785a79790dabe1cfb14fecd549ad..621186a5429526a910c489b9333065c5aceafa16 100644 GIT binary patch literal 4870 zcma)A&2QVt73Y^IN|qf*`E)m%<#y8~+9q}o6y3s2gKZO}Srkx%b(%wVQJhMC3cYz`}GleY~v zkGFNq=Ga14!>Utk5pNpoG;4xxo}FQ5@w-6u{FJ0P$IgRdkzHUH@#QpIfYeLLnI~?~ zV{XJB3)c_bPUHoCM=NMR&vf6z9g?kLHC7y$VoiCYwp(g)I-0I@*7qEbwY0Z#g4O zOu?g}IIb@_P3n6E53zaxw;#e~TUxk~&-5)#EHYzFVHFtC9DQB4Cs($m9c^;EU>qA- zha_xHxjJx#+qa|L)#w<;uhOUgZd@J3Da4IaJGLn7|etTnT*{1J#qaM!z+ZzbLBvd zVY;yvYho_e)|3dbAFF>y{(PtJt12>xa%c2yuIZON5+2{-wmW0Gt_b?J7lrm0_jA(R zdLCP~3qsG(Waea^3~g8J+1)_&?VwxapYt4@FEV=JMVs)B$Lx1?+Ycfv9r~=@(q&|e z1*n-+2Ey|rypm2Zh&q`onER+t&O#!oVbLdN$Z8D?+RKLY#-av~TrcxT+42rqKD$T` z3J~cDLM?a#7`3Ft+LAIhVuRr~M)<<=`sg3I>e*(YjN7myyg{_)ZP=WP3j2yGJR~?VZ7JeT(m{+G|pp zN5l2s@jW{V>~&7b!eao1p6sSLy8jZB?9ZyGO@}RzE`q;Pb8z_r;WbK-H@?P z!>AY!ZXekb<%D_qmVn=k3)`uBr%JT_K&p!BRZJ!)-DtU>a&@$l$u;ANN$o3H-R_XN zvx{;nN4H0q5V{?pG#sj5pg25K#(JzXb*P975ekJwIDLX}C=s!Kps@;Y(1^82kBxnG zt0JyY@rW_&QYM;i1wPM>^W_92QY%sv9!sy(zsGWt0OIq@-NGxU)#}e(BpO| z@FN$6)!&e~P%4V+vlKK2yd#s+PKCRX>jMzE-VS_z!=|xidw}0fJ>B z?a@b%$`qcWv1q~tEv?;_NsCNAkGUWaO)*(V%OE}?nl#A;Iy<AIkT^}0 zQb(qHv^&Y2&p;8STyzr&xA&J&<8N4GPfO0%Yv!T;6xkGYih zHhE`j5(+bw!wV4@!*xVoIN(1FGIFLTsD26%m-2OpFXN;up{=QmJ}J}m2>py|)Cya; zB}zAuyHiLAEp`fhp^mmkJy4l(sNPqey#Pcg2O40aLgkm4OUj`J@TfjizShRZxDr>w zpP(VK016e*+ha4ez~{SlcqhS6Bt?o_>9HW?YLZaShbX>QEm#$xwm7<%`{jhNwX=`}icI^5eC3089~Z*U-gHCs z401`(AITv8b1;r#mDq? z2}7de(3Jw`hxAT%6hEe?PcUE&6+W?u0mr-`a(k0*M_MkifRxfk5>=CiM9);WFvVEe zQ#I3-=?7{~VD#(6_Xq-?Ez@ZWgyAnRXfNv(oKv&~?J{uK1O_*7H#NfLi@Jq33;KeJ zcdGW^x^4ogP1V$lf0lRAWn0apH&uVS_i2AJ;4AeTrHeiqCwGKDCDT}ol;>)!{aHa8 z>krYQ;Ux1-9=1;=%|&QadE9VO?*yKEy)@j5p-xrajqDe}u*d8k+8IF|Zr}4cJ6_+X z84@1vobug|#VdJI9y6#`Zcw~d$LhC}Cg!i&HkuIWkd`V|K!Lcuh97Yb!y7e?1L-f> zW!CTXP_a8fe{lCZ5J(U`uZ(_`>0HRy(j#P82-Tbu@-?Zt;hx{|g2YS>mYu5_1z-r1 zO5}!HVH!WTNJ=TxxiY7Qeb0V{*zlK=n! literal 4652 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 diff --git a/src/validation.py b/src/validation.py index 5b0f808..ae39e24 100644 --- a/src/validation.py +++ b/src/validation.py @@ -1,147 +1,160 @@ import asyncio -from bitcoin.core import MAX_BLOCK_SIZE -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.backends import default_backend -from typing import Tuple -from bitcoinlib.transactions import Transaction as BitcoinTransaction - -async def validate_transaction(transaction) -> Tuple[bool, str]: - """ - Validate a single transaction. - :param transaction: The transaction to validate. - :return: A tuple containing a boolean indicating whether the transaction is valid, and a string message. - """ - # Check if the transaction has inputs and outputs - if not transaction.vin: - return False, "Transaction has no inputs" - if not transaction.vout: - return False, "Transaction has no outputs" - - # Initialize the total input and output values - total_input_value = 0 - total_output_value = 0 - # Iterate over the inputs - for tx_input in transaction.vin: - # Handle coinbase transactions separately - if tx_input.is_coinbase: - # Coinbase transactions have specific rules that need to be validated - # Check if the coinbase transaction follows the correct format - # and has a valid block height and coinbase value - if not validate_coinbase_transaction(tx_input): - return False, f"Invalid coinbase transaction input: {tx_input}" + +class ValidateTransaction: + + def __init__(self, transaction_by_id): + self.transaction_by_id = transaction_by_id + + def validate_transaction_version(self, tx) -> bool: + """ + Validate the transaction version. + :param tx: + :return: Bool + """ + if tx.get("version") == 1 or tx.get("version") == 2: + return True + return False + + async def retrieve_transaction(self, txid) -> dict: + """ + Retrieve a transaction from its ID + :param txid: Transaction ID + :return: dictionary form of transaction + """ + transaction = self.transaction_by_id.get(txid) + if transaction: + return transaction else: - # Extract the public key from the input witness - if not tx_input.witness: - return False, f"Input {tx_input.txid}:{tx_input.vout} has no witness" - - public_key_bytes = bytes.fromhex(tx_input.witness[-1]) - - # Construct the public key object - public_key = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), public_key_bytes) - - # Extract the signature from the input witness - signature_bytes = b"".join(bytes.fromhex(witness_item) for witness_item in tx_input.witness[:-1]) - - # Get the transaction data that was signed for this input - tx_input_data = get_tx_input_data(transaction, tx_input) - - # Define the signature algorithm - signature_algorithm = ec.EllipticCurveSignatureAlgorithm(hashes.SHA256()) - - try: - # Verify the signature using the public key, signature, and transaction data - public_key.verify( - signature_bytes, - tx_input_data, - signature_algorithm - ) - except InvalidSignature: - return False, f"Invalid signature for input {tx_input.txid}:{tx_input.vout}" - - # Add the input value to the total input value - total_input_value += tx_input.prevout.value - - # Validate the input script - try: - bitcoin_tx = BitcoinTransaction.from_dict(transaction) - bitcoin_tx.verify_input_signature(tx_input.vout) - except Exception as e: - return False, f"Invalid input script for {tx_input.txid}:{tx_input.vout}: {str(e)}" - - # Iterate over the outputs - for tx_output in transaction.vout: - # Add the output value to the total output value - total_output_value += tx_output.value - - # Validate the output script - try: - bitcoin_tx = BitcoinTransaction.from_dict(transaction) - bitcoin_tx.verify_output_script(tx_output.scriptpubkey_asm) - except Exception as e: - return False, f"Invalid output script: {str(e)}" - - # Check if the total input value is greater than or equal to the total output value - if total_input_value < total_output_value: - return False, "Total input value is less than total output value" - - # Calculate the transaction fee - transaction_fee = total_input_value - total_output_value - - # Validate transaction fee according to the fee rules - if transaction_fee < 0: - return False, "Transaction fee cannot be negative" - - # Check if the transaction size exceeds the maximum block size - transaction_size = sum(len(tx_input.scriptsig) for tx_input in transaction.vin) + \ - sum(len(tx_output.scriptpubkey) for tx_output in transaction.vout) - if transaction_size > MAX_BLOCK_SIZE: - return False, "Transaction size exceeds the maximum block size" - - return True, "Transaction is valid" - -def validate_coinbase_transaction(tx_input) -> bool: - """ - Validate a coinbase transaction input. - :param tx_input: The coinbase transaction input to validate. - :return: True if the coinbase transaction input is valid, False otherwise. - """ - # Implement your coinbase transaction validation logic here - # For example, you could check if the block height and coinbase value are valid - # based on the current network rules and block subsidies. - # This is just a placeholder function, you need to implement the actual validation logic. - return True - -async def validate_transactions(transactions) -> list: - """ - Validate a list of transactions asynchronously. - :param transactions: A list of transactions to validate. - :return: A list of valid transactions. - """ - async_tasks = [validate_transaction(tx) for tx in transactions] - - try: - validation_results = await asyncio.gather(*async_tasks) - except Exception as e: - print(f"An error occurred during transaction validation, Error: {e}") - return [] - - valid_transactions = [tx for tx, (is_valid, _) in zip(transactions, validation_results) if is_valid] - - return valid_transactions - -def get_tx_input_data(transaction, tx_input): - """ - Helper function to construct the transaction data that was signed for a given input. - This implementation assumes the transaction version is 1 or higher. - """ - tx_data = b"" - tx_data += transaction.version.to_bytes(4, byteorder="little") - tx_data += tx_input.prevout.scriptpubkey.encode() - tx_data += tx_input.prevout.value.to_bytes(8, byteorder="little") - tx_data += tx_input.sequence.to_bytes(4, byteorder="little") - tx_data += transaction.locktime.to_bytes(4, byteorder="little") - - return tx_data \ No newline at end of file + print(f"Transaction with txid {txid} not found") + return None + + async def verify_input_script(self, prev_tx, vout, script_pubkey) -> bool: + """ + Validates the scriptSig or witness. + :param prev_tx: Previous transaction dictionary + :param vout: Output index + :param script_pubkey: ScriptPubKey to be verified + :return: bool + """ + prev_output = prev_tx["vout"][vout] + return prev_output["scriptpubkey"] == script_pubkey + + async def is_valid_output_script(self, scriptpubkey): + """ + Validate the script public key of the output + :param scriptpubkey: + :return: + """ + + async def validate_transaction_amount(self, tx) -> bool: + """ + Validates the transaction amount + :param tx: + :return: bool + """ + # Implement logic to validate the transaction amount + pass + + async def get_prev_tx_output(self, txid, vout) -> (list, None): + """ + Retrieves the previous transaction output. + :param txid: Transaction ID + :param vout: Output index + :return: A tuple containing the previous transaction and the specified output, + or (None, None) if they don't exist + """ + # Step 1: Retrieve the previous transaction + prev_tx = await self.retrieve_transaction(txid) + if prev_tx is None: + print(f"Previous transaction with txid {txid} not found") + return None, None + + prev_output = prev_tx.get("vout") + # Step 2: Check if the specified output index exists + if vout >= len(prev_output): + return None, None + + return prev_tx, prev_output + + # Step 3: Verify that the output is unspent + # if not await self.is_unspent(prev_tx, vout): + # return None, None + + # # Step 4: Retrieve the script pub key from the referred output + # script_pubkey = prev_tx.get("vout")[0].get("scriptpubkey") + # + # # Step 5: Validate the scriptSig or Witness + # if not await self.validate_script(prev_tx, vout, script_pubkey): + # return None, None + + # Step 6: Ensure the sum of input values is greater than or equal to the sum of output values + if not await self.validate_transaction_amount(prev_tx): + return None, None + + return prev_tx, prev_tx.get("vout")[vout] + + async def validate_locktime_and_sequence(self, tx): + """ + Validates Transactions locktime + :param tx: + :return: + """ + pass + + async def is_double_spend(self, tx): + """ + Check if the transaction is double spent + :param tx: + :return: + """ + async def validate_transaction(self, tx) -> (bool, str): + """ + :param tx: + :return: validated transaction + """ + if not self.validate_transaction_version(tx): + return False, f"Transaction has invalid version \n" + + total_input_value = 0 + + for tx_input in tx.get("vin"): + prev_tx, prev_outputs = await self.get_prev_tx_output(tx_input["txid"], tx_input["vout"]) + if not prev_outputs: + return False, "Previous output not found" + + for prev_output in prev_outputs: + total_input_value += prev_output["value"] + + vout = tx_input["vout"] + + if not self.verify_input_script(prev_tx, vout, prev_output["scriptpubkey"]): + return False, "Failed to verify input script" + total_input_value += prev_output.get("value") + + # Validate outputs + total_output_value = sum(output.get("value") for output in tx.get("vout")) + if total_output_value > total_input_value: + return False + + for output in tx.get("vout"): + if not self.is_valid_output_script(output.get("scriptpubkey")): + return False + + if not self.validate_locktime_and_sequence(tx): + return False + + if self.is_double_spend(tx): + return False + + return True + # print("Transaction has valid version") tested + + async def validate_transactions(self, transactions): + """ + Validates and gathers all valid transactions that would later be mined. + :param transactions: + :return: + """ + tasks = [self.validate_transaction(transaction) for transaction in transactions] + return await asyncio.gather(*tasks) \ No newline at end of file