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 ab51b4d..621186a 100644 Binary files a/src/__pycache__/validation.cpython-310.pyc and b/src/__pycache__/validation.cpython-310.pyc differ 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