Skip to content

Commit

Permalink
implementation of the dict format of accessing transaction data
Browse files Browse the repository at this point in the history
  • Loading branch information
obamwonyi committed Apr 26, 2024
1 parent 343eb2f commit 9d089bd
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 62 deletions.
1 change: 1 addition & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ async def main():
# Step 2: Validate transactions asynchronously
valid_transactions = await validate_transactions(transactions)

print(valid_transactions)
# Step 3: Mine the block
block_data = mine_block(valid_transactions)

Expand Down
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ docutils~=0.17.1
keyring~=24.3.0
typing_extensions~=4.9.0
setuptools~=69.0.3
future~=0.18.2
future~=0.18.2
bitcoinlib~=0.6.15
ply~=3.11
bcrypt~=3.2.0
Binary file modified src/__pycache__/mine.cpython-310.pyc
Binary file not shown.
Binary file modified src/__pycache__/transaction.cpython-310.pyc
Binary file not shown.
Binary file modified src/__pycache__/validation.cpython-310.pyc
Binary file not shown.
69 changes: 62 additions & 7 deletions src/mine.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,68 @@
import hashlib
import time

def mine_block(valid_transactions) -> str:
"""
Mine all blocks from all the valid transactions
Mine a block by including the valid transactions and finding a valid block hash.
:param valid_transactions: all the validated transactions
:return: str
:param valid_transactions: List of valid transactions to include in the block.
:return: str representing the block data in the required format.
"""
# TODO: implement block mining here
# Initialize the block header
version = 0x20000000 # Version number (0x20000000 for Bitcoin)
prev_block_hash = bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000000") # Placeholder for the previous block hash
merkle_root = calculate_merkle_root(valid_transactions)
time_stamp = int(time.time()) # Current Unix timestamp
bits = 0x1d00ffff # Difficulty target (0x1d00ffff for the given target)
nonce = 0 # Initial nonce value

# Construct the block header
block_header = (
version.to_bytes(4, byteorder="little") +
prev_block_hash +
merkle_root.digest() +
time_stamp.to_bytes(4, byteorder="little") +
bits.to_bytes(4, byteorder="little") +
nonce.to_bytes(4, byteorder="little")
)

# Mine the block by finding a valid hash
target = 0x00000000ffff0000000000000000000000000000000000000000000000000000
while True:
block_hash = hashlib.sha256(hashlib.sha256(block_header).digest()).digest()
if int.from_bytes(block_hash, byteorder="little") < target:
break
nonce += 1
block_header = (
block_header[:68] +
nonce.to_bytes(4, byteorder="little")
)

# Construct the block data
block_data = f"{block_header.hex()}\n" # Block header
block_data += f"{calculate_coinbase_transaction(valid_transactions).hex()}\n" # Coinbase transaction
block_data += "\n".join(tx.txid for tx in valid_transactions) # Transaction IDs

block_data = "Block header\nCoinbase transaction\n"
for tx in valid_transactions:
block_data += f"{tx.txid}\n"
return block_data

def calculate_merkle_root(transactions):
"""
Calculate the Merkle root for the given transactions.
:param transactions: List of transactions to include in the Merkle tree.
:return: Hashlib object representing the Merkle root.
"""
# TODO: Implement the Merkle tree calculation logic
# For now, we'll use a placeholder value
return hashlib.sha256(b"placeholder_merkle_root")

def calculate_coinbase_transaction(transactions):
"""
Calculate the coinbase transaction for the given block.
:param transactions: List of transactions to include in the block.
:return: Bytes representing the coinbase transaction.
"""
# TODO: Implement the coinbase transaction calculation logic
# For now, we'll use a placeholder value
return b"placeholder_coinbase_transaction"
8 changes: 5 additions & 3 deletions src/transaction.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from marshmallow import Schema, fields, post_load
# transaction.py
from marshmallow import Schema, fields, post_load, ValidationError
from typing import Optional

class PrevOutSchema(Schema):
scriptpubkey = fields.Str(required=True)
Expand All @@ -14,7 +16,7 @@ class VinSchema(Schema):
scriptsig = fields.Str(required=True)
scriptsig_asm = fields.Str(required=True)
witness = fields.List(fields.Str(), required=False)
is_coinbase = fields.Bool(required=True)
is_coinbase = fields.Bool(required=False, missing="")
sequence = fields.Int(required=True)
inner_redeemscript_asm = fields.Str(required=False)
inner_witnessscript_asm = fields.Str(required=False)
Expand Down Expand Up @@ -44,4 +46,4 @@ def __init__(self, version, locktime, vin, vout) -> None:
self.version = version
self.locktime = locktime
self.vin = vin
self.vout = vout
self.vout = vout
163 changes: 112 additions & 51 deletions src/validation.py
Original file line number Diff line number Diff line change
@@ -1,86 +1,147 @@
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


def validate_transaction(transaction):
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:
# 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)
# 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}"
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)}"

# Extract the signature from the input witness
signature_bytes = b"".join(bytes.fromhex(witness_item) for witness_item in tx_input.witness[:-1])
# 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"

# Get the transaction data that was signed for this input
tx_input_data = get_tx_input_data(transaction, tx_input)
# Calculate the transaction fee
transaction_fee = total_input_value - total_output_value

# Define the signature algorithm
signature_algorithm = ec.EllipticCurveSignatureAlgorithm(hashes.SHA256())
# Validate transaction fee according to the fee rules
if transaction_fee < 0:
return False, "Transaction fee cannot be negative"

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}"
# 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 get_tx_input_data(transaction, tx_input):
def validate_coinbase_transaction(tx_input) -> bool:
"""
Helper function to construct the transaction data that was signed for a given input.
This implementation assumes the transaction version is 1 or higher.
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.
"""
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

# 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:
"""
gather and return all valid transactions
:param transactions: all transactions, both valid and invalid
:return: list
Validate a list of transactions asynchronously.
:param transactions: A list of transactions to validate.
:return: A list of valid transactions.
"""
async_tasks = []
for tx in transactions:
async_task = validate_transaction(tx)
async_tasks.append(async_task)
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 = []
# Check and store valid transactions
for tx, is_valid in zip(transactions, validation_results):
if is_valid:
valid_transactions.append(tx)

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

0 comments on commit 9d089bd

Please sign in to comment.