From 3c600c81d12657e0d77b0ec6dccc648302fcb2ba Mon Sep 17 00:00:00 2001 From: siddhant Date: Fri, 27 Mar 2026 16:02:40 +0530 Subject: [PATCH 1/2] refactor: Consolidate scattered validation logic --- main.py | 6 +-- minichain/block.py | 45 +++++++++++++-------- minichain/chain.py | 2 +- minichain/mempool.py | 4 +- minichain/p2p.py | 68 ++++---------------------------- minichain/state.py | 17 ++------ minichain/transaction.py | 44 ++++++++++++++++----- minichain/validators.py | 5 --- tests/test_protocol_hardening.py | 2 +- 9 files changed, 81 insertions(+), 112 deletions(-) delete mode 100644 minichain/validators.py diff --git a/main.py b/main.py index e1edc51..0134d3e 100644 --- a/main.py +++ b/main.py @@ -27,7 +27,7 @@ from nacl.encoding import HexEncoder from minichain import Transaction, Blockchain, Block, State, Mempool, P2PNetwork, mine_block -from minichain.validators import is_valid_receiver + logger = logging.getLogger(__name__) @@ -67,7 +67,7 @@ def mine_and_process_block(chain, mempool, miner_pk): if tx.nonce < expected_nonce: stale_txs.append(tx) continue - if temp_state.validate_and_apply(tx): + if temp_state.apply_transaction(tx): mineable_txs.append(tx) if stale_txs: @@ -212,7 +212,7 @@ async def cli_loop(sk, pk, chain, mempool, network): print(" Usage: send ") continue receiver = parts[1] - if not is_valid_receiver(receiver): + if not Transaction.is_valid_address(receiver): print(" Invalid receiver format. Expected 40 or 64 hex characters.") continue try: diff --git a/minichain/block.py b/minichain/block.py index 9854cf4..4659c5e 100644 --- a/minichain/block.py +++ b/minichain/block.py @@ -101,19 +101,32 @@ def compute_hash(self) -> str: @classmethod def from_dict(cls, payload: dict): - transactions = [ - Transaction.from_dict(tx_payload) - for tx_payload in payload.get("transactions", []) - ] - block = cls( - index=payload["index"], - previous_hash=payload["previous_hash"], - transactions=transactions, - timestamp=payload.get("timestamp"), - difficulty=payload.get("difficulty"), - ) - block.nonce = payload.get("nonce", 0) - block.hash = payload.get("hash") - if "merkle_root" in payload: - block.merkle_root = payload["merkle_root"] - return block + try: + transactions = [ + Transaction.from_dict(tx_payload) + for tx_payload in payload.get("transactions", []) + ] + if any(tx is None for tx in transactions): + return None + + block = cls( + index=payload["index"], + previous_hash=payload["previous_hash"], + transactions=transactions, + timestamp=payload.get("timestamp"), + difficulty=payload.get("difficulty"), + ) + block.nonce = payload.get("nonce", 0) + block.hash = payload.get("hash") + if "merkle_root" in payload: + block.merkle_root = payload["merkle_root"] + return block + except (KeyError, TypeError, ValueError): + return None + + def is_valid(self): + if type(self.index) is not int or type(self.nonce) is not int or type(self.timestamp) is not int: + return False + if not isinstance(self.previous_hash, str) or (self.hash is not None and not isinstance(self.hash, str)): + return False + return all(tx.is_valid() for tx in self.transactions) diff --git a/minichain/chain.py b/minichain/chain.py index b65d575..53bdfc4 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -71,7 +71,7 @@ def add_block(self, block): temp_state = self.state.copy() for tx in block.transactions: - result = temp_state.validate_and_apply(tx) + result = temp_state.apply_transaction(tx) # Reject block if any transaction fails if not result: diff --git a/minichain/mempool.py b/minichain/mempool.py index 4b71e08..0bb8af5 100644 --- a/minichain/mempool.py +++ b/minichain/mempool.py @@ -12,8 +12,8 @@ def __init__(self, max_size=1000, transactions_per_block=100): self.transactions_per_block = transactions_per_block def add_transaction(self, tx): - if not tx.verify(): - logger.warning("Mempool: Invalid signature rejected") + if not tx.is_valid(): + logger.warning("Mempool: Invalid transaction rejected") return False with self._lock: diff --git a/minichain/p2p.py b/minichain/p2p.py index 3271598..b05417f 100644 --- a/minichain/p2p.py +++ b/minichain/p2p.py @@ -10,7 +10,6 @@ import logging from .serialization import canonical_json_hash -from .validators import is_valid_receiver logger = logging.getLogger(__name__) @@ -114,41 +113,13 @@ async def _handle_incoming( await self._notify_peer_connected(writer, "Network: Error during peer sync") def _validate_transaction_payload(self, payload): + from .transaction import Transaction if not isinstance(payload, dict): return False - - required_fields = { - "sender": str, - "amount": int, - "nonce": int, - "timestamp": int, - "signature": str, - } - optional_fields = { - "receiver": (str, type(None)), - "data": (str, type(None)), - } - allowed_fields = set(required_fields) | set(optional_fields) - - if set(payload) != allowed_fields: - return False - - for field, expected_type in required_fields.items(): - if not isinstance(payload.get(field), expected_type): - return False - - for field, expected_type in optional_fields.items(): - if not isinstance(payload.get(field), expected_type): - return False - - if payload["amount"] <= 0: - return False - - receiver = payload.get("receiver") - if receiver is not None and not is_valid_receiver(receiver): + tx = Transaction.from_dict(payload) + if not tx: return False - - return True + return tx.is_valid() def _validate_sync_payload(self, payload): if not isinstance(payload, dict) or set(payload) != {"accounts"}: @@ -176,36 +147,11 @@ def _validate_sync_payload(self, payload): return True def _validate_block_payload(self, payload): + from .block import Block if not isinstance(payload, dict): return False - - required_fields = { - "index": int, - "previous_hash": str, - "merkle_root": (str, type(None)), - "transactions": list, - "timestamp": int, - "difficulty": (int, type(None)), - "nonce": int, - "hash": str, - } - optional_fields = {"miner": str} - allowed_fields = set(required_fields) | set(optional_fields) - - if not set(payload).issubset(allowed_fields): - return False - - for field, expected_type in required_fields.items(): - if not isinstance(payload.get(field), expected_type): - return False - - if "miner" in payload and not isinstance(payload["miner"], str): - return False - - return all( - self._validate_transaction_payload(tx_payload) - for tx_payload in payload["transactions"] - ) + block = Block.from_dict(payload) + return block is not None and block.is_valid() def _validate_message(self, message): if not isinstance(message, dict): diff --git a/minichain/state.py b/minichain/state.py index ce9a6f0..509544f 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -26,8 +26,8 @@ def get_account(self, address): return self.accounts[address] def verify_transaction_logic(self, tx): - if not tx.verify(): - logger.error(f"Error: Invalid signature for tx from {tx.sender[:8]}...") + if not tx.is_valid(): + logger.error("Error: Invalid transaction schema or signature") return False sender_acc = self.get_account(tx.sender) @@ -50,18 +50,7 @@ def copy(self): new_state.contract_machine = ContractMachine(new_state) # Reinitialize contract_machine return new_state - def validate_and_apply(self, tx): - """ - Validate and apply a transaction. - Returns the same success/failure shape as apply_transaction(). - NOTE: Delegates to apply_transaction. Callers should use this for - semantic validation entry points. - """ - # Semantic validation: amount must be an integer and non-negative - if not isinstance(tx.amount, int) or tx.amount < 0: - return False - # Further checks can be added here - return self.apply_transaction(tx) + def apply_transaction(self, tx): """ diff --git a/minichain/transaction.py b/minichain/transaction.py index 27f41f3..6732a67 100644 --- a/minichain/transaction.py +++ b/minichain/transaction.py @@ -41,17 +41,43 @@ def to_signing_dict(self): "timestamp": self.timestamp, } + @staticmethod + def is_valid_address(address): + import re + return bool(re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", address)) + @classmethod def from_dict(cls, payload: dict): - return cls( - sender=payload["sender"], - receiver=payload.get("receiver"), - amount=payload["amount"], - nonce=payload["nonce"], - data=payload.get("data"), - signature=payload.get("signature"), - timestamp=payload.get("timestamp"), - ) + try: + return cls( + sender=payload["sender"], + receiver=payload.get("receiver"), + amount=payload["amount"], + nonce=payload["nonce"], + data=payload.get("data"), + signature=payload.get("signature"), + timestamp=payload.get("timestamp"), + ) + except (KeyError, TypeError): + return None + + def is_valid(self): + """Unified, stateless validation (Types, Schema, Signatures)""" + if not isinstance(self.amount, int) or self.amount < 0: + return False + if not isinstance(self.nonce, int) or self.nonce < 0: + return False + if not isinstance(self.sender, str) or not self.is_valid_address(self.sender): + return False + if self.receiver is not None: + if not isinstance(self.receiver, str) or not self.is_valid_address(self.receiver): + return False + if self.data is not None and not isinstance(self.data, str): + return False + if not isinstance(self.timestamp, int) or self.timestamp <= 0: + return False + + return self.verify() @property def hash_payload(self): diff --git a/minichain/validators.py b/minichain/validators.py deleted file mode 100644 index b813df4..0000000 --- a/minichain/validators.py +++ /dev/null @@ -1,5 +0,0 @@ -import re - - -def is_valid_receiver(receiver): - return bool(re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", receiver)) diff --git a/tests/test_protocol_hardening.py b/tests/test_protocol_hardening.py index 6b169e7..78e65a3 100644 --- a/tests/test_protocol_hardening.py +++ b/tests/test_protocol_hardening.py @@ -105,7 +105,7 @@ async def test_block_schema_accepts_current_block_wire_format(self): sender_pk = sender_sk.verify_key.encode(encoder=HexEncoder).decode() receiver_pk = SigningKey.generate().verify_key.encode(encoder=HexEncoder).decode() - tx = Transaction(sender_pk, receiver_pk, 1, 0, timestamp=123) + tx = Transaction(sender_pk, receiver_pk, 1, 0, timestamp=1600000000000) tx.sign(sender_sk) block = Block(index=1, previous_hash="0" * 64, transactions=[tx], timestamp=456, difficulty=2) From 7859e3a7aa06c347fcd5eca4e011ee7c32e19346 Mon Sep 17 00:00:00 2001 From: siddhant Date: Fri, 27 Mar 2026 17:01:45 +0530 Subject: [PATCH 2/2] Harden block merkle validation --- .gitignore | 3 +++ minichain/block.py | 5 +++++ minichain/transaction.py | 7 +++---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index b8edfb6..7c7cbda 100644 --- a/.gitignore +++ b/.gitignore @@ -218,6 +218,9 @@ _minted* *.spell.bad *.spell.txt +# MiniChain local persistence directories +/.node*/ + # svg svg-inkscape/ diff --git a/minichain/block.py b/minichain/block.py index 4659c5e..c91997e 100644 --- a/minichain/block.py +++ b/minichain/block.py @@ -119,6 +119,7 @@ def from_dict(cls, payload: dict): block.nonce = payload.get("nonce", 0) block.hash = payload.get("hash") if "merkle_root" in payload: + # Accept payload value, but validate against computed value in is_valid(). block.merkle_root = payload["merkle_root"] return block except (KeyError, TypeError, ValueError): @@ -129,4 +130,8 @@ def is_valid(self): return False if not isinstance(self.previous_hash, str) or (self.hash is not None and not isinstance(self.hash, str)): return False + if self.merkle_root is not None and not isinstance(self.merkle_root, str): + return False + if self.merkle_root != _calculate_merkle_root(self.transactions): + return False return all(tx.is_valid() for tx in self.transactions) diff --git a/minichain/transaction.py b/minichain/transaction.py index 6732a67..ffb3589 100644 --- a/minichain/transaction.py +++ b/minichain/transaction.py @@ -1,4 +1,5 @@ import time +import re from nacl.signing import SigningKey, VerifyKey from nacl.encoding import HexEncoder from nacl.exceptions import BadSignatureError, CryptoError @@ -43,7 +44,6 @@ def to_signing_dict(self): @staticmethod def is_valid_address(address): - import re return bool(re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", address)) @classmethod @@ -69,9 +69,8 @@ def is_valid(self): return False if not isinstance(self.sender, str) or not self.is_valid_address(self.sender): return False - if self.receiver is not None: - if not isinstance(self.receiver, str) or not self.is_valid_address(self.receiver): - return False + if self.receiver is not None and (not isinstance(self.receiver, str) or not self.is_valid_address(self.receiver)): + return False if self.data is not None and not isinstance(self.data, str): return False if not isinstance(self.timestamp, int) or self.timestamp <= 0: