From 873fa0e81adb73e067cbcdc36a9500d4206848a4 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sat, 21 Sep 2024 11:34:00 +0200 Subject: [PATCH 01/21] FIXED Fixed imports for PKCS1 to use it a signature instead of a cipher --- src/transactions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transactions.py b/src/transactions.py index 33d57eb..0d65277 100644 --- a/src/transactions.py +++ b/src/transactions.py @@ -1,8 +1,8 @@ import binascii from typing import OrderedDict from Crypto.PublicKey import RSA -from Crypto.Cipher import PKCS1_v1_5 from Crypto.Hash import SHA +from Crypto.Signature import PKCS1_v1_5 class Transaction: From 7bb725183d6ec88899c3540611e6026ccd47bbb4 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sat, 21 Sep 2024 11:34:58 +0200 Subject: [PATCH 02/21] ADDED Integrated Flask into it and implemented the routes --- src/app/__init__.py | 10 ++++++++++ src/app/routes.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/main.py | 7 +++++++ 3 files changed, 61 insertions(+) create mode 100644 src/app/__init__.py create mode 100644 src/app/routes.py create mode 100644 src/main.py diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 0000000..0a7d735 --- /dev/null +++ b/src/app/__init__.py @@ -0,0 +1,10 @@ +from flask import Flask + + +def create_app(): + app = Flask(__name__) + + from .routes import bp as routes_bp + app.register_blueprint(routes_bp) + + return app diff --git a/src/app/routes.py b/src/app/routes.py new file mode 100644 index 0000000..6721117 --- /dev/null +++ b/src/app/routes.py @@ -0,0 +1,44 @@ +import binascii +import Crypto +from Crypto.PublicKey import RSA +from flask import Blueprint, jsonify, request + +from transactions import Transaction + + +bp = Blueprint('routes', __name__) + + +@bp.route('/') +def home(): + return jsonify({"message": "Welcome to Flask!"}) + + +@bp.route('/wallet/new', methods=['GET']) +def new_wallet(): + random_gen = Crypto.Random.new().read + private_key = RSA.generate(1024, random_gen) + public_key = private_key.publickey() + response = { + 'private_key': binascii.hexlify(private_key.exportKey(format='DER')).decode('ascii'), + 'public_key': binascii.hexlify(public_key.exportKey(format='DER')).decode('ascii') + } + + return jsonify(response), 200 + + +@bp.route('/generate/transaction', methods=['POST']) +def generate_transaction(): + + sender_address = request.form['sender_address'] + sender_private_key = request.form['sender_private_key'] + recipient_address = request.form['recipient_address'] + value = request.form['amount'] + + transaction = Transaction( + sender_address, sender_private_key, recipient_address, value) + + response = {'transaction': transaction.to_dict( + ), 'signature': transaction.sign_transaction()} + + return jsonify(response), 200 diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..979c3a9 --- /dev/null +++ b/src/main.py @@ -0,0 +1,7 @@ + +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run() From 07ff7e41c8901dfda4ed3217034174fbbb8d9949 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sat, 21 Sep 2024 11:35:18 +0200 Subject: [PATCH 03/21] ADDED Added routes tests --- tests/test_routes.py | 117 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/test_routes.py diff --git a/tests/test_routes.py b/tests/test_routes.py new file mode 100644 index 0000000..8bdade4 --- /dev/null +++ b/tests/test_routes.py @@ -0,0 +1,117 @@ +import binascii +import os +import sys +import unittest +from unittest.mock import MagicMock, patch +from src.app import create_app + +sys.path.insert(0, os.path.abspath( + # to solve import issues in src + os.path.join(os.path.dirname(__file__), '../src'))) + + +class TestRoutes(unittest.TestCase): + + def setUp(self): + # Set up Flask test client + self.app = create_app() + self.client = self.app.test_client() + + def test_home(self): + # Simulate a request to the '/' route + response = self.client.get('/') + self.assertEqual(response.status_code, 200) + self.assertIn(b'Welcome to Flask!', response.data) + + @patch('src.app.routes.RSA.generate') # Mock RSA.generate + @patch('src.app.routes.Crypto.Random.new') # Mock Crypto.Random.new().read + def test_new_wallet(self, mock_random_new, mock_rsa_generate): + # Mocking the random generator + mock_random_gen = MagicMock() + mock_random_new.return_value.read = mock_random_gen + + # Mocking the private and public key generation + mock_private_key = MagicMock() + mock_public_key = MagicMock() + mock_rsa_generate.return_value = mock_private_key + mock_private_key.publickey.return_value = mock_public_key + + # Mocking the exportKey function for both private and public keys + mock_private_key.exportKey.return_value = b'private_key_mock' + mock_public_key.exportKey.return_value = b'public_key_mock' + + # Send GET request to the /wallet/new route + response = self.client.get('/wallet/new') + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and data + self.assertEqual(response.status_code, 200) + self.assertEqual( + response_json['private_key'], + binascii.hexlify(b'private_key_mock').decode('ascii') + ) + self.assertEqual( + response_json['public_key'], + binascii.hexlify(b'public_key_mock').decode('ascii') + ) + + # Ensure that the appropriate methods were called + mock_rsa_generate.assert_called_once_with(1024, mock_random_gen) + mock_private_key.publickey.assert_called_once() + mock_private_key.exportKey.assert_called_once_with(format='DER') + mock_public_key.exportKey.assert_called_once_with(format='DER') + + @patch('src.app.routes.Transaction') # Mock the Transaction class + def test_generate_transaction(self, mock_transaction): + # Mock form data for the POST request + form_data = { + 'sender_address': 'sender_address_123', + 'sender_private_key': 'private_key_abc123', + 'recipient_address': 'recipient_address_456', + 'amount': '100' + } + + # Mock transaction instance and its methods + mock_transaction_instance = MagicMock() + mock_transaction.return_value = mock_transaction_instance + + # Mocking the to_dict and sign_transaction methods + mock_transaction_instance.to_dict.return_value = { + 'sender_address': form_data['sender_address'], + 'recipient_address': form_data['recipient_address'], + 'value': int(form_data['amount']) + } + mock_transaction_instance.sign_transaction.return_value = 'mock_signature' + + # Send POST request to /generate/transaction route + response = self.client.post('/generate/transaction', data=form_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 200) + self.assertEqual(response_json['transaction'], { + 'sender_address': form_data['sender_address'], + 'recipient_address': form_data['recipient_address'], + 'value': int(form_data['amount']) + }) + self.assertEqual(response_json['signature'], 'mock_signature') + + # Ensure that the Transaction class was instantiated with the correct parameters + mock_transaction.assert_called_once_with( + form_data['sender_address'], + form_data['sender_private_key'], + form_data['recipient_address'], + form_data['amount'] + ) + + # Ensure that the to_dict and sign_transaction methods were called + mock_transaction_instance.to_dict.assert_called_once() + mock_transaction_instance.sign_transaction.assert_called_once() + + +if __name__ == '__main__': + unittest.main() From dc12d319894ee01854f9b922d2a4e332ef86073a Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sat, 21 Sep 2024 11:37:49 +0200 Subject: [PATCH 04/21] ADDED Added Flask to req. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 11e4296..8866b94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ pycryptodome==3.20.0 +Flask==3.0.3 \ No newline at end of file From c8aa6d236db9369f1e7a47afe057f3899811c16e Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sat, 21 Sep 2024 12:13:38 +0200 Subject: [PATCH 05/21] ADDED Added docker support --- docker-compose.yaml | 22 ++++++++++++++++++++++ dockerfile | 28 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 docker-compose.yaml create mode 100644 dockerfile diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..08c2ac6 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,22 @@ +version: '0.1' + +services: + # Service for running tests + test: + build: . + command: ["pytest"] + environment: + FLASK_ENV: testing + + # Service for running the Flask app (depends on the test service) + web: + build: . + ports: + - "5000:5000" + environment: + FLASK_ENV: development + volumes: + - .:/app + depends_on: + test: + condition: service_completed_successfully # Only start web if tests passed \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..93d0f9c --- /dev/null +++ b/dockerfile @@ -0,0 +1,28 @@ +# Use an official Python runtime as a parent image +FROM python:3.12.3-slim + +# Set the working directory inside the container +WORKDIR /app + +# Copy the requirements file to the container +COPY requirements.txt /app/ + +# Install the Python dependencies +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir pytest pytest-cov + +# Add the src directory to the PYTHONPATH +ENV PYTHONPATH=/app/src + +# Copy the entire Flask app code to the container +COPY . /app + +# Set environment variables to make Flask work inside Docker +ENV FLASK_APP=src/main.py +ENV FLASK_RUN_HOST=0.0.0.0 + +# Expose port 5000 (Flask default port) +EXPOSE 5000 + +# Command to run the Flask app +CMD ["flask", "run"] From e831bc5e21472d9637b1fe8bf13a31d0c7254ca3 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sat, 21 Sep 2024 19:52:28 +0200 Subject: [PATCH 06/21] ADDED Added simple transaction validators --- src/app/routes.py | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/app/routes.py b/src/app/routes.py index 6721117..7ea7257 100644 --- a/src/app/routes.py +++ b/src/app/routes.py @@ -14,14 +14,29 @@ def home(): return jsonify({"message": "Welcome to Flask!"}) +wallets = {} + + @bp.route('/wallet/new', methods=['GET']) def new_wallet(): random_gen = Crypto.Random.new().read private_key = RSA.generate(1024, random_gen) public_key = private_key.publickey() + + public_key_str = binascii.hexlify( + public_key.exportKey(format='DER')).decode('ascii') + private_key_str = binascii.hexlify( + private_key.exportKey(format='DER')).decode('ascii') + + # Save wallet in a dictionary with initial balance of 0 + wallets[public_key_str] = { + 'private_key': private_key_str, + 'balance': 100 # Starting each wallet with a 100 + } + response = { - 'private_key': binascii.hexlify(private_key.exportKey(format='DER')).decode('ascii'), - 'public_key': binascii.hexlify(public_key.exportKey(format='DER')).decode('ascii') + 'private_key': private_key_str, + 'public_key': public_key_str } return jsonify(response), 200 @@ -29,16 +44,34 @@ def new_wallet(): @bp.route('/generate/transaction', methods=['POST']) def generate_transaction(): - sender_address = request.form['sender_address'] sender_private_key = request.form['sender_private_key'] recipient_address = request.form['recipient_address'] - value = request.form['amount'] + value = float(request.form['amount']) + # Check if sender and recipient exist + if sender_address not in wallets: + return jsonify({'error': 'Sender address does not exist.'}), 400 + + if recipient_address not in wallets: + return jsonify({'error': 'Recipient address does not exist.'}), 400 + + # Check if sender has enough balance + if wallets[sender_address]['balance'] < value: + return jsonify({'error': 'Insufficient balance.'}), 400 + + # Create transaction transaction = Transaction( sender_address, sender_private_key, recipient_address, value) - response = {'transaction': transaction.to_dict( - ), 'signature': transaction.sign_transaction()} + # Sign transaction + response = { + 'transaction': transaction.to_dict(), + 'signature': transaction.sign_transaction() + } + + # Subtract value from sender and add it to recipient + wallets[sender_address]['balance'] -= value + wallets[recipient_address]['balance'] += value return jsonify(response), 200 From d550f0e4f65e06f09764fac084f72d9369abdf59 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sat, 21 Sep 2024 19:52:47 +0200 Subject: [PATCH 07/21] UPDATE Updated the tests for transaction validators --- tests/test_routes.py | 52 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/test_routes.py b/tests/test_routes.py index 8bdade4..0c3ebc0 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -23,6 +23,8 @@ def test_home(self): self.assertEqual(response.status_code, 200) self.assertIn(b'Welcome to Flask!', response.data) + # Patch the wallets dictionary with an empty dictionary + @patch.dict('src.app.routes.wallets', {}, clear=True) @patch('src.app.routes.RSA.generate') # Mock RSA.generate @patch('src.app.routes.Crypto.Random.new') # Mock Crypto.Random.new().read def test_new_wallet(self, mock_random_new, mock_rsa_generate): @@ -57,20 +59,36 @@ def test_new_wallet(self, mock_random_new, mock_rsa_generate): binascii.hexlify(b'public_key_mock').decode('ascii') ) + # Check that the new wallet is stored in the wallets dictionary (from the module) + from src.app.routes import wallets # Import wallets after patching + self.assertIn(response_json['public_key'], wallets) + self.assertEqual(wallets[response_json['public_key']]['private_key'], + response_json['private_key']) + self.assertEqual(wallets[response_json['public_key']]['balance'], 100) + # Ensure that the appropriate methods were called mock_rsa_generate.assert_called_once_with(1024, mock_random_gen) mock_private_key.publickey.assert_called_once() mock_private_key.exportKey.assert_called_once_with(format='DER') mock_public_key.exportKey.assert_called_once_with(format='DER') + # Patch the wallets dictionary with an empty dictionary + @patch.dict('src.app.routes.wallets', {}, clear=True) @patch('src.app.routes.Transaction') # Mock the Transaction class def test_generate_transaction(self, mock_transaction): + # Add mock wallets with initial balances + from src.app.routes import wallets # Import wallets after patching + wallets['sender_address_123'] = { + 'private_key': 'private_key_abc123', 'balance': 200} + wallets['recipient_address_456'] = { + 'private_key': 'private_key_xyz456', 'balance': 50} + # Mock form data for the POST request form_data = { 'sender_address': 'sender_address_123', 'sender_private_key': 'private_key_abc123', 'recipient_address': 'recipient_address_456', - 'amount': '100' + 'amount': 100 } # Mock transaction instance and its methods @@ -112,6 +130,38 @@ def test_generate_transaction(self, mock_transaction): mock_transaction_instance.to_dict.assert_called_once() mock_transaction_instance.sign_transaction.assert_called_once() + # Check that balances were updated correctly + self.assertEqual(wallets['sender_address_123'] + ['balance'], 100) # 200 - 100 + self.assertEqual(wallets['recipient_address_456'] + ['balance'], 150) # 50 + 100 + + # Patch the wallets dictionary with an empty dictionary + @patch.dict('src.app.routes.wallets', {}, clear=True) + def test_generate_transaction_insufficient_balance(self): + # Add mock wallets with low balance for sender + from src.app.routes import wallets # Import wallets after patching + wallets['sender_address_123'] = { + 'private_key': 'private_key_abc123', 'balance': 50} + wallets['recipient_address_456'] = { + 'private_key': 'private_key_xyz456', 'balance': 50} + + # Mock form data for the POST request + form_data = { + 'sender_address': 'sender_address_123', + 'sender_private_key': 'private_key_abc123', + 'recipient_address': 'recipient_address_456', + 'amount': '100' # More than sender's balance + } + + # Send POST request to /generate/transaction route + response = self.client.post('/generate/transaction', data=form_data) + + # Validate the response status code and content + response_json = response.get_json() + self.assertEqual(response.status_code, 400) + self.assertEqual(response_json['error'], 'Insufficient balance.') + if __name__ == '__main__': unittest.main() From 74803cdf76e297c07adbda35dab30d18a0526cab Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sat, 21 Sep 2024 19:56:52 +0200 Subject: [PATCH 08/21] UPDATE Updated the route tests to cover the cases of walets not being found --- tests/test_routes.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_routes.py b/tests/test_routes.py index 0c3ebc0..39a5370 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -162,6 +162,60 @@ def test_generate_transaction_insufficient_balance(self): self.assertEqual(response.status_code, 400) self.assertEqual(response_json['error'], 'Insufficient balance.') + # Patch the wallets dictionary with an empty dictionary + @patch.dict('src.app.routes.wallets', {}, clear=True) + def test_generate_transaction_sender_address_not_exist(self): + # Add a mock wallet for the recipient only + from src.app.routes import wallets # Import wallets after patching + wallets['recipient_address_456'] = { + 'private_key': 'private_key_xyz456', 'balance': 50} + + # Mock form data for the POST request + form_data = { + 'sender_address': 'non_existent_sender', + 'sender_private_key': 'private_key_abc123', + 'recipient_address': 'recipient_address_456', + 'amount': '100' + } + + # Send POST request to /generate/transaction route + response = self.client.post('/generate/transaction', data=form_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 400) + self.assertEqual(response_json['error'], + 'Sender address does not exist.') + + # Patch the wallets dictionary with an empty dictionary + @patch.dict('src.app.routes.wallets', {}, clear=True) + def test_generate_transaction_recipient_address_not_exist(self): + # Add a mock wallet for the sender only + from src.app.routes import wallets # Import wallets after patching + wallets['sender_address_123'] = { + 'private_key': 'private_key_abc123', 'balance': 200} + + # Mock form data for the POST request + form_data = { + 'sender_address': 'sender_address_123', + 'sender_private_key': 'private_key_abc123', + 'recipient_address': 'non_existent_recipient', + 'amount': '100' + } + + # Send POST request to /generate/transaction route + response = self.client.post('/generate/transaction', data=form_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 400) + self.assertEqual(response_json['error'], + 'Recipient address does not exist.') + if __name__ == '__main__': unittest.main() From a72cbb52a71a882fba1760a83bb17f35f74b55bd Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sat, 21 Sep 2024 20:03:54 +0200 Subject: [PATCH 09/21] UPDATE Updated tests to improve the coverage --- src/transactions.py | 6 +++++- tests/test_transaction.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/transactions.py b/src/transactions.py index 0d65277..cd23df2 100644 --- a/src/transactions.py +++ b/src/transactions.py @@ -14,7 +14,11 @@ def __init__(self, sender_address, sender_private_key, recipient_address, value) self.value = value def __getattr__(self, attr): - return self.data[attr] + if attr in self.__dict__: + return self.__dict__[attr] + else: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{attr}'") def to_dict(self): return OrderedDict({'sender_address': self.sender_address, diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 07f5b9a..4485675 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -31,6 +31,18 @@ def test_init(self): self.recipient_address) self.assertEqual(self.transaction.value, self.value) + def test_getattr_existing_attribute(self): + # Test accessing an existing attribute via __getattr__ + self.assertEqual(self.transaction.sender_address, 'sender_address_123') + self.assertEqual(self.transaction.recipient_address, + 'recipient_address_456') + self.assertEqual(self.transaction.value, 100) + + def test_getattr_non_existing_attribute(self): + # Test accessing a non-existing attribute via __getattr__ + with self.assertRaises(AttributeError): + non_existent = self.transaction.non_existent_attr + def test_to_dict(self): # Test the to_dict method expected_dict = OrderedDict({ From 27958bcb2587468f714bbd2880258d04e54a471a Mon Sep 17 00:00:00 2001 From: Hamdi Date: Wed, 25 Sep 2024 21:25:21 +0200 Subject: [PATCH 10/21] UPDATE: Removed the transaction class and added the blockchain class --- src/blockchain.py | 251 ++++++++++++++++++++++++++++++++++++++++++++ src/transactions.py | 36 ------- 2 files changed, 251 insertions(+), 36 deletions(-) create mode 100644 src/blockchain.py delete mode 100644 src/transactions.py diff --git a/src/blockchain.py b/src/blockchain.py new file mode 100644 index 0000000..31504de --- /dev/null +++ b/src/blockchain.py @@ -0,0 +1,251 @@ +import binascii +import hashlib +import json +import requests +from time import time +from urllib.parse import urlparse +from uuid import uuid4 +from Crypto.PublicKey import RSA +from Crypto.Hash import SHA256 +from Crypto.Signature import PKCS1_v1_5 +from typing import OrderedDict + +MINING_SENDER = "THE BLOCKCHAIN" +MINING_REWARD = 1.0 +MINING_DIFFICULTY = 2 + + +class Blockchain: + + def __init__(self): + + self.transactions = [] + self.chain = [] + self.nodes = set() + # Generate random number to be used as node_id + self.node_id = str(uuid4()).replace('-', '') + # Create genesis block + self.create_block(nonce=0, previous_hash='00') + + def register_node(self, node_url): + """ + Add a new node to the list of nodes + """ + # Checking node_url has valid format + parsed_url = urlparse(url=node_url) + if parsed_url.netloc: + self.nodes.add(parsed_url.netloc) + elif parsed_url.path: + # Accepts an URL without scheme like '192.168.0.5:5000'. + self.nodes.add(parsed_url.path) + else: + raise ValueError('Invalid URL') + + def sign_transaction(self, sender_private_key, transaction): + """ + Sign transaction with private key + """ + private_key = RSA.importKey( + binascii.unhexlify(sender_private_key)) + signer = PKCS1_v1_5.new(private_key) + h = SHA256.new(str(transaction).encode('utf8')) + return binascii.hexlify(signer.sign(h)).decode('ascii') + + def verify_transaction_signature(self, sender_address, signature, transaction): + """ + Check that the provided signature corresponds to transaction + signed by the public key (sender_address) + """ + public_key = RSA.importKey( + extern_key=binascii.unhexlify(sender_address)) + verifier = PKCS1_v1_5.new(rsa_key=public_key) + h = SHA256.new(str(transaction).encode('utf-8')) + return verifier.verify(h, binascii.unhexlify(signature)) + + def submit_transaction(self, sender_address, sender_private_key, recipient_address, value): + """ + Add a transaction to transactions array if the signature verified + """ + transaction = OrderedDict({ + 'sender_address': sender_address, + 'recipient_address': recipient_address, + 'value': value + }) + + # If it's a mining reward, skip the signature process + if sender_address == MINING_SENDER: + self.transactions.append(transaction) + return len(self.chain) + 1 + + # Manages transactions from wallet to another wallet + else: + transaction_signature = self.sign_transaction( + sender_private_key=sender_private_key, + transaction=transaction + ) + transaction_verification = self.verify_transaction_signature( + sender_address=sender_address, + signature=transaction_signature, + transaction=transaction + ) + + if transaction_verification: + self.transactions.append(transaction) + return len(self.chain) + 1 + else: + return False + + def get_balance(self, address): + """ + Calculate the balance of a wallet by iterating over the blockchain. + """ + balance = 0.0 + + # Iterate through all blocks in the chain + for block in self.chain: + # Iterate through all transactions in each block + for transaction in block['transactions']: + # Check if the wallet is the sender + if transaction['sender_address'] == address: + balance -= transaction['value'] + + # Check if the wallet is the recipient + if transaction['recipient_address'] == address: + balance += transaction['value'] + + return balance + + def get_available_balance(self, address): + """ + Calculate the user's available balance, including both confirmed transactions (in blocks) + and pending transactions (in the transaction pool). + """ + confirmed_balance = self.get_balance( + address) # Balance from mined blocks + + # Now, adjust the balance by considering pending transactions + pending_debits = 0 + pending_credits = 0 + + for transaction in self.transactions: # Loop through pending transactions + if transaction['sender_address'] == address: + # Subtract pending debits + pending_debits += transaction['value'] + if transaction['recipient_address'] == address: + pending_credits += transaction['value'] # Add pending credits + + # Available balance is confirmed balance minus pending debits plus pending credits + available_balance = confirmed_balance - pending_debits + pending_credits + return available_balance + + def create_block(self, nonce, previous_hash): + """ + Add a block of transactions to the blockchain + """ + block = {'block_number': len(self.chain) + 1, + 'timestamp': time(), + 'transactions': self.transactions, + 'nonce': nonce, + 'previous_hash': previous_hash} + + # Reset the current list of transactions + self.transactions = [] + + self.chain.append(block) + return block + + def hash(self, block): + """ + Create a SHA-256 hash of a block + """ + # We must make sure that the Dictionary is Ordered, or we'll have inconsistent hashes + block_string = json.dumps(block, sort_keys=True).encode() + + return hashlib.sha256(block_string).hexdigest() + + def proof_of_work(self): + """ + Proof of work algorithm + """ + last_block = self.chain[-1] + last_hash = self.hash(last_block) + + nonce = 0 + while self.valid_proof(transactions=self.transactions, last_hash=last_hash, nonce=nonce) is False: + nonce += 1 + + return nonce + + def valid_proof(self, transactions, last_hash, nonce, difficulty=MINING_DIFFICULTY): + """ + Check if a hash value satisfies the mining conditions. This function is used within the proof_of_work function. + """ + guess = (str(transactions)+str(last_hash)+str(nonce)).encode() + guess_hash = hashlib.sha256(guess).hexdigest() + return guess_hash[:difficulty] == '0'*difficulty + + def valid_chain(self, chain): + """ + check if a bockchain is valid + """ + last_block = chain[0] + current_index = 1 + + while current_index < len(chain): + block = chain[current_index] + # print(last_block) + # print(block) + # print("\n-----------\n") + # Check that the hash of the block is correct + if block['previous_hash'] != self.hash(last_block): + return False + + # Check that the Proof of Work is correct + # Delete the reward transaction + transactions = block['transactions'][:-1] + # Need to make sure that the dictionary is ordered. Otherwise we'll get a different hash + transaction_elements = [ + 'sender_address', 'recipient_address', 'value'] + transactions = [OrderedDict( + (k, transaction[k]) for k in transaction_elements) for transaction in transactions] + + if not self.valid_proof(transactions=transactions, last_hash=block['previous_hash'], nonce=block['nonce']): + return False + + last_block = block + current_index += 1 + + return True + + def resolve_conflicts(self): + """ + Resolve conflicts between blockchain's nodes + by replacing our chain with the longest one in the network. + """ + neighbours = self.nodes + new_chain = None + + # We're only looking for chains longer than ours + max_length = len(self.chain) + + # Grab and verify the chains from all the nodes in our network + for node in neighbours: + print('http://' + node + '/chain') + response = requests.get( + url='http://' + node + '/chain', timeout=10) + + if response.status_code == 200: + length = response.json()['length'] + chain = response.json()['chain'] + + # Check if the length is longer and the chain is valid + if length > max_length and self.valid_chain(chain): + max_length = length + new_chain = chain + + # Replace our chain if we discovered a new, valid chain longer than ours + if new_chain: + self.chain = new_chain + return True + + return False diff --git a/src/transactions.py b/src/transactions.py deleted file mode 100644 index cd23df2..0000000 --- a/src/transactions.py +++ /dev/null @@ -1,36 +0,0 @@ -import binascii -from typing import OrderedDict -from Crypto.PublicKey import RSA -from Crypto.Hash import SHA -from Crypto.Signature import PKCS1_v1_5 - - -class Transaction: - - def __init__(self, sender_address, sender_private_key, recipient_address, value): - self.sender_address = sender_address - self.sender_private_key = sender_private_key - self.recipient_address = recipient_address - self.value = value - - def __getattr__(self, attr): - if attr in self.__dict__: - return self.__dict__[attr] - else: - raise AttributeError( - f"'{self.__class__.__name__}' object has no attribute '{attr}'") - - def to_dict(self): - return OrderedDict({'sender_address': self.sender_address, - 'recipient_address': self.recipient_address, - 'value': self.value}) - - def sign_transaction(self): - """ - Sign transaction with private key - """ - private_key = RSA.importKey( - binascii.unhexlify(self.sender_private_key)) - signer = PKCS1_v1_5.new(private_key) - h = SHA.new(str(self.to_dict()).encode('utf8')) - return binascii.hexlify(signer.sign(h)).decode('ascii') From bbfe7ddca6dc2c6d3f60480f1a3b0ce5ca7417e7 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Wed, 25 Sep 2024 21:26:01 +0200 Subject: [PATCH 11/21] UPDATE & ADD Updated the routes based on the new blockchain class & added more routes --- src/app/routes.py | 131 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 19 deletions(-) diff --git a/src/app/routes.py b/src/app/routes.py index 7ea7257..d6192e9 100644 --- a/src/app/routes.py +++ b/src/app/routes.py @@ -3,7 +3,7 @@ from Crypto.PublicKey import RSA from flask import Blueprint, jsonify, request -from transactions import Transaction +from blockchain import Blockchain, MINING_REWARD, MINING_SENDER bp = Blueprint('routes', __name__) @@ -15,12 +15,13 @@ def home(): wallets = {} +blockchain = Blockchain() -@bp.route('/wallet/new', methods=['GET']) +@bp.route('/wallet/new', methods=['POST']) def new_wallet(): random_gen = Crypto.Random.new().read - private_key = RSA.generate(1024, random_gen) + private_key = RSA.generate(3072, random_gen) public_key = private_key.publickey() public_key_str = binascii.hexlify( @@ -31,7 +32,7 @@ def new_wallet(): # Save wallet in a dictionary with initial balance of 0 wallets[public_key_str] = { 'private_key': private_key_str, - 'balance': 100 # Starting each wallet with a 100 + 'balance': 0.0 # Starting each wallet with a 0 } response = { @@ -42,36 +43,128 @@ def new_wallet(): return jsonify(response), 200 -@bp.route('/generate/transaction', methods=['POST']) -def generate_transaction(): +@bp.route('/transactions/new', methods=['POST']) +def new_transaction(): sender_address = request.form['sender_address'] sender_private_key = request.form['sender_private_key'] recipient_address = request.form['recipient_address'] - value = float(request.form['amount']) + amount = float(request.form['amount']) - # Check if sender and recipient exist + # Dynamically calculate the sender's balance + sender_balance = blockchain.get_available_balance(address=sender_address) + + # Check if sender and recipient exist (in the current context wallets dictionary) if sender_address not in wallets: return jsonify({'error': 'Sender address does not exist.'}), 400 if recipient_address not in wallets: return jsonify({'error': 'Recipient address does not exist.'}), 400 - # Check if sender has enough balance - if wallets[sender_address]['balance'] < value: + # Check if sender has enough balance dynamically + if sender_balance < amount: return jsonify({'error': 'Insufficient balance.'}), 400 - # Create transaction - transaction = Transaction( - sender_address, sender_private_key, recipient_address, value) + # Create a new Transaction + transaction_result = blockchain.submit_transaction( + sender_address, sender_private_key, recipient_address, amount + ) + + if not transaction_result: + response = {'message': 'Invalid Transaction!'} + return jsonify(response), 406 + else: + response = { + 'message': f'Transaction will be added to Block {str(transaction_result)}' + } + return jsonify(response), 201 + + +@bp.route('/transactions/get', methods=['GET']) +def get_transactions(): + # Get pending transactions from transactions pool + transactions = blockchain.transactions + + response = {'transactions': transactions} + return jsonify(response), 200 + - # Sign transaction +@bp.route('/chain', methods=['GET']) +def full_chain(): response = { - 'transaction': transaction.to_dict(), - 'signature': transaction.sign_transaction() + 'chain': blockchain.chain, + 'length': len(blockchain.chain), } + return jsonify(response), 200 + + +@bp.route('/mine', methods=['POST']) +def mine(): + miner_address = request.form['miner_address'] + + # We run the proof of work algorithm to get the next proof... + last_block = blockchain.chain[-1] + nonce = blockchain.proof_of_work() + + # Submit a reward transaction without a private key (since it's from the system) + blockchain.submit_transaction( + sender_address=MINING_SENDER, # System address for mining rewards + sender_private_key=None, # No private key needed for mining rewards + recipient_address=miner_address, # Miner receives the reward + value=MINING_REWARD # The reward for mining the block + ) + + # Forge the new Block by adding it to the chain + previous_hash = blockchain.hash(last_block) + block = blockchain.create_block(nonce, previous_hash) + + response = { + 'message': "New Block Forged", + 'block_number': block['block_number'], + 'transactions': block['transactions'], + 'nonce': block['nonce'], + 'previous_hash': block['previous_hash'], + 'miner_balance': blockchain.get_balance(miner_address) + } + return jsonify(response), 200 + + +@bp.route('/nodes/register', methods=['POST']) +def register_nodes(): + values = request.form + nodes = values.get('nodes').replace(" ", "").split(',') + + if nodes is None: + return "Error: Please supply a valid list of nodes", 400 + + for node in nodes: + blockchain.register_node(node) + + response = { + 'message': 'New nodes have been added', + 'total_nodes': [node for node in blockchain.nodes], + } + return jsonify(response), 201 + + +@bp.route('/nodes/resolve', methods=['GET']) +def consensus(): + replaced = blockchain.resolve_conflicts() + + if replaced: + response = { + 'message': 'Our chain was replaced', + 'new_chain': blockchain.chain + } + else: + response = { + 'message': 'Our chain is authoritative', + 'chain': blockchain.chain + } + return jsonify(response), 200 - # Subtract value from sender and add it to recipient - wallets[sender_address]['balance'] -= value - wallets[recipient_address]['balance'] += value +@bp.route('/nodes/get', methods=['GET']) +def get_nodes(): + nodes = list(blockchain.nodes) + response = {'nodes': nodes} return jsonify(response), 200 From a99996fc77c56d2a6543854c39e8d496256aa8d8 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Wed, 25 Sep 2024 21:26:20 +0200 Subject: [PATCH 12/21] UPDATE Updated the tests --- tests/test_blockchain.py | 218 ++++++++++++++++++++++++++++++++++ tests/test_routes.py | 243 ++++++++++++++++++++++++++------------ tests/test_transaction.py | 82 ------------- 3 files changed, 388 insertions(+), 155 deletions(-) create mode 100644 tests/test_blockchain.py delete mode 100644 tests/test_transaction.py diff --git a/tests/test_blockchain.py b/tests/test_blockchain.py new file mode 100644 index 0000000..40cc3c2 --- /dev/null +++ b/tests/test_blockchain.py @@ -0,0 +1,218 @@ +import os +import sys +import unittest +import binascii +from unittest.mock import MagicMock, patch +from uuid import uuid4 +from Crypto.PublicKey import RSA +import requests + +from src.blockchain import MINING_REWARD, Blockchain, MINING_SENDER # Assuming you save your class in a file called blockchain.py + + +sys.path.insert(0, os.path.abspath( + # to solve import issues in src + os.path.join(os.path.dirname(__file__), '../src'))) + +class TestBlockchain(unittest.TestCase): + + def setUp(self): + self.blockchain = Blockchain() + self.private_key = RSA.generate(1024) + self.public_key = self.private_key.publickey() + self.sender_private_key = binascii.hexlify(self.private_key.exportKey(format='DER')).decode('ascii') + self.sender_address = binascii.hexlify(self.public_key.exportKey(format='DER')).decode('ascii') + self.recipient_address = str(uuid4()).replace('-', '') + + def test_register_node_valid(self): + # Valid node registration + self.blockchain.register_node('http://192.168.0.1:5000') + self.assertIn('192.168.0.1:5000', self.blockchain.nodes) + + def test_register_node_invalid(self): + # Invalid node URLs + invalid_urls = [ + '????', + '####', + ] + + for url in invalid_urls: + with self.assertRaises(ValueError): + self.blockchain.register_node(url) + + def test_sign_transaction(self): + transaction = {'sender_address': self.sender_address, + 'recipient_address': self.recipient_address, 'value': 100} + signature = self.blockchain.sign_transaction( + self.sender_private_key, transaction) + self.assertIsInstance(signature, str) + self.assertTrue(len(signature) % 2 == 0) + + def test_verify_transaction_signature(self): + transaction = {'sender_address': self.sender_address, + 'recipient_address': self.recipient_address, 'value': 100} + signature = self.blockchain.sign_transaction( + self.sender_private_key, transaction) + self.assertTrue(self.blockchain.verify_transaction_signature( + self.sender_address, signature, transaction)) + + def test_invalid_transaction_signature(self): + transaction = {'sender_address': self.sender_address, + 'recipient_address': self.recipient_address, 'value': 100} + signature = self.blockchain.sign_transaction( + self.sender_private_key, transaction) + + # Modify transaction data to invalidate the signature + altered_transaction = {'sender_address': self.sender_address, + 'recipient_address': self.recipient_address, 'value': 200} + + self.assertFalse(self.blockchain.verify_transaction_signature( + self.sender_address, signature, altered_transaction)) + + def test_submit_transaction(self): + # Submit valid transaction + block_index = self.blockchain.submit_transaction( + self.sender_address, self.sender_private_key, self.recipient_address, 100) + self.assertEqual(block_index, 2) + + def test_submit_invalid_transaction(self): + # Invalid transaction (wrong signature) + with self.assertRaises(ValueError): + self.blockchain.submit_transaction( + self.sender_address, 'invalid_private_key', self.recipient_address, 100) + + def test_get_balance(self): + # Mining reward, expect no reduction in balance for the mining sender + self.blockchain.submit_transaction( + MINING_SENDER, None, self.recipient_address, MINING_REWARD) + self.blockchain.create_block(nonce=1, previous_hash='abcd') + + balance = self.blockchain.get_balance(self.recipient_address) + self.assertEqual(balance, MINING_REWARD) + + def test_get_available_balance(self): + # Submit a mining reward transaction (add it to pending transactions) + self.blockchain.submit_transaction( + MINING_SENDER, None, self.recipient_address, MINING_REWARD) + + # Mine a block to confirm the mining reward transaction + self.blockchain.create_block(nonce=12345, previous_hash='abcd') + + # Now, get the confirmed balance (after the mining reward transaction has been included in a block) + confirmed_balance = self.blockchain.get_balance(self.recipient_address) + + # Assert that the confirmed balance is equal to the mining reward + self.assertEqual(confirmed_balance, MINING_REWARD) + + # Submit a pending transaction to the recipient + self.blockchain.submit_transaction( + self.sender_address, self.sender_private_key, self.recipient_address, 50) + + # Available balance should be confirmed balance + pending transaction amount + available_balance = self.blockchain.get_available_balance(self.recipient_address) + + # The available balance should now be the mining reward + 50 + self.assertEqual(available_balance, confirmed_balance + 50) + + + def test_create_block(self): + previous_block = self.blockchain.chain[-1] + block = self.blockchain.create_block(nonce=12345, previous_hash='abcd') + + self.assertEqual(block['block_number'], 2) + self.assertEqual(block['previous_hash'], 'abcd') + self.assertEqual(len(block['transactions']), 0) # Transactions list should be reset + self.assertEqual(block['nonce'], 12345) + + def test_hash_block(self): + block = self.blockchain.chain[-1] + block_hash = self.blockchain.hash(block) + self.assertEqual(len(block_hash), 64) # SHA-256 hash length + + def test_proof_of_work(self): + nonce = self.blockchain.proof_of_work() + self.assertIsInstance(nonce, int) + + def test_valid_proof(self): + # Test that valid_proof correctly identifies a valid hash + last_block = self.blockchain.chain[-1] + last_hash = self.blockchain.hash(last_block) + nonce = self.blockchain.proof_of_work() + + self.assertTrue(self.blockchain.valid_proof( + self.blockchain.transactions, last_hash, nonce)) + + def test_invalid_proof(self): + # Test that valid_proof correctly identifies an invalid hash + self.assertFalse(self.blockchain.valid_proof( + self.blockchain.transactions, 'abcd', 12345)) + + def test_valid_chain(self): + # Create the first block (genesis block is already created) + previous_block = self.blockchain.chain[-1] + previous_hash = self.blockchain.hash(previous_block) # Correct previous hash + nonce = self.blockchain.proof_of_work() # Calculate valid nonce + + # Create a new block with valid nonce and previous hash + self.blockchain.create_block(nonce=nonce, previous_hash=previous_hash) + + # Test if the blockchain is valid + self.assertTrue(self.blockchain.valid_chain(self.blockchain.chain)) + + + def test_invalid_chain(self): + # Submit a transaction to ensure the block has a transaction + self.blockchain.submit_transaction( + self.sender_address, self.sender_private_key, self.recipient_address, 100) + + # Create the first block (genesis block is already created) + previous_block = self.blockchain.chain[-1] + previous_hash = self.blockchain.hash(previous_block) + nonce = self.blockchain.proof_of_work() + + # Create a new block that includes the transaction + self.blockchain.create_block(nonce=nonce, previous_hash=previous_hash) + + # Tamper with the chain by modifying the value of the transaction + self.blockchain.chain[1]['transactions'][0]['value'] = 999 # Tampering the chain + + # Test if the chain is invalid + self.assertFalse(self.blockchain.valid_chain(self.blockchain.chain)) + + + def test_resolve_conflicts(self): + # Create another blockchain instance with a longer chain + other_blockchain = Blockchain() + + # Mine multiple blocks to make other_blockchain longer than the current blockchain + for _ in range(2): # Creating 2 more blocks to make it longer + previous_block = other_blockchain.chain[-1] + previous_hash = other_blockchain.hash(previous_block) + nonce = other_blockchain.proof_of_work() + other_blockchain.create_block(nonce=nonce, previous_hash=previous_hash) + + # Simulate adding a node and that node providing the other blockchain + self.blockchain.nodes.add('localhost:5000') # Simulate another node + + # Mock the requests.get to simulate returning the longer chain from the other node + requests.get = unittest.mock.MagicMock(return_value=unittest.mock.Mock( + status_code=200, + json=lambda: { + 'length': len(other_blockchain.chain), + 'chain': other_blockchain.chain + } + )) + + # Now resolve conflicts; our blockchain should adopt the longer valid chain + conflict_resolved = self.blockchain.resolve_conflicts() + + # Assert that the conflict was resolved (i.e., our chain was replaced by the longer one) + self.assertTrue(conflict_resolved) + + # Assert that our blockchain's chain is now the same as the other blockchain's chain + self.assertEqual(self.blockchain.chain, other_blockchain.chain) + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_routes.py b/tests/test_routes.py index 39a5370..e547dbe 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -42,8 +42,8 @@ def test_new_wallet(self, mock_random_new, mock_rsa_generate): mock_private_key.exportKey.return_value = b'private_key_mock' mock_public_key.exportKey.return_value = b'public_key_mock' - # Send GET request to the /wallet/new route - response = self.client.get('/wallet/new') + # Send POST request to the /wallet/new route + response = self.client.post('/wallet/new') # Parse the JSON response response_json = response.get_json() @@ -64,158 +64,255 @@ def test_new_wallet(self, mock_random_new, mock_rsa_generate): self.assertIn(response_json['public_key'], wallets) self.assertEqual(wallets[response_json['public_key']]['private_key'], response_json['private_key']) - self.assertEqual(wallets[response_json['public_key']]['balance'], 100) + self.assertEqual(wallets[response_json['public_key']]['balance'], 0.0) # Ensure that the appropriate methods were called - mock_rsa_generate.assert_called_once_with(1024, mock_random_gen) + mock_rsa_generate.assert_called_once_with(3072, mock_random_gen) mock_private_key.publickey.assert_called_once() mock_private_key.exportKey.assert_called_once_with(format='DER') mock_public_key.exportKey.assert_called_once_with(format='DER') # Patch the wallets dictionary with an empty dictionary @patch.dict('src.app.routes.wallets', {}, clear=True) - @patch('src.app.routes.Transaction') # Mock the Transaction class - def test_generate_transaction(self, mock_transaction): - # Add mock wallets with initial balances + # Mock balance calculation + @patch('src.app.routes.blockchain.get_available_balance') + # Mock transaction submission + @patch('src.app.routes.blockchain.submit_transaction') + def test_new_transaction(self, mock_submit_transaction, mock_get_available_balance): + # Set up wallets with balances from src.app.routes import wallets # Import wallets after patching wallets['sender_address_123'] = { - 'private_key': 'private_key_abc123', 'balance': 200} + 'private_key': 'private_key_abc123', 'balance': 200.0} wallets['recipient_address_456'] = { - 'private_key': 'private_key_xyz456', 'balance': 50} + 'private_key': 'private_key_xyz456', 'balance': 50.0} # Mock form data for the POST request form_data = { 'sender_address': 'sender_address_123', 'sender_private_key': 'private_key_abc123', 'recipient_address': 'recipient_address_456', - 'amount': 100 + 'amount': '100.0' } - # Mock transaction instance and its methods - mock_transaction_instance = MagicMock() - mock_transaction.return_value = mock_transaction_instance + # Mock balance check to return enough balance + mock_get_available_balance.return_value = 200.0 - # Mocking the to_dict and sign_transaction methods - mock_transaction_instance.to_dict.return_value = { - 'sender_address': form_data['sender_address'], - 'recipient_address': form_data['recipient_address'], - 'value': int(form_data['amount']) - } - mock_transaction_instance.sign_transaction.return_value = 'mock_signature' + # Mock transaction submission success + mock_submit_transaction.return_value = 1 - # Send POST request to /generate/transaction route - response = self.client.post('/generate/transaction', data=form_data) + # Send POST request to /transactions/new route + response = self.client.post('/transactions/new', data=form_data) # Parse the JSON response response_json = response.get_json() # Validate the response status code and content - self.assertEqual(response.status_code, 200) - self.assertEqual(response_json['transaction'], { - 'sender_address': form_data['sender_address'], - 'recipient_address': form_data['recipient_address'], - 'value': int(form_data['amount']) - }) - self.assertEqual(response_json['signature'], 'mock_signature') - - # Ensure that the Transaction class was instantiated with the correct parameters - mock_transaction.assert_called_once_with( - form_data['sender_address'], - form_data['sender_private_key'], - form_data['recipient_address'], - form_data['amount'] + self.assertEqual(response.status_code, 201) + self.assertEqual(response_json['message'], + 'Transaction will be added to Block 1') + + # Ensure that the balance was checked correctly + mock_get_available_balance.assert_called_once_with( + address='sender_address_123') + + # Ensure that the submit_transaction method was called correctly + mock_submit_transaction.assert_called_once_with( + 'sender_address_123', + 'private_key_abc123', + 'recipient_address_456', + 100.0 ) - # Ensure that the to_dict and sign_transaction methods were called - mock_transaction_instance.to_dict.assert_called_once() - mock_transaction_instance.sign_transaction.assert_called_once() - - # Check that balances were updated correctly - self.assertEqual(wallets['sender_address_123'] - ['balance'], 100) # 200 - 100 - self.assertEqual(wallets['recipient_address_456'] - ['balance'], 150) # 50 + 100 - - # Patch the wallets dictionary with an empty dictionary @patch.dict('src.app.routes.wallets', {}, clear=True) - def test_generate_transaction_insufficient_balance(self): - # Add mock wallets with low balance for sender + @patch('src.app.routes.blockchain.get_available_balance') + def test_new_transaction_insufficient_balance(self, mock_get_available_balance): + # Set up wallets with low balance for the sender from src.app.routes import wallets # Import wallets after patching wallets['sender_address_123'] = { - 'private_key': 'private_key_abc123', 'balance': 50} + 'private_key': 'private_key_abc123', 'balance': 50.0} wallets['recipient_address_456'] = { - 'private_key': 'private_key_xyz456', 'balance': 50} + 'private_key': 'private_key_xyz456', 'balance': 50.0} # Mock form data for the POST request form_data = { 'sender_address': 'sender_address_123', 'sender_private_key': 'private_key_abc123', 'recipient_address': 'recipient_address_456', - 'amount': '100' # More than sender's balance + 'amount': '100.0' # More than sender's balance } - # Send POST request to /generate/transaction route - response = self.client.post('/generate/transaction', data=form_data) + # Mock balance check to return insufficient balance + mock_get_available_balance.return_value = 50.0 - # Validate the response status code and content + # Send POST request to /transactions/new route + response = self.client.post('/transactions/new', data=form_data) + + # Parse the JSON response response_json = response.get_json() self.assertEqual(response.status_code, 400) self.assertEqual(response_json['error'], 'Insufficient balance.') - # Patch the wallets dictionary with an empty dictionary + # Ensure the balance check was performed + mock_get_available_balance.assert_called_once_with( + address='sender_address_123') + @patch.dict('src.app.routes.wallets', {}, clear=True) - def test_generate_transaction_sender_address_not_exist(self): - # Add a mock wallet for the recipient only + def test_new_transaction_sender_address_not_exist(self): + # Set up only the recipient wallet from src.app.routes import wallets # Import wallets after patching wallets['recipient_address_456'] = { - 'private_key': 'private_key_xyz456', 'balance': 50} + 'private_key': 'private_key_xyz456', 'balance': 50.0} # Mock form data for the POST request form_data = { 'sender_address': 'non_existent_sender', 'sender_private_key': 'private_key_abc123', 'recipient_address': 'recipient_address_456', - 'amount': '100' + 'amount': '100.0' } - # Send POST request to /generate/transaction route - response = self.client.post('/generate/transaction', data=form_data) + # Send POST request to /transactions/new route + response = self.client.post('/transactions/new', data=form_data) # Parse the JSON response response_json = response.get_json() - - # Validate the response status code and content self.assertEqual(response.status_code, 400) self.assertEqual(response_json['error'], 'Sender address does not exist.') - # Patch the wallets dictionary with an empty dictionary @patch.dict('src.app.routes.wallets', {}, clear=True) - def test_generate_transaction_recipient_address_not_exist(self): - # Add a mock wallet for the sender only + def test_new_transaction_recipient_address_not_exist(self): + # Set up only the sender wallet from src.app.routes import wallets # Import wallets after patching wallets['sender_address_123'] = { - 'private_key': 'private_key_abc123', 'balance': 200} + 'private_key': 'private_key_abc123', 'balance': 200.0} # Mock form data for the POST request form_data = { 'sender_address': 'sender_address_123', 'sender_private_key': 'private_key_abc123', 'recipient_address': 'non_existent_recipient', - 'amount': '100' + 'amount': '100.0' } - # Send POST request to /generate/transaction route - response = self.client.post('/generate/transaction', data=form_data) + # Send POST request to /transactions/new route + response = self.client.post('/transactions/new', data=form_data) # Parse the JSON response response_json = response.get_json() - - # Validate the response status code and content self.assertEqual(response.status_code, 400) self.assertEqual(response_json['error'], 'Recipient address does not exist.') + @patch.dict('src.app.routes.wallets', {}, clear=True) + @patch('src.app.routes.blockchain.proof_of_work') + @patch('src.app.routes.blockchain.create_block') + @patch('src.app.routes.blockchain.submit_transaction') + def test_mine(self, mock_submit_transaction, mock_create_block, mock_proof_of_work): + # Set up wallet for the miner + from src.app.routes import wallets, blockchain # Import wallets after patching + wallets['miner_address_123'] = { + 'private_key': 'private_key_abc123', 'balance': 0.0} + + # Mock form data for the POST request + form_data = { + 'miner_address': 'miner_address_123' + } + + # Mock the proof_of_work and create_block methods + mock_proof_of_work.return_value = 123 # Mocked nonce + # Capture the real previous hash + previous_hash = blockchain.hash(blockchain.chain[-1]) + mock_create_block.return_value = { + 'block_number': 2, + 'transactions': [], + 'nonce': 123, + 'previous_hash': previous_hash + } + + # Mock the transaction submission + mock_submit_transaction.return_value = True + + # Send POST request to /mine route + response = self.client.post('/mine', data=form_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 200) + self.assertEqual(response_json['message'], 'New Block Forged') + self.assertEqual(response_json['block_number'], 2) + + # Ensure the submit_transaction method was called for the mining reward + mock_submit_transaction.assert_called_once_with( + sender_address='THE BLOCKCHAIN', + sender_private_key=None, + recipient_address='miner_address_123', + value=1 + ) + + # Ensure the proof_of_work and create_block methods were called + mock_proof_of_work.assert_called_once() + mock_create_block.assert_called_once_with(123, previous_hash) + + + @patch('src.app.routes.blockchain.proof_of_work') + @patch('src.app.routes.blockchain.create_block') + def test_mine_no_transactions(self, mock_create_block, mock_proof_of_work): + # Mock the proof_of_work and create_block methods + mock_proof_of_work.return_value = 123 # Mocked nonce + mock_create_block.return_value = { + 'block_number': 2, + 'transactions': [], # No transactions in the block + 'nonce': 123, + 'previous_hash': 'abcd1234' + } + + # Mock form data for the POST request with a valid miner address + form_data = { + 'miner_address': 'miner_address_123' + } + + # Send POST request to /mine route + response = self.client.post('/mine', data=form_data) + + # Validate the response status code and content + self.assertEqual(response.status_code, 200) + response_json = response.get_json() + self.assertEqual(response_json['message'], 'New Block Forged') + self.assertEqual(response_json['transactions'], []) + + @patch('src.app.routes.blockchain.resolve_conflicts') + def test_resolve_conflicts(self, mock_resolve_conflicts): + # Mock the resolve_conflicts method to return True (indicating the chain was replaced) + mock_resolve_conflicts.return_value = True + + # Simulate GET request to resolve node conflicts + response = self.client.get('/nodes/resolve') + + # Validate the response + self.assertEqual(response.status_code, 200) + response_json = response.get_json() + self.assertIn('message', response_json) + self.assertIn('new_chain', response_json) + self.assertEqual(response_json['message'], 'Our chain was replaced') + + def test_register_node(self): + # Mock form data for the POST request + form_data = { + 'nodes': 'http://localhost:5001, http://localhost:5002' + } + + # Simulate POST request to register new nodes + response = self.client.post('/nodes/register', data=form_data) + + # Validate the response status code and content + self.assertEqual(response.status_code, 201) + response_json = response.get_json() + self.assertIn('total_nodes', response_json) + self.assertEqual(len(response_json['total_nodes']), 2) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_transaction.py b/tests/test_transaction.py deleted file mode 100644 index 4485675..0000000 --- a/tests/test_transaction.py +++ /dev/null @@ -1,82 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock -from collections import OrderedDict -import binascii - -from src.transactions import Transaction - - -class TestTransaction(unittest.TestCase): - - def setUp(self): - # Sample data for testing - self.sender_address = 'sender_address_123' - self.sender_private_key = '73656e6465725f707269766174655f6b65795f686578' # hex-encoded - self.recipient_address = 'recipient_address_456' - self.value = 100 - - self.transaction = Transaction( - self.sender_address, - self.sender_private_key, - self.recipient_address, - self.value - ) - - def test_init(self): - # Test initialization - self.assertEqual(self.transaction.sender_address, self.sender_address) - self.assertEqual(self.transaction.sender_private_key, - self.sender_private_key) - self.assertEqual(self.transaction.recipient_address, - self.recipient_address) - self.assertEqual(self.transaction.value, self.value) - - def test_getattr_existing_attribute(self): - # Test accessing an existing attribute via __getattr__ - self.assertEqual(self.transaction.sender_address, 'sender_address_123') - self.assertEqual(self.transaction.recipient_address, - 'recipient_address_456') - self.assertEqual(self.transaction.value, 100) - - def test_getattr_non_existing_attribute(self): - # Test accessing a non-existing attribute via __getattr__ - with self.assertRaises(AttributeError): - non_existent = self.transaction.non_existent_attr - - def test_to_dict(self): - # Test the to_dict method - expected_dict = OrderedDict({ - 'sender_address': self.sender_address, - 'recipient_address': self.recipient_address, - 'value': self.value - }) - self.assertEqual(self.transaction.to_dict(), expected_dict) - - @patch('src.transactions.RSA.importKey') # Mock the private key - @patch('src.transactions.PKCS1_v1_5.new') # Mock the signer - def test_sign_transaction(self, mock_pkcs1, mock_import_key): - # Mock the private key import - mock_private_key = MagicMock() - mock_import_key.return_value = mock_private_key - - # Mock the signer object and the signing process - mock_signer = MagicMock() - mock_pkcs1.return_value = mock_signer - - # Mock the signature - mock_signature = b'signed_data' - mock_signer.sign.return_value = mock_signature - - # Call sign_transaction and verify the result - signature = self.transaction.sign_transaction() - self.assertEqual(signature, binascii.hexlify( - mock_signature).decode('ascii')) - - # Check that importKey and sign were called with the correct parameters - mock_import_key.assert_called_once_with( - binascii.unhexlify(self.sender_private_key)) - mock_signer.sign.assert_called_once() - - -if __name__ == '__main__': - unittest.main() From 584695a9ca4b33ddfc9178e88f46d9e82dd3c65b Mon Sep 17 00:00:00 2001 From: Hamdi Date: Wed, 25 Sep 2024 21:26:59 +0200 Subject: [PATCH 13/21] FIX Removed the version from the docker-compose.yaml file since it's obsolete --- docker-compose.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 08c2ac6..bec620e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '0.1' - services: # Service for running tests test: From b1d87dc7a69f12fd518a8f914e6cb26a1583f869 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Wed, 25 Sep 2024 21:27:24 +0200 Subject: [PATCH 14/21] UPDATE Updated the requirements.txt file --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8866b94..527181e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pycryptodome==3.20.0 -Flask==3.0.3 \ No newline at end of file +Flask==3.0.3 +requests==2.32.3 \ No newline at end of file From 8dfd03646634c0bcb1041ce867f8815a6d8d075e Mon Sep 17 00:00:00 2001 From: Hamdi Date: Wed, 25 Sep 2024 21:38:18 +0200 Subject: [PATCH 15/21] FIX Code fixes --- tests/test_blockchain.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_blockchain.py b/tests/test_blockchain.py index 40cc3c2..c070c14 100644 --- a/tests/test_blockchain.py +++ b/tests/test_blockchain.py @@ -2,7 +2,6 @@ import sys import unittest import binascii -from unittest.mock import MagicMock, patch from uuid import uuid4 from Crypto.PublicKey import RSA import requests @@ -18,7 +17,7 @@ class TestBlockchain(unittest.TestCase): def setUp(self): self.blockchain = Blockchain() - self.private_key = RSA.generate(1024) + self.private_key = RSA.generate(1024) # while RSA key sizes below 2048 bits are considered breakable, this is for test only. self.public_key = self.private_key.publickey() self.sender_private_key = binascii.hexlify(self.private_key.exportKey(format='DER')).decode('ascii') self.sender_address = binascii.hexlify(self.public_key.exportKey(format='DER')).decode('ascii') @@ -116,7 +115,6 @@ def test_get_available_balance(self): def test_create_block(self): - previous_block = self.blockchain.chain[-1] block = self.blockchain.create_block(nonce=12345, previous_hash='abcd') self.assertEqual(block['block_number'], 2) From b53d96952f5f942665dc3ae790fb567041904430 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Fri, 27 Sep 2024 20:39:27 +0200 Subject: [PATCH 16/21] ADD & UPDATE - Added OpenAI(Swagger) to document the api. - Updated the routes to use JSON instead of forms (for Consistency, flexibility, scalability). --- src/app/__init__.py | 11 +- src/app/routes.py | 55 ++++++-- src/app/static/openapi.yaml | 243 ++++++++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+), 10 deletions(-) create mode 100644 src/app/static/openapi.yaml diff --git a/src/app/__init__.py b/src/app/__init__.py index 0a7d735..01d12db 100644 --- a/src/app/__init__.py +++ b/src/app/__init__.py @@ -1,10 +1,19 @@ from flask import Flask +from flask_cors import CORS def create_app(): app = Flask(__name__) - from .routes import bp as routes_bp + from .routes import bp as routes_bp, swaggerui_blueprint, SWAGGER_URL + + # Register the routes blueprint app.register_blueprint(routes_bp) + # Register the Swagger UI blueprint + app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL) + + # Enable CORS for the entire app + CORS(app) + return app diff --git a/src/app/routes.py b/src/app/routes.py index d6192e9..9cc75cc 100644 --- a/src/app/routes.py +++ b/src/app/routes.py @@ -2,10 +2,24 @@ import Crypto from Crypto.PublicKey import RSA from flask import Blueprint, jsonify, request +from flask_swagger_ui import get_swaggerui_blueprint from blockchain import Blockchain, MINING_REWARD, MINING_SENDER +# Swagger UI setup +SWAGGER_URL = '/swagger' +API_URL = '/static/openapi.yaml' # OpenAPI specification location + +swaggerui_blueprint = get_swaggerui_blueprint( + SWAGGER_URL, # Swagger UI served at /swagger + API_URL, # OpenAPI specification served at /static/openapi.yaml + config={ # Swagger UI configuration + 'app_name': "ChainAlchemy API Documentation" + } +) + + bp = Blueprint('routes', __name__) @@ -45,10 +59,22 @@ def new_wallet(): @bp.route('/transactions/new', methods=['POST']) def new_transaction(): - sender_address = request.form['sender_address'] - sender_private_key = request.form['sender_private_key'] - recipient_address = request.form['recipient_address'] - amount = float(request.form['amount']) + # Parse JSON payload + data = request.get_json() + + # Ensure all required fields are present in the JSON body + required_fields = ['sender_address', 'sender_private_key', 'recipient_address', 'amount'] + for field in required_fields: + if field not in data: + return jsonify({'error': f"Missing required field: {field}"}), 400 + + sender_address = data['sender_address'] + sender_private_key = data['sender_private_key'] + recipient_address = data['recipient_address'] + try: + amount = float(data['amount']) + except ValueError: + return jsonify({'error': 'Amount must be a number.'}), 400 # Dynamically calculate the sender's balance sender_balance = blockchain.get_available_balance(address=sender_address) @@ -99,7 +125,14 @@ def full_chain(): @bp.route('/mine', methods=['POST']) def mine(): - miner_address = request.form['miner_address'] + # Parse JSON payload + data = request.get_json() + + # Ensure the 'miner_address' field is present + if 'miner_address' not in data: + return jsonify({'error': 'Missing required field: miner_address'}), 400 + + miner_address = data['miner_address'] # We run the proof of work algorithm to get the next proof... last_block = blockchain.chain[-1] @@ -130,12 +163,16 @@ def mine(): @bp.route('/nodes/register', methods=['POST']) def register_nodes(): - values = request.form - nodes = values.get('nodes').replace(" ", "").split(',') + # Parse JSON payload + data = request.get_json() + + # Ensure the 'nodes' field is present and valid + if 'nodes' not in data or not isinstance(data['nodes'], list) or len(data['nodes']) == 0: + return jsonify({'error': 'Missing or invalid required field: nodes (must be a non-empty list)'}), 400 - if nodes is None: - return "Error: Please supply a valid list of nodes", 400 + nodes = data['nodes'] + # Register each node in the blockchain for node in nodes: blockchain.register_node(node) diff --git a/src/app/static/openapi.yaml b/src/app/static/openapi.yaml new file mode 100644 index 0000000..2e12373 --- /dev/null +++ b/src/app/static/openapi.yaml @@ -0,0 +1,243 @@ +openapi: 3.0.0 +info: + title: ChainAlchemy API + description: This API allows interaction with the blockchain network, including wallet creation, transactions, mining, and node management. + version: 1.0.0 + +servers: + - url: http://localhost:5000 + +paths: + /: + get: + summary: Home route + description: A simple welcome message from the API. + responses: + '200': + description: A welcome message from Flask. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Welcome to Flask! + + /wallet/new: + post: + summary: Create a new wallet + description: Generates a new RSA key pair and stores the wallet in the system with an initial balance of 0. + responses: + '200': + description: The newly generated wallet, including private and public keys. + content: + application/json: + schema: + type: object + properties: + private_key: + type: string + description: The private key for the wallet. + public_key: + type: string + description: The public key for the wallet. + + /transactions/new: + post: + summary: Submit a new transaction + description: Submits a transaction between two wallets, transferring a specific amount from the sender to the recipient. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + sender_address: + type: string + description: The sender's public key. + sender_private_key: + type: string + description: The sender's private key. + recipient_address: + type: string + description: The recipient's public key. + amount: + type: number + description: The amount to be transferred. + responses: + '201': + description: The transaction will be added to the blockchain. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Transaction will be added to Block 1 + '400': + description: Error due to insufficient balance or invalid addresses. + '406': + description: Invalid transaction. + + /transactions/get: + get: + summary: Get all pending transactions + description: Retrieves the list of pending transactions from the transactions pool. + responses: + '200': + description: A list of pending transactions. + content: + application/json: + schema: + type: object + properties: + transactions: + type: array + items: + type: object + properties: + sender: + type: string + recipient: + type: string + amount: + type: number + + /chain: + get: + summary: Retrieve the full blockchain + description: Returns the full blockchain, including all blocks. + responses: + '200': + description: A JSON object containing the entire blockchain. + content: + application/json: + schema: + type: object + properties: + chain: + type: array + items: + type: object + properties: + block_number: + type: integer + transactions: + type: array + items: + type: object + nonce: + type: integer + previous_hash: + type: string + length: + type: integer + description: The number of blocks in the chain. + + /mine: + post: + summary: Mine a new block + description: Mines a new block using proof of work, adding it to the blockchain. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + miner_address: + type: string + description: The public key of the miner who will receive the mining reward. + responses: + '200': + description: A JSON object containing details of the newly forged block. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "New Block Forged" + block_number: + type: integer + transactions: + type: array + items: + type: object + nonce: + type: integer + previous_hash: + type: string + miner_balance: + type: number + + /nodes/register: + post: + summary: Register new nodes + description: Registers a list of new nodes to the blockchain network. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + nodes: + type: string + description: A comma-separated list of node addresses. + responses: + '201': + description: A confirmation message showing the total nodes added. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "New nodes have been added" + total_nodes: + type: array + items: + type: string + + /nodes/resolve: + get: + summary: Resolve conflicts between nodes + description: Initiates the consensus algorithm to resolve conflicts and ensure the blockchain is consistent across nodes. + responses: + '200': + description: The chain was either replaced or found authoritative. + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: "Our chain was replaced" + new_chain: + type: array + items: + type: object + + /nodes/get: + get: + summary: Get all registered nodes + description: Retrieves a list of all nodes registered in the blockchain network. + responses: + '200': + description: A list of all registered nodes. + content: + application/json: + schema: + type: object + properties: + nodes: + type: array + items: + type: string From 057fa23f3b79e60da89068a8f807b2e22fdb30d5 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Fri, 27 Sep 2024 20:39:47 +0200 Subject: [PATCH 17/21] UPDATE Updated the requirements.txt --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 527181e..6107cbb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ pycryptodome==3.20.0 Flask==3.0.3 -requests==2.32.3 \ No newline at end of file +requests==2.32.3 +flask-cors==5.0.0 +flask-swagger-ui==4.11.1 \ No newline at end of file From f992de2e9ce1e5f094a116e84261c4e9fac8f860 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Fri, 27 Sep 2024 20:40:32 +0200 Subject: [PATCH 18/21] UPDATE - Updated the routes tests to accept JSON instead of forms - Increased coverage --- tests/test_routes.py | 403 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 351 insertions(+), 52 deletions(-) diff --git a/tests/test_routes.py b/tests/test_routes.py index e547dbe..da12da3 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -86,12 +86,12 @@ def test_new_transaction(self, mock_submit_transaction, mock_get_available_balan wallets['recipient_address_456'] = { 'private_key': 'private_key_xyz456', 'balance': 50.0} - # Mock form data for the POST request - form_data = { + # Mock JSON data for the POST request + json_data = { 'sender_address': 'sender_address_123', 'sender_private_key': 'private_key_abc123', 'recipient_address': 'recipient_address_456', - 'amount': '100.0' + 'amount': 100.0 } # Mock balance check to return enough balance @@ -100,8 +100,8 @@ def test_new_transaction(self, mock_submit_transaction, mock_get_available_balan # Mock transaction submission success mock_submit_transaction.return_value = 1 - # Send POST request to /transactions/new route - response = self.client.post('/transactions/new', data=form_data) + # Send POST request to /transactions/new route with JSON data + response = self.client.post('/transactions/new', json=json_data) # Parse the JSON response response_json = response.get_json() @@ -123,6 +123,51 @@ def test_new_transaction(self, mock_submit_transaction, mock_get_available_balan 100.0 ) + @patch.dict('src.app.routes.wallets', {}, clear=True) + @patch('src.app.routes.blockchain.get_available_balance') + @patch('src.app.routes.blockchain.submit_transaction') + def test_new_transaction_missing_fields(self, mock_submit_transaction, mock_get_available_balance): + # Test missing fields + json_data = { + 'sender_address': 'sender_address_123', + 'recipient_address': 'recipient_address_456', + # 'amount' and 'sender_private_key' are missing + } + + # Send POST request with missing fields + response = self.client.post('/transactions/new', json=json_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and error message + self.assertEqual(response.status_code, 400) + self.assertIn('error', response_json) + self.assertEqual(response_json['error'], 'Missing required field: sender_private_key') + + @patch.dict('src.app.routes.wallets', {}, clear=True) + @patch('src.app.routes.blockchain.get_available_balance') + @patch('src.app.routes.blockchain.submit_transaction') + def test_new_transaction_invalid_amount(self, mock_submit_transaction, mock_get_available_balance): + # Test invalid amount + json_data = { + 'sender_address': 'sender_address_123', + 'sender_private_key': 'private_key_abc123', + 'recipient_address': 'recipient_address_456', + 'amount': 'invalid_amount' # Invalid non-numeric amount + } + + # Send POST request with invalid amount + response = self.client.post('/transactions/new', json=json_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and error message + self.assertEqual(response.status_code, 400) + self.assertIn('error', response_json) + self.assertEqual(response_json['error'], 'Amount must be a number.') + @patch.dict('src.app.routes.wallets', {}, clear=True) @patch('src.app.routes.blockchain.get_available_balance') def test_new_transaction_insufficient_balance(self, mock_get_available_balance): @@ -133,19 +178,19 @@ def test_new_transaction_insufficient_balance(self, mock_get_available_balance): wallets['recipient_address_456'] = { 'private_key': 'private_key_xyz456', 'balance': 50.0} - # Mock form data for the POST request - form_data = { + # Mock JSON data for the POST request + json_data = { 'sender_address': 'sender_address_123', 'sender_private_key': 'private_key_abc123', 'recipient_address': 'recipient_address_456', - 'amount': '100.0' # More than sender's balance + 'amount': 100.0 # More than sender's balance } # Mock balance check to return insufficient balance mock_get_available_balance.return_value = 50.0 - # Send POST request to /transactions/new route - response = self.client.post('/transactions/new', data=form_data) + # Send POST request to /transactions/new route with JSON data + response = self.client.post('/transactions/new', json=json_data) # Parse the JSON response response_json = response.get_json() @@ -163,16 +208,16 @@ def test_new_transaction_sender_address_not_exist(self): wallets['recipient_address_456'] = { 'private_key': 'private_key_xyz456', 'balance': 50.0} - # Mock form data for the POST request - form_data = { + # Mock JSON data for the POST request + json_data = { 'sender_address': 'non_existent_sender', 'sender_private_key': 'private_key_abc123', 'recipient_address': 'recipient_address_456', - 'amount': '100.0' + 'amount': 100.0 } - # Send POST request to /transactions/new route - response = self.client.post('/transactions/new', data=form_data) + # Send POST request to /transactions/new route with JSON data + response = self.client.post('/transactions/new', json=json_data) # Parse the JSON response response_json = response.get_json() @@ -187,16 +232,16 @@ def test_new_transaction_recipient_address_not_exist(self): wallets['sender_address_123'] = { 'private_key': 'private_key_abc123', 'balance': 200.0} - # Mock form data for the POST request - form_data = { + # Mock JSON data for the POST request + json_data = { 'sender_address': 'sender_address_123', 'sender_private_key': 'private_key_abc123', 'recipient_address': 'non_existent_recipient', - 'amount': '100.0' + 'amount': 100.0 } - # Send POST request to /transactions/new route - response = self.client.post('/transactions/new', data=form_data) + # Send POST request to /transactions/new route with JSON data + response = self.client.post('/transactions/new', json=json_data) # Parse the JSON response response_json = response.get_json() @@ -204,6 +249,42 @@ def test_new_transaction_recipient_address_not_exist(self): self.assertEqual(response_json['error'], 'Recipient address does not exist.') + @patch.dict('src.app.routes.wallets', {}, clear=True) + @patch('src.app.routes.blockchain.get_available_balance') + @patch('src.app.routes.blockchain.submit_transaction') + def test_new_transaction_invalid_transaction(self, mock_submit_transaction, mock_get_available_balance): + # Set up wallets with balances + from src.app.routes import wallets # Import wallets after patching + wallets['sender_address_123'] = { + 'private_key': 'private_key_abc123', 'balance': 200.0} + wallets['recipient_address_456'] = { + 'private_key': 'private_key_xyz456', 'balance': 50.0} + + # Mock balance check to return enough balance + mock_get_available_balance.return_value = 200.0 + + # Mock transaction submission to return None (indicating invalid transaction) + mock_submit_transaction.return_value = None + + # Mock JSON data for the POST request + json_data = { + 'sender_address': 'sender_address_123', + 'sender_private_key': 'private_key_abc123', + 'recipient_address': 'recipient_address_456', + 'amount': 100.0 + } + + # Send POST request to /transactions/new route with JSON data + response = self.client.post('/transactions/new', json=json_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 406) + self.assertIn('message', response_json) + self.assertEqual(response_json['message'], 'Invalid Transaction!') + @patch.dict('src.app.routes.wallets', {}, clear=True) @patch('src.app.routes.blockchain.proof_of_work') @patch('src.app.routes.blockchain.create_block') @@ -214,8 +295,8 @@ def test_mine(self, mock_submit_transaction, mock_create_block, mock_proof_of_wo wallets['miner_address_123'] = { 'private_key': 'private_key_abc123', 'balance': 0.0} - # Mock form data for the POST request - form_data = { + # Mock JSON data for the POST request + json_data = { 'miner_address': 'miner_address_123' } @@ -233,8 +314,8 @@ def test_mine(self, mock_submit_transaction, mock_create_block, mock_proof_of_wo # Mock the transaction submission mock_submit_transaction.return_value = True - # Send POST request to /mine route - response = self.client.post('/mine', data=form_data) + # Send POST request to /mine route with JSON data + response = self.client.post('/mine', json=json_data) # Parse the JSON response response_json = response.get_json() @@ -256,32 +337,91 @@ def test_mine(self, mock_submit_transaction, mock_create_block, mock_proof_of_wo mock_proof_of_work.assert_called_once() mock_create_block.assert_called_once_with(123, previous_hash) - + @patch.dict('src.app.routes.wallets', {}, clear=True) @patch('src.app.routes.blockchain.proof_of_work') @patch('src.app.routes.blockchain.create_block') - def test_mine_no_transactions(self, mock_create_block, mock_proof_of_work): - # Mock the proof_of_work and create_block methods - mock_proof_of_work.return_value = 123 # Mocked nonce - mock_create_block.return_value = { - 'block_number': 2, - 'transactions': [], # No transactions in the block - 'nonce': 123, - 'previous_hash': 'abcd1234' - } + @patch('src.app.routes.blockchain.submit_transaction') + def test_mine_missing_miner_address(self, mock_submit_transaction, mock_create_block, mock_proof_of_work): + # Test missing miner_address in the JSON payload + json_data = {} # No miner_address provided - # Mock form data for the POST request with a valid miner address - form_data = { - 'miner_address': 'miner_address_123' + # Send POST request to /mine route with missing miner_address + response = self.client.post('/mine', json=json_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and error message + self.assertEqual(response.status_code, 400) + self.assertIn('error', response_json) + self.assertEqual(response_json['error'], 'Missing required field: miner_address') + + def test_register_node(self): + # Mock JSON data for the POST request + json_data = { + 'nodes': ['http://localhost:5001', 'http://localhost:5002'] } - # Send POST request to /mine route - response = self.client.post('/mine', data=form_data) + # Simulate POST request to register new nodes + response = self.client.post('/nodes/register', json=json_data) # Validate the response status code and content - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 201) response_json = response.get_json() - self.assertEqual(response_json['message'], 'New Block Forged') - self.assertEqual(response_json['transactions'], []) + self.assertIn('total_nodes', response_json) + self.assertEqual(len(response_json['total_nodes']), 2) + + @patch('src.app.routes.blockchain.register_node') # Mock the register_node method + def test_register_nodes_missing_field(self, mock_register_node): + # Test when 'nodes' field is missing in the JSON payload + json_data = {} # No 'nodes' field provided + + # Send POST request to /nodes/register route with missing 'nodes' + response = self.client.post('/nodes/register', json=json_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and error message + self.assertEqual(response.status_code, 400) + self.assertIn('error', response_json) + self.assertEqual(response_json['error'], 'Missing or invalid required field: nodes (must be a non-empty list)') + + @patch('src.app.routes.blockchain.register_node') # Mock the register_node method + def test_register_nodes_invalid_field_type(self, mock_register_node): + # Test when 'nodes' field is not a list + json_data = { + 'nodes': 'not_a_list' # Invalid type + } + + # Send POST request to /nodes/register route with invalid 'nodes' field type + response = self.client.post('/nodes/register', json=json_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and error message + self.assertEqual(response.status_code, 400) + self.assertIn('error', response_json) + self.assertEqual(response_json['error'], 'Missing or invalid required field: nodes (must be a non-empty list)') + + @patch('src.app.routes.blockchain.register_node') # Mock the register_node method + def test_register_nodes_empty_list(self, mock_register_node): + # Test when 'nodes' field is an empty list + json_data = { + 'nodes': [] # Empty list + } + + # Send POST request to /nodes/register route with empty 'nodes' field + response = self.client.post('/nodes/register', json=json_data) + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and error message + self.assertEqual(response.status_code, 400) + self.assertIn('error', response_json) + self.assertEqual(response_json['error'], 'Missing or invalid required field: nodes (must be a non-empty list)') @patch('src.app.routes.blockchain.resolve_conflicts') def test_resolve_conflicts(self, mock_resolve_conflicts): @@ -298,21 +438,180 @@ def test_resolve_conflicts(self, mock_resolve_conflicts): self.assertIn('new_chain', response_json) self.assertEqual(response_json['message'], 'Our chain was replaced') - def test_register_node(self): - # Mock form data for the POST request - form_data = { - 'nodes': 'http://localhost:5001, http://localhost:5002' - } + @patch('src.app.routes.blockchain.resolve_conflicts') # Mock resolve_conflicts method + @patch('src.app.routes.blockchain.chain', new_callable=list) # Mock blockchain.chain + def test_consensus_authoritative_chain(self, mock_chain, mock_resolve_conflicts): + # Simulate the chain being authoritative (not replaced) + mock_resolve_conflicts.return_value = False + + # Simulate a blockchain with a few blocks + mock_chain.extend([ + { + 'block_number': 1, + 'transactions': [], + 'nonce': 1234, + 'previous_hash': 'abcd1234' + }, + { + 'block_number': 2, + 'transactions': [{'sender': 'sender_1', 'recipient': 'recipient_1', 'amount': 50.0}], + 'nonce': 5678, + 'previous_hash': 'abcd5678' + } + ]) + + # Send GET request to /nodes/resolve route + response = self.client.get('/nodes/resolve') - # Simulate POST request to register new nodes - response = self.client.post('/nodes/register', data=form_data) + # Parse the JSON response + response_json = response.get_json() # Validate the response status code and content - self.assertEqual(response.status_code, 201) + self.assertEqual(response.status_code, 200) + self.assertIn('message', response_json) + self.assertEqual(response_json['message'], 'Our chain is authoritative') + + # Check that the chain is returned correctly in the response + self.assertIn('chain', response_json) + self.assertEqual(len(response_json['chain']), 2) + self.assertEqual(response_json['chain'][0]['block_number'], 1) + self.assertEqual(response_json['chain'][1]['block_number'], 2) + + @patch('src.app.routes.blockchain.nodes', new_callable=set) + def test_get_nodes(self, mock_nodes): + # Simulate some nodes in the blockchain + mock_nodes.update({'http://localhost:5001', 'http://localhost:5002'}) + + # Send GET request to /nodes/get route + response = self.client.get('/nodes/get') + + # Parse the JSON response response_json = response.get_json() - self.assertIn('total_nodes', response_json) - self.assertEqual(len(response_json['total_nodes']), 2) + # Validate the response status code and content + self.assertEqual(response.status_code, 200) + self.assertIn('nodes', response_json) + self.assertEqual(len(response_json['nodes']), 2) + self.assertIn('http://localhost:5001', response_json['nodes']) + self.assertIn('http://localhost:5002', response_json['nodes']) + + @patch('src.app.routes.blockchain.nodes', new_callable=set) + def test_get_nodes_empty(self, mock_nodes): + # Simulate an empty set of nodes + mock_nodes.clear() + + # Send GET request to /nodes/get route + response = self.client.get('/nodes/get') + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 200) + self.assertIn('nodes', response_json) + self.assertEqual(len(response_json['nodes']), 0) # No nodes in the blockchain + + @patch('src.app.routes.blockchain.transactions', new_callable=list) + def test_get_transactions(self, mock_transactions): + # Simulate some transactions in the blockchain + mock_transactions.extend([ + {'sender': 'sender_1', 'recipient': 'recipient_1', 'amount': 50.0}, + {'sender': 'sender_2', 'recipient': 'recipient_2', 'amount': 100.0} + ]) + + # Send GET request to /transactions/get route + response = self.client.get('/transactions/get') + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 200) + self.assertIn('transactions', response_json) + self.assertEqual(len(response_json['transactions']), 2) + + # Validate transaction details + self.assertEqual(response_json['transactions'][0]['sender'], 'sender_1') + self.assertEqual(response_json['transactions'][0]['recipient'], 'recipient_1') + self.assertEqual(response_json['transactions'][0]['amount'], 50.0) + + self.assertEqual(response_json['transactions'][1]['sender'], 'sender_2') + self.assertEqual(response_json['transactions'][1]['recipient'], 'recipient_2') + self.assertEqual(response_json['transactions'][1]['amount'], 100.0) + + @patch('src.app.routes.blockchain.transactions', new_callable=list) + def test_get_transactions_empty(self, mock_transactions): + # Simulate no transactions in the blockchain + mock_transactions.clear() + + # Send GET request to /transactions/get route + response = self.client.get('/transactions/get') + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 200) + self.assertIn('transactions', response_json) + self.assertEqual(len(response_json['transactions']), 0) # No transactions in the pool + + @patch('src.app.routes.blockchain.chain', new_callable=list) + def test_full_chain(self, mock_chain): + # Simulate a blockchain with a few blocks + mock_chain.extend([ + { + 'block_number': 1, + 'transactions': [], + 'nonce': 1234, + 'previous_hash': 'abcd1234' + }, + { + 'block_number': 2, + 'transactions': [{'sender': 'sender_1', 'recipient': 'recipient_1', 'amount': 50.0}], + 'nonce': 5678, + 'previous_hash': 'abcd5678' + } + ]) + + # Send GET request to /chain route + response = self.client.get('/chain') + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 200) + self.assertIn('chain', response_json) + self.assertEqual(len(response_json['chain']), 2) + self.assertEqual(response_json['length'], 2) + + # Validate block details + self.assertEqual(response_json['chain'][0]['block_number'], 1) + self.assertEqual(response_json['chain'][1]['block_number'], 2) + self.assertEqual(response_json['chain'][1]['transactions'][0]['sender'], 'sender_1') + + @patch('src.app.routes.blockchain.chain', new_callable=list) + def test_full_chain_empty(self, mock_chain): + # Simulate an empty blockchain + mock_chain.clear() + + # Send GET request to /chain route + response = self.client.get('/chain') + + # Parse the JSON response + response_json = response.get_json() + + # Validate the response status code and content + self.assertEqual(response.status_code, 200) + self.assertIn('chain', response_json) + self.assertEqual(len(response_json['chain']), 0) + self.assertEqual(response_json['length'], 0) + + def test_swagger_ui(self): + # Test the Swagger UI route and follow redirects + response = self.client.get('/swagger', follow_redirects=True) + self.assertEqual(response.status_code, 200) + self.assertIn(b"ChainAlchemy API Documentation", response.data) if __name__ == '__main__': unittest.main() From a212e72b548dd163798bb16feeac307b46284cd5 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sun, 22 Dec 2024 22:01:42 +0100 Subject: [PATCH 19/21] FIX Fixed some linters warnings --- tests/test_blockchain.py | 38 +++++++++++++------------- tests/test_routes.py | 58 +++++++++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/tests/test_blockchain.py b/tests/test_blockchain.py index c070c14..c49c2ee 100644 --- a/tests/test_blockchain.py +++ b/tests/test_blockchain.py @@ -6,21 +6,23 @@ from Crypto.PublicKey import RSA import requests -from src.blockchain import MINING_REWARD, Blockchain, MINING_SENDER # Assuming you save your class in a file called blockchain.py - - sys.path.insert(0, os.path.abspath( # to solve import issues in src os.path.join(os.path.dirname(__file__), '../src'))) +from src.blockchain import MINING_REWARD, Blockchain, MINING_SENDER + class TestBlockchain(unittest.TestCase): def setUp(self): self.blockchain = Blockchain() - self.private_key = RSA.generate(1024) # while RSA key sizes below 2048 bits are considered breakable, this is for test only. + # while RSA key sizes below 2048 bits are considered breakable, this is for test only. + self.private_key = RSA.generate(1024) self.public_key = self.private_key.publickey() - self.sender_private_key = binascii.hexlify(self.private_key.exportKey(format='DER')).decode('ascii') - self.sender_address = binascii.hexlify(self.public_key.exportKey(format='DER')).decode('ascii') + self.sender_private_key = binascii.hexlify( + self.private_key.exportKey(format='DER')).decode('ascii') + self.sender_address = binascii.hexlify( + self.public_key.exportKey(format='DER')).decode('ascii') self.recipient_address = str(uuid4()).replace('-', '') def test_register_node_valid(self): @@ -34,7 +36,7 @@ def test_register_node_invalid(self): '????', '####', ] - + for url in invalid_urls: with self.assertRaises(ValueError): self.blockchain.register_node(url) @@ -108,18 +110,19 @@ def test_get_available_balance(self): self.sender_address, self.sender_private_key, self.recipient_address, 50) # Available balance should be confirmed balance + pending transaction amount - available_balance = self.blockchain.get_available_balance(self.recipient_address) + available_balance = self.blockchain.get_available_balance( + self.recipient_address) # The available balance should now be the mining reward + 50 self.assertEqual(available_balance, confirmed_balance + 50) - def test_create_block(self): block = self.blockchain.create_block(nonce=12345, previous_hash='abcd') self.assertEqual(block['block_number'], 2) self.assertEqual(block['previous_hash'], 'abcd') - self.assertEqual(len(block['transactions']), 0) # Transactions list should be reset + # Transactions list should be reset + self.assertEqual(len(block['transactions']), 0) self.assertEqual(block['nonce'], 12345) def test_hash_block(self): @@ -148,7 +151,8 @@ def test_invalid_proof(self): def test_valid_chain(self): # Create the first block (genesis block is already created) previous_block = self.blockchain.chain[-1] - previous_hash = self.blockchain.hash(previous_block) # Correct previous hash + previous_hash = self.blockchain.hash( + previous_block) # Correct previous hash nonce = self.blockchain.proof_of_work() # Calculate valid nonce # Create a new block with valid nonce and previous hash @@ -157,7 +161,6 @@ def test_valid_chain(self): # Test if the blockchain is valid self.assertTrue(self.blockchain.valid_chain(self.blockchain.chain)) - def test_invalid_chain(self): # Submit a transaction to ensure the block has a transaction self.blockchain.submit_transaction( @@ -172,12 +175,12 @@ def test_invalid_chain(self): self.blockchain.create_block(nonce=nonce, previous_hash=previous_hash) # Tamper with the chain by modifying the value of the transaction - self.blockchain.chain[1]['transactions'][0]['value'] = 999 # Tampering the chain + # Tampering the chain + self.blockchain.chain[1]['transactions'][0]['value'] = 999 # Test if the chain is invalid self.assertFalse(self.blockchain.valid_chain(self.blockchain.chain)) - def test_resolve_conflicts(self): # Create another blockchain instance with a longer chain other_blockchain = Blockchain() @@ -187,7 +190,8 @@ def test_resolve_conflicts(self): previous_block = other_blockchain.chain[-1] previous_hash = other_blockchain.hash(previous_block) nonce = other_blockchain.proof_of_work() - other_blockchain.create_block(nonce=nonce, previous_hash=previous_hash) + other_blockchain.create_block( + nonce=nonce, previous_hash=previous_hash) # Simulate adding a node and that node providing the other blockchain self.blockchain.nodes.add('localhost:5000') # Simulate another node @@ -210,7 +214,5 @@ def test_resolve_conflicts(self): # Assert that our blockchain's chain is now the same as the other blockchain's chain self.assertEqual(self.blockchain.chain, other_blockchain.chain) - - if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_routes.py b/tests/test_routes.py index da12da3..229244d 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -143,7 +143,8 @@ def test_new_transaction_missing_fields(self, mock_submit_transaction, mock_get_ # Validate the response status code and error message self.assertEqual(response.status_code, 400) self.assertIn('error', response_json) - self.assertEqual(response_json['error'], 'Missing required field: sender_private_key') + self.assertEqual( + response_json['error'], 'Missing required field: sender_private_key') @patch.dict('src.app.routes.wallets', {}, clear=True) @patch('src.app.routes.blockchain.get_available_balance') @@ -354,7 +355,8 @@ def test_mine_missing_miner_address(self, mock_submit_transaction, mock_create_b # Validate the response status code and error message self.assertEqual(response.status_code, 400) self.assertIn('error', response_json) - self.assertEqual(response_json['error'], 'Missing required field: miner_address') + self.assertEqual(response_json['error'], + 'Missing required field: miner_address') def test_register_node(self): # Mock JSON data for the POST request @@ -371,7 +373,8 @@ def test_register_node(self): self.assertIn('total_nodes', response_json) self.assertEqual(len(response_json['total_nodes']), 2) - @patch('src.app.routes.blockchain.register_node') # Mock the register_node method + # Mock the register_node method + @patch('src.app.routes.blockchain.register_node') def test_register_nodes_missing_field(self, mock_register_node): # Test when 'nodes' field is missing in the JSON payload json_data = {} # No 'nodes' field provided @@ -385,9 +388,11 @@ def test_register_nodes_missing_field(self, mock_register_node): # Validate the response status code and error message self.assertEqual(response.status_code, 400) self.assertIn('error', response_json) - self.assertEqual(response_json['error'], 'Missing or invalid required field: nodes (must be a non-empty list)') + self.assertEqual( + response_json['error'], 'Missing or invalid required field: nodes (must be a non-empty list)') - @patch('src.app.routes.blockchain.register_node') # Mock the register_node method + # Mock the register_node method + @patch('src.app.routes.blockchain.register_node') def test_register_nodes_invalid_field_type(self, mock_register_node): # Test when 'nodes' field is not a list json_data = { @@ -403,9 +408,11 @@ def test_register_nodes_invalid_field_type(self, mock_register_node): # Validate the response status code and error message self.assertEqual(response.status_code, 400) self.assertIn('error', response_json) - self.assertEqual(response_json['error'], 'Missing or invalid required field: nodes (must be a non-empty list)') + self.assertEqual( + response_json['error'], 'Missing or invalid required field: nodes (must be a non-empty list)') - @patch('src.app.routes.blockchain.register_node') # Mock the register_node method + # Mock the register_node method + @patch('src.app.routes.blockchain.register_node') def test_register_nodes_empty_list(self, mock_register_node): # Test when 'nodes' field is an empty list json_data = { @@ -421,7 +428,8 @@ def test_register_nodes_empty_list(self, mock_register_node): # Validate the response status code and error message self.assertEqual(response.status_code, 400) self.assertIn('error', response_json) - self.assertEqual(response_json['error'], 'Missing or invalid required field: nodes (must be a non-empty list)') + self.assertEqual( + response_json['error'], 'Missing or invalid required field: nodes (must be a non-empty list)') @patch('src.app.routes.blockchain.resolve_conflicts') def test_resolve_conflicts(self, mock_resolve_conflicts): @@ -438,8 +446,10 @@ def test_resolve_conflicts(self, mock_resolve_conflicts): self.assertIn('new_chain', response_json) self.assertEqual(response_json['message'], 'Our chain was replaced') - @patch('src.app.routes.blockchain.resolve_conflicts') # Mock resolve_conflicts method - @patch('src.app.routes.blockchain.chain', new_callable=list) # Mock blockchain.chain + # Mock resolve_conflicts method + @patch('src.app.routes.blockchain.resolve_conflicts') + # Mock blockchain.chain + @patch('src.app.routes.blockchain.chain', new_callable=list) def test_consensus_authoritative_chain(self, mock_chain, mock_resolve_conflicts): # Simulate the chain being authoritative (not replaced) mock_resolve_conflicts.return_value = False @@ -469,7 +479,8 @@ def test_consensus_authoritative_chain(self, mock_chain, mock_resolve_conflicts) # Validate the response status code and content self.assertEqual(response.status_code, 200) self.assertIn('message', response_json) - self.assertEqual(response_json['message'], 'Our chain is authoritative') + self.assertEqual(response_json['message'], + 'Our chain is authoritative') # Check that the chain is returned correctly in the response self.assertIn('chain', response_json) @@ -509,7 +520,8 @@ def test_get_nodes_empty(self, mock_nodes): # Validate the response status code and content self.assertEqual(response.status_code, 200) self.assertIn('nodes', response_json) - self.assertEqual(len(response_json['nodes']), 0) # No nodes in the blockchain + # No nodes in the blockchain + self.assertEqual(len(response_json['nodes']), 0) @patch('src.app.routes.blockchain.transactions', new_callable=list) def test_get_transactions(self, mock_transactions): @@ -531,12 +543,16 @@ def test_get_transactions(self, mock_transactions): self.assertEqual(len(response_json['transactions']), 2) # Validate transaction details - self.assertEqual(response_json['transactions'][0]['sender'], 'sender_1') - self.assertEqual(response_json['transactions'][0]['recipient'], 'recipient_1') + self.assertEqual( + response_json['transactions'][0]['sender'], 'sender_1') + self.assertEqual(response_json['transactions'] + [0]['recipient'], 'recipient_1') self.assertEqual(response_json['transactions'][0]['amount'], 50.0) - self.assertEqual(response_json['transactions'][1]['sender'], 'sender_2') - self.assertEqual(response_json['transactions'][1]['recipient'], 'recipient_2') + self.assertEqual( + response_json['transactions'][1]['sender'], 'sender_2') + self.assertEqual(response_json['transactions'] + [1]['recipient'], 'recipient_2') self.assertEqual(response_json['transactions'][1]['amount'], 100.0) @patch('src.app.routes.blockchain.transactions', new_callable=list) @@ -553,7 +569,8 @@ def test_get_transactions_empty(self, mock_transactions): # Validate the response status code and content self.assertEqual(response.status_code, 200) self.assertIn('transactions', response_json) - self.assertEqual(len(response_json['transactions']), 0) # No transactions in the pool + # No transactions in the pool + self.assertEqual(len(response_json['transactions']), 0) @patch('src.app.routes.blockchain.chain', new_callable=list) def test_full_chain(self, mock_chain): @@ -588,7 +605,8 @@ def test_full_chain(self, mock_chain): # Validate block details self.assertEqual(response_json['chain'][0]['block_number'], 1) self.assertEqual(response_json['chain'][1]['block_number'], 2) - self.assertEqual(response_json['chain'][1]['transactions'][0]['sender'], 'sender_1') + self.assertEqual(response_json['chain'][1] + ['transactions'][0]['sender'], 'sender_1') @patch('src.app.routes.blockchain.chain', new_callable=list) def test_full_chain_empty(self, mock_chain): @@ -611,7 +629,9 @@ def test_swagger_ui(self): # Test the Swagger UI route and follow redirects response = self.client.get('/swagger', follow_redirects=True) self.assertEqual(response.status_code, 200) - self.assertIn(b"ChainAlchemy API Documentation", response.data) + self.assertIn( + b"ChainAlchemy API Documentation", response.data) + if __name__ == '__main__': unittest.main() From e6952f13d015eefa9f0c4e000f3a6ef8c6914b6d Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sun, 22 Dec 2024 22:01:58 +0100 Subject: [PATCH 20/21] ADD Added a logging configuration --- src/logging_config.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/logging_config.py diff --git a/src/logging_config.py b/src/logging_config.py new file mode 100644 index 0000000..3985cb8 --- /dev/null +++ b/src/logging_config.py @@ -0,0 +1,45 @@ +import os +import sys +import logging +import logging.config + + +def setup_logging(): + """ + Sets up the logging configuration for the application. + + The configuration includes: + - Console output with detailed formatting. + - Log level controlled by the LOG_LEVEL environment variable. + + The default log level is DEBUG if not specified. + """ + log_level = os.getenv('LOG_LEVEL', 'DEBUG').upper() + + logging_config = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'detailed': { + 'format': '[%(asctime)s] %(levelname)s in %(filename)s, function %(funcName)s: %(message)s' + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'level': log_level, + 'formatter': 'detailed', + 'stream': sys.stdout, + }, + }, + 'root': { + 'handlers': ['console'], + 'level': log_level, + }, + } + + # Apply the logging configuration + logging.config.dictConfig(logging_config) + + # Return a logger object for this module + return logging.getLogger(__name__) From 216e2dc3655dcd5bf716959c61b22e412b8138c0 Mon Sep 17 00:00:00 2001 From: Hamdi Date: Sun, 22 Dec 2024 22:02:53 +0100 Subject: [PATCH 21/21] UPDATE Added some loggers to the code --- src/blockchain.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/blockchain.py b/src/blockchain.py index 31504de..bab952a 100644 --- a/src/blockchain.py +++ b/src/blockchain.py @@ -10,9 +10,15 @@ from Crypto.Signature import PKCS1_v1_5 from typing import OrderedDict +from logging_config import setup_logging + +# Set up logging +logger = setup_logging() + MINING_SENDER = "THE BLOCKCHAIN" MINING_REWARD = 1.0 MINING_DIFFICULTY = 2 +logger.debug("successfully initalized constants") class Blockchain: @@ -26,6 +32,7 @@ def __init__(self): self.node_id = str(uuid4()).replace('-', '') # Create genesis block self.create_block(nonce=0, previous_hash='00') + logger.debug("successfully initalized Blockchain class parameters") def register_node(self, node_url): """ @@ -33,12 +40,16 @@ def register_node(self, node_url): """ # Checking node_url has valid format parsed_url = urlparse(url=node_url) + logger.debug(f"Trying to add the following node: {parsed_url}") if parsed_url.netloc: self.nodes.add(parsed_url.netloc) + logger.info("Successfully added node") elif parsed_url.path: # Accepts an URL without scheme like '192.168.0.5:5000'. self.nodes.add(parsed_url.path) + logger.info("Successfully added node") else: + logger.warning(f"The given URL '{node_url}' is not valid") raise ValueError('Invalid URL') def sign_transaction(self, sender_private_key, transaction): @@ -71,28 +82,46 @@ def submit_transaction(self, sender_address, sender_private_key, recipient_addre 'recipient_address': recipient_address, 'value': value }) + # Generate a random transaction id + transaction_id = str(uuid4()).replace('-', '') + logger.info(f"Initating Transaction {transaction_id}") # If it's a mining reward, skip the signature process if sender_address == MINING_SENDER: + logger.debug(f"Initating mining process for transaction { + transaction_id}") self.transactions.append(transaction) + logger.info(f"Transaction {transaction_id} was successfull") return len(self.chain) + 1 # Manages transactions from wallet to another wallet else: + logger.debug(f"Transaction {transaction_id} from sender adress ending with { + sender_address[-3:]} to recipient adress ending with {recipient_address[-3:]}") + # Signing Transaction + logger.debug(f"Signing transaction {transaction_id}") transaction_signature = self.sign_transaction( sender_private_key=sender_private_key, transaction=transaction ) + logger.debug(f"Successfully signed transaction {transaction_id}") + + # Verifying Transaction + logger.debug(f"Verifying transaction {transaction_id}") transaction_verification = self.verify_transaction_signature( sender_address=sender_address, signature=transaction_signature, transaction=transaction ) + logger.debug(f"Successfully verfied transaction {transaction_id}") if transaction_verification: self.transactions.append(transaction) + logger.info(f"Transaction {transaction_id} was successfull") return len(self.chain) + 1 else: + logger.warning( + f"Transaction {transaction_id} wasn't successfull") return False def get_balance(self, address): @@ -101,6 +130,8 @@ def get_balance(self, address): """ balance = 0.0 + logger.debug(f"Calculating balance for the wallet ending with '{ + address[-3:]}'") # Iterate through all blocks in the chain for block in self.chain: # Iterate through all transactions in each block @@ -112,7 +143,8 @@ def get_balance(self, address): # Check if the wallet is the recipient if transaction['recipient_address'] == address: balance += transaction['value'] - + logger.debug(f" balance for the wallet ending with '{ + address[-3:]}' is: {balance}") return balance def get_available_balance(self, address):