From 3c9992f20d370740ba23b313732ce90a2170dd3e Mon Sep 17 00:00:00 2001 From: siddhant Date: Wed, 24 Jun 2026 23:47:38 +0530 Subject: [PATCH 1/2] implement difficulty adjustment --- genesis.json | 2 ++ main.py | 1 + minichain/chain.py | 35 ++++++++++++++++++++++ minichain/pow.py | 3 +- tests/test_difficulty.py | 63 +++++++++++++++++++++++++++++++++++++++ tests/test_persistence.py | 11 ++++--- tests/test_reorg.py | 2 +- 7 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 tests/test_difficulty.py diff --git a/genesis.json b/genesis.json index 4668575..eb966c5 100644 --- a/genesis.json +++ b/genesis.json @@ -2,6 +2,8 @@ "chain_id": "minichain-default", "timestamp": 1716880000000, "difficulty": 4, + "target_block_time": 10000, + "alpha": 0.1, "alloc": { "0000000000000000000000000000000000000001": { "balance": 1000000000 diff --git a/main.py b/main.py index 2d02ed8..b81ca95 100644 --- a/main.py +++ b/main.py @@ -92,6 +92,7 @@ def mine_and_process_block(chain, mempool, miner_pk): receipt_root=calculate_receipt_root(receipts), receipts=receipts, miner=miner_pk, + difficulty=chain.current_difficulty, ) mined_block = mine_block(block) diff --git a/minichain/chain.py b/minichain/chain.py index 1aa3917..5c31972 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -70,6 +70,11 @@ def _create_genesis_block(self, genesis_path): timestamp = config.get("timestamp") difficulty = config.get("difficulty") + self.target_block_time = config.get("target_block_time", 10000) + self.alpha = config.get("alpha", 0.1) + self.current_difficulty = difficulty + self.avg_block_time = self.target_block_time + genesis_block = Block( index=0, previous_hash="0", @@ -128,6 +133,10 @@ def add_block(self, block): logger.warning("Block %s rejected: %s", block.index, exc) return False + if block.difficulty != self.current_difficulty: + logger.warning("Block %s rejected: Invalid difficulty. Expected %s, got %s", block.index, self.current_difficulty, block.difficulty) + return False + # Validate transactions on a temporary state copy temp_state = self.state.copy() temp_state.chain_id = self.chain_id @@ -161,6 +170,15 @@ def add_block(self, block): logger.warning("Block %s rejected: Invalid state root. Expected %s, got %s", block.index, temp_state.state_root(), block.state_root) return False + # Update EMA difficulty state + time_diff = block.timestamp - self.last_block.timestamp + self.avg_block_time = self.alpha * time_diff + (1 - self.alpha) * self.avg_block_time + + if self.avg_block_time > self.target_block_time: + self.current_difficulty = max(1, self.current_difficulty - 1) + elif self.avg_block_time < self.target_block_time: + self.current_difficulty += 1 + # All transactions valid → commit state and append block self.state = temp_state self.chain.append(block) @@ -198,11 +216,18 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: temp_state.chain_id = self.chain_id temp_state.restore(self._genesis_state_snapshot) + temp_difficulty = new_chain_list[0].difficulty + temp_avg_block_time = self.target_block_time + # Verify and apply blocks 1 to N for i in range(1, len(new_chain_list)): prev_block = new_chain_list[i-1] block = new_chain_list[i] + if block.difficulty != temp_difficulty: + logger.warning("Reorg failed at block %s: Invalid difficulty. Expected %s, got %s", block.index, temp_difficulty, block.difficulty) + return False, [] + try: validate_block_link_and_hash(prev_block, block) except ValueError as exc: @@ -230,6 +255,14 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: logger.warning("Reorg failed: Invalid state root at block %s", block.index) return False, [] + # Update EMA difficulty state for reorg validation + time_diff = block.timestamp - prev_block.timestamp + temp_avg_block_time = self.alpha * time_diff + (1 - self.alpha) * temp_avg_block_time + if temp_avg_block_time > self.target_block_time: + temp_difficulty = max(1, temp_difficulty - 1) + elif temp_avg_block_time < self.target_block_time: + temp_difficulty += 1 + # 4. Success! Compute orphaned transactions. old_txs = {tx.tx_id: tx for b in original_chain[1:] for tx in b.transactions} new_tx_ids = {tx.tx_id for b in new_chain_list[1:] for tx in b.transactions} @@ -237,5 +270,7 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: self.chain = new_chain_list self.state = temp_state + self.current_difficulty = temp_difficulty + self.avg_block_time = temp_avg_block_time logger.info("Reorg successful! Switched to new chain tip: Block %s", self.last_block.index) return True, orphans diff --git a/minichain/pow.py b/minichain/pow.py index 40503a5..813bcc7 100644 --- a/minichain/pow.py +++ b/minichain/pow.py @@ -13,7 +13,7 @@ def calculate_hash(block_dict): def mine_block( block, - difficulty=4, + difficulty=None, max_nonce=10_000_000, timeout_seconds=None, logger=None, @@ -21,6 +21,7 @@ def mine_block( ): """Mines a block using Proof-of-Work without mutating input block until success.""" + difficulty = difficulty if difficulty is not None else block.difficulty if not isinstance(difficulty, int) or difficulty <= 0: raise ValueError("Difficulty must be a positive integer.") diff --git a/tests/test_difficulty.py b/tests/test_difficulty.py new file mode 100644 index 0000000..0176f9b --- /dev/null +++ b/tests/test_difficulty.py @@ -0,0 +1,63 @@ +import unittest +from minichain import Blockchain, Block +from minichain.pow import mine_block + +class TestEMADifficulty(unittest.TestCase): + def test_difficulty_adjustment(self): + chain = Blockchain() + chain.target_block_time = 1000 + chain.alpha = 0.5 + chain.avg_block_time = 1000 + chain.current_difficulty = 3 + chain.chain[0].difficulty = 3 + + # Fast mining: timestamps only 1ms apart + # avg = 0.5 * 1 + 0.5 * 1000 = 500.5 (which is < 1000) => difficulty increments to 4 + ts = chain.last_block.timestamp + 1 + block1 = Block(index=1, previous_hash=chain.last_block.hash, transactions=[], timestamp=ts, difficulty=chain.current_difficulty, state_root=chain.state.state_root()) + mined_block1 = mine_block(block1) + self.assertTrue(chain.add_block(mined_block1)) + self.assertEqual(chain.current_difficulty, 4) + + # Slow mining: timestamp 5000ms apart + # avg = 0.5 * 5000 + 0.5 * 500.5 = 2750.25 (which is > 1000) => difficulty decrements to 3 + ts = chain.last_block.timestamp + 5000 + block2 = Block(index=2, previous_hash=chain.last_block.hash, transactions=[], timestamp=ts, difficulty=chain.current_difficulty, state_root=chain.state.state_root()) + mined_block2 = mine_block(block2) + self.assertTrue(chain.add_block(mined_block2)) + self.assertEqual(chain.current_difficulty, 3) + + def test_reorg_difficulty_validation(self): + chain1 = Blockchain() + chain1.target_block_time = 1000 + chain1.alpha = 0.5 + chain1.avg_block_time = 1000 + chain1.current_difficulty = 1 + chain1.chain[0].difficulty = 1 + + chain2 = Blockchain() + chain2.target_block_time = 1000 + chain2.alpha = 0.5 + chain2.avg_block_time = 1000 + chain2.current_difficulty = 1 + chain2.chain[0].difficulty = 1 + + # Chain 2 mines a fast block, difficulty goes to 2 + block1 = Block(1, chain2.last_block.hash, [], timestamp=chain2.last_block.timestamp + 1, difficulty=chain2.current_difficulty, state_root=chain2.state.state_root()) + mine_block(block1) + chain2.add_block(block1) + self.assertEqual(chain2.current_difficulty, 2) + + # Reorg chain1 to chain2 + success, orphans = chain1.resolve_conflicts(chain2.chain) + self.assertTrue(success) + self.assertEqual(chain1.current_difficulty, 2) + + # Forging a chain with wrong difficulty should be rejected + forged_chain = list(chain2.chain) + forged_block = Block(2, chain2.last_block.hash, [], timestamp=chain2.last_block.timestamp + 1000, difficulty=1, state_root=chain2.state.state_root()) + mine_block(forged_block) + forged_chain.append(forged_block) + + success, _ = chain1.resolve_conflicts(forged_chain) + self.assertFalse(success) # Rejected because difficulty should have been 2! diff --git a/tests/test_persistence.py b/tests/test_persistence.py index c215712..d8e60b9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -5,6 +5,7 @@ import shutil import sqlite3 import tempfile +import time import unittest from nacl.encoding import HexEncoder @@ -49,12 +50,13 @@ def _chain_with_tx(self): index=1, previous_hash=bc.last_block.hash, transactions=[tx], - difficulty=1, + difficulty=bc.current_difficulty, state_root=temp_state.state_root(), receipt_root=calculate_receipt_root([receipt]), receipts=[receipt], + timestamp=int(time.time()), ) - mine_block(block, difficulty=1) + mine_block(block) bc.add_block(block) return bc, alice_pk, bob_pk @@ -243,12 +245,13 @@ def test_loaded_chain_can_add_new_block(self): index=len(restored.chain), previous_hash=restored.last_block.hash, transactions=[tx2], - difficulty=1, + difficulty=restored.current_difficulty, state_root=temp_state.state_root(), receipt_root=calculate_receipt_root([receipt2]), receipts=[receipt2], + timestamp=int(time.time()), ) - mine_block(block2, difficulty=1) + mine_block(block2) self.assertTrue(restored.add_block(block2)) self.assertEqual(len(restored.chain), len(bc.chain) + 1) diff --git a/tests/test_reorg.py b/tests/test_reorg.py index 4abb7bf..e931b47 100644 --- a/tests/test_reorg.py +++ b/tests/test_reorg.py @@ -21,7 +21,7 @@ def genesis_file(tmp_path): pk = sk.verify_key.encode(encoder=HexEncoder).decode() data = { "timestamp": int(time.time()), - "difficulty": 0, + "difficulty": 1, "alloc": { pk: {"balance": 1000} } From 824e21a809c26175dbf5d13cf0ac5c8d03c19cfb Mon Sep 17 00:00:00 2001 From: siddhant Date: Sat, 27 Jun 2026 03:39:14 +0530 Subject: [PATCH 2/2] implement peer blacklisting --- main.py | 34 ++++++++++++++++++++++ minichain/chain.py | 26 +++++++++-------- minichain/persistence.py | 62 ++++++++++++++++++++++++++++++++++++++++ minichain/state.py | 36 ++++++++++++++++------- minichain/validators.py | 9 ++++++ 5 files changed, 145 insertions(+), 22 deletions(-) diff --git a/main.py b/main.py index b81ca95..fe1c1ce 100644 --- a/main.py +++ b/main.py @@ -256,6 +256,9 @@ async def handler(data): ║ connect : - connect to a peer ║ ║ address - show your public key ║ ║ chain - show chain summary ║ +║ list-banned - show banned peers ║ +║ ban - ban a peer ║ +║ unban - unban a peer ║ ║ help - show this help ║ ║ quit - shut down ║ ╚════════════════════════════════════════════════╝ @@ -420,6 +423,37 @@ async def cli_loop(sk, pk, chain, mempool, network): tx_count = len(b.transactions) if b.transactions else 0 print(f" Block #{b.index} hash={b.hash[:16]}... txs={tx_count}") + # ── list-banned ── + elif cmd == "list-banned": + from minichain.persistence import get_banned_peers + banned = get_banned_peers() + if not banned: + print(" No peers are currently banned.") + else: + print(f" {len(banned)} banned peer(s):") + for p in banned: + print(f" - {p['peer_id']} (Reason: {p['reason']}, Time: {p['timestamp']})") + + # ── ban ── + elif cmd == "ban": + if len(parts) < 2: + print(" Usage: ban ") + continue + peer_id = parts[1] + from minichain.persistence import ban_peer + ban_peer(peer_id, reason="Manual ban via CLI") + print(f" ✅ Peer {peer_id} banned.") + + # ── unban ── + elif cmd == "unban": + if len(parts) < 2: + print(" Usage: unban ") + continue + peer_id = parts[1] + from minichain.persistence import unban_peer + unban_peer(peer_id) + print(f" ✅ Peer {peer_id} unbanned.") + # ── help ── elif cmd == "help": print(HELP_TEXT) diff --git a/minichain/chain.py b/minichain/chain.py index 5c31972..1ed9b84 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -125,17 +125,18 @@ def add_block(self, block): Validates and adds a block to the chain if all transactions succeed. Uses a copied State to ensure atomic validation. """ + from .validators import ValidationStatus with self._lock: try: validate_block_link_and_hash(self.last_block, block) except ValueError as exc: logger.warning("Block %s rejected: %s", block.index, exc) - return False + return ValidationStatus.INVALID if "hash" in str(exc) else ValidationStatus.FAILED if block.difficulty != self.current_difficulty: logger.warning("Block %s rejected: Invalid difficulty. Expected %s, got %s", block.index, self.current_difficulty, block.difficulty) - return False + return ValidationStatus.INVALID # Validate transactions on a temporary state copy temp_state = self.state.copy() @@ -143,12 +144,12 @@ def add_block(self, block): receipts = [] for tx in block.transactions: - receipt = temp_state.validate_and_apply(tx) + status, receipt = temp_state.validate_and_apply_with_status(tx) - # Reject block if any transaction fails mathematical validation (None) - if receipt is None: + # Reject block if any transaction fails mathematical validation + if status != ValidationStatus.VALID: logger.warning("Block %s rejected: Transaction failed validation", block.index) - return False + return status receipts.append(receipt) @@ -159,16 +160,16 @@ def add_block(self, block): computed_receipt_root = calculate_receipt_root(receipts) if block.receipt_root != computed_receipt_root: logger.warning("Block %s rejected: Invalid receipt root. Expected %s, got %s", block.index, computed_receipt_root, block.receipt_root) - return False + return ValidationStatus.INVALID if [r.to_dict() for r in block.receipts] != [r.to_dict() for r in receipts]: logger.warning("Block %s rejected: Receipts payload mismatch", block.index) - return False + return ValidationStatus.INVALID # Verify state root if block.state_root != temp_state.state_root(): logger.warning("Block %s rejected: Invalid state root. Expected %s, got %s", block.index, temp_state.state_root(), block.state_root) - return False + return ValidationStatus.INVALID # Update EMA difficulty state time_diff = block.timestamp - self.last_block.timestamp @@ -182,7 +183,7 @@ def add_block(self, block): # All transactions valid → commit state and append block self.state = temp_state self.chain.append(block) - return True + return ValidationStatus.VALID def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: """ @@ -236,8 +237,9 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: receipts = [] for tx in block.transactions: - receipt = temp_state.validate_and_apply(tx) - if receipt is None: + from .validators import ValidationStatus + status, receipt = temp_state.validate_and_apply_with_status(tx) + if status != ValidationStatus.VALID: logger.warning("Reorg failed: Transaction validation failed in block %s", block.index) return False, [] receipts.append(receipt) diff --git a/minichain/persistence.py b/minichain/persistence.py index d142879..8de4148 100644 --- a/minichain/persistence.py +++ b/minichain/persistence.py @@ -257,6 +257,68 @@ def _load_snapshot_from_sqlite(db_path: str) -> dict[str, Any]: return {"chain": chain, "state": state} +# --------------------------------------------------------------------------- +# Banned Peers (Track 1) +# --------------------------------------------------------------------------- + +import time + +def _ensure_banned_peers_table(conn: sqlite3.Connection) -> None: + conn.execute( + "CREATE TABLE IF NOT EXISTS banned_peers (peer_id TEXT PRIMARY KEY, reason TEXT, timestamp REAL)" + ) + +def ban_peer(peer_id: str, reason: str, path: str = ".") -> None: + db_path = os.path.join(path, _DB_FILE) + os.makedirs(path, exist_ok=True) + conn = _connect(db_path) + try: + _ensure_banned_peers_table(conn) + with conn: + conn.execute( + "INSERT OR REPLACE INTO banned_peers (peer_id, reason, timestamp) VALUES (?, ?, ?)", + (peer_id, reason, time.time()) + ) + finally: + conn.close() + +def unban_peer(peer_id: str, path: str = ".") -> None: + db_path = os.path.join(path, _DB_FILE) + if not os.path.exists(db_path): + return + conn = _connect(db_path) + try: + _ensure_banned_peers_table(conn) + with conn: + conn.execute("DELETE FROM banned_peers WHERE peer_id = ?", (peer_id,)) + finally: + conn.close() + +def is_peer_banned(peer_id: str, path: str = ".") -> bool: + db_path = os.path.join(path, _DB_FILE) + if not os.path.exists(db_path): + return False + conn = _connect(db_path) + try: + _ensure_banned_peers_table(conn) + row = conn.execute("SELECT peer_id FROM banned_peers WHERE peer_id = ?", (peer_id,)).fetchone() + return row is not None + finally: + conn.close() + +def get_banned_peers(path: str = ".") -> list[dict[str, Any]]: + db_path = os.path.join(path, _DB_FILE) + if not os.path.exists(db_path): + return [] + conn = _connect(db_path) + try: + _ensure_banned_peers_table(conn) + rows = conn.execute("SELECT peer_id, reason, timestamp FROM banned_peers ORDER BY timestamp DESC").fetchall() + return [{"peer_id": r["peer_id"], "reason": r["reason"], "timestamp": r["timestamp"]} for r in rows] + finally: + conn.close() + + # --------------------------------------------------------------------------- # Legacy JSON helpers # --------------------------------------------------------------------------- diff --git a/minichain/state.py b/minichain/state.py index 13c7c02..413fec5 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -43,26 +43,27 @@ def get_account(self, address): return self.accounts[address] def verify_transaction_logic(self, tx): + from .validators import ValidationStatus if not tx.verify(): logger.error("Error: Invalid signature for tx from %s...", tx.sender[:8]) - return False + return ValidationStatus.INVALID if getattr(tx, "chain_id", None) != self.chain_id: logger.error("Error: Invalid chain_id in tx from %s...", tx.sender[:8]) - return False + return ValidationStatus.INVALID sender_acc = self.get_account(tx.sender) total_cost = tx.amount + getattr(tx, 'fee', 0) if sender_acc['balance'] < total_cost: logger.warning("Invalid tx %s: insufficient balance", tx.tx_id) - return False + return ValidationStatus.FAILED if sender_acc['nonce'] != tx.nonce: logger.error("Error: Invalid nonce. Expected %s, got %s", sender_acc['nonce'], tx.nonce) - return False + return ValidationStatus.FAILED - return True + return ValidationStatus.VALID def copy(self): """ @@ -88,22 +89,37 @@ def restore(self, snapshot_data): 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. + Returns: Receipt|None """ # Semantic validation: amount must be an integer and non-negative if not isinstance(tx.amount, int) or tx.amount < 0: return None - # Further checks can be added here return self.apply_transaction(tx) + def validate_and_apply_with_status(self, tx): + """ + Validate and apply a transaction, bubbling up the precise ValidationStatus. + Returns: (ValidationStatus, Receipt|None) + """ + from .validators import ValidationStatus + if not isinstance(tx.amount, int) or tx.amount < 0: + return ValidationStatus.MALFORMED, None + + status = self.verify_transaction_logic(tx) + if status != ValidationStatus.VALID: + return status, None + + # We know it's valid, so apply_transaction will succeed and return a Receipt + return ValidationStatus.VALID, self.apply_transaction(tx) + def apply_transaction(self, tx): """ Applies transaction and mutates state. Returns: Receipt object if mathematically valid, None if invalid. """ - if not self.verify_transaction_logic(tx): + from .validators import ValidationStatus + status = self.verify_transaction_logic(tx) + if status != ValidationStatus.VALID: return None sender = self.accounts[tx.sender] diff --git a/minichain/validators.py b/minichain/validators.py index b813df4..3994e3a 100644 --- a/minichain/validators.py +++ b/minichain/validators.py @@ -1,5 +1,14 @@ import re +from enum import Enum, auto + + +class ValidationStatus(Enum): + VALID = auto() + INVALID = auto() + FAILED = auto() + MALFORMED = auto() def is_valid_receiver(receiver): return bool(re.fullmatch(r"[0-9a-fA-F]{40}|[0-9a-fA-F]{64}", receiver)) +