-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
reimplemented the transaction verification
- Loading branch information
Showing
3 changed files
with
174 additions
and
148 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
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) |