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): 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__) 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()