From af47bc47ed152448830874edfdca9cc0b9ead817 Mon Sep 17 00:00:00 2001 From: anuragShingare30 Date: Thu, 5 Mar 2026 19:12:00 +0530 Subject: [PATCH 1/2] feat: replace functionalities --- COMMAND.md | 84 +++++++++++ main.py | 326 +++++++++++++++++++++++++++++-------------- minichain/chain.py | 191 ++++++++++++++++++++++++- minichain/mempool.py | 13 ++ minichain/p2p.py | 28 +++- minichain/state.py | 24 ++++ 6 files changed, 556 insertions(+), 110 deletions(-) create mode 100644 COMMAND.md diff --git a/COMMAND.md b/COMMAND.md new file mode 100644 index 0000000..207a4c7 --- /dev/null +++ b/COMMAND.md @@ -0,0 +1,84 @@ +## Commands for testing + +### Test 1: Same Machine, Two Terminals +```bash +# activate the virtual environment +source .venv/bin/activate + +# Terminal 1 +python cli.py --port 9000 + +# Terminal 2 +python cli.py --port 9001 --peers 127.0.0.1:9000 +``` + + +### Test 2: Two Machines, Same LAN +```bash +# Machine A (Ex: 192.168.1.10) +python3 cli.py --port 9000 --mine + +# Machine B (Ex: 192.168.1.20) +python3 cli.py --port 9001 --peers 192.168.1.10:9000 +``` + + +**Follow the below steps for test 2** + +**find the IP addresses** +```bash +# run this both on machine A and machine B +ip addr | grep "inet " | grep -v 127.0.0.1 +# or +hostname -I +``` + +### Machine A (WSL + Windows Port Forwarding) + +**WSL: Start the node (miner)** +```bash +cd ~/web2/minichain +source .venv/bin/activate +python3 cli.py --port 9000 --mine +``` + +**Windows PowerShell (Admin): Forward port 9000 to WSL** +```powershell +$wslIp = wsl hostname -I +$wslIp = $wslIp.Trim() +netsh interface portproxy add v4tov4 listenport=9000 listenaddress=0.0.0.0 connectport=9000 connectaddress=$wslIp +netsh advfirewall firewall add rule name="MiniChain Node" dir=in action=allow protocol=tcp localport=9000 +netsh interface portproxy show all +``` + +### Machine B (Same LAN) + +**Test connectivity first**: +```bash +ping 192.168.137.10 +``` + +**Test port first:** +```bash +# linux +nc -zv 192.168.137.10 9000 + +# windows +Test-NetConnection -ComputerName 192.168.137.10 -Port 9000 +``` + + +**If ping works, start the node for (Linux/macOS):** +```bash +cd minichain +source .venv/bin/activate +python3 cli.py --port 9001 --peers 192.168.137.10:9000 +``` + +**Windows:** +```powershell +cd minichain +python cli.py --port 9001 --peers 192.168.137.10:9000 +``` + + diff --git a/main.py b/main.py index 12577dd..209272f 100644 --- a/main.py +++ b/main.py @@ -2,16 +2,17 @@ MiniChain interactive node — testnet demo entry point. Usage: - python main.py --port 9000 + python main.py --port 9000 --mine python main.py --port 9001 --connect 127.0.0.1:9000 Commands (type in the terminal while the node is running): balance — show all account balances send — send coins to another address - mine — mine a block from the mempool + mine — mine a block from the mempool (or toggle auto-mine) peers — show connected peers connect : — connect to another node address — show this node's public key + sync — request chain sync from peers help — show available commands quit — shut down the node """ @@ -21,6 +22,8 @@ import logging import re import sys +import time +import threading from nacl.signing import SigningKey from nacl.encoding import HexEncoder @@ -75,10 +78,10 @@ def mine_and_process_block(chain, mempool, miner_pk): # Network message handler # ────────────────────────────────────────────── -def make_network_handler(chain, mempool): +def make_network_handler(chain, mempool, network): """Return an async callback that processes incoming P2P messages.""" - async def handler(data): + async def handler(data, writer=None): msg_type = data.get("type") payload = data.get("data") @@ -91,6 +94,23 @@ async def handler(data): logger.info("🔄 Synced account %s... (balance=%d)", addr[:12], acc.get("balance", 0)) logger.info("🔄 State sync complete — %d accounts", len(chain.state.accounts)) + elif msg_type == "get_chain": + # Peer is requesting our chain + if writer: + chain_data = chain.to_dict_list() + await network.send_chain(writer, chain_data) + logger.info("📤 Sent chain (%d blocks) to peer", len(chain_data)) + + elif msg_type == "chain": + # Received a chain from peer + chain_data = payload if isinstance(payload, list) else [] + if chain_data: + logger.info("📥 Received chain with %d blocks", len(chain_data)) + if chain.replace_chain(chain_data): + logger.info("🔄 Replaced chain with received chain (new height: %d)", len(chain.chain)) + else: + logger.info("🔄 Chain validation: keeping our chain") + elif msg_type == "tx": tx = Transaction(**payload) if mempool.add_transaction(tx): @@ -136,120 +156,210 @@ async def handler(data): ╠════════════════════════════════════════════════╣ ║ balance — show all balances ║ ║ send — send coins ║ -║ mine — mine a block ║ +║ mine — mine a block / toggle ║ ║ peers — show connected peers ║ ║ connect — connect to a peer ║ ║ address — show your public key ║ ║ chain — show chain summary ║ +║ sync — sync chain from peers ║ ║ help — show this help ║ ║ quit — shut down ║ ╚════════════════════════════════════════════════╝ """ -async def cli_loop(sk, pk, chain, mempool, network, nonce_counter): - """Read commands from stdin asynchronously.""" +async def cli_loop(sk, pk, chain, mempool, network, nonce_counter, mining_enabled): + """Read commands from stdin asynchronously with optional auto-mining.""" loop = asyncio.get_event_loop() print(HELP_TEXT) print(f"Your address: {pk}\n") - while True: - try: - raw = await loop.run_in_executor(None, lambda: input("minichain> ")) - except (EOFError, KeyboardInterrupt): - break - - parts = raw.strip().split() - if not parts: - continue - cmd = parts[0].lower() - - # ── balance ── - if cmd == "balance": - accounts = chain.state.accounts - if not accounts: - print(" (no accounts yet)") - for addr, acc in accounts.items(): - tag = " (you)" if addr == pk else "" - print(f" {addr[:12]}... balance={acc['balance']} nonce={acc['nonce']}{tag}") - - # ── send ── - elif cmd == "send": - if len(parts) < 3: - print(" Usage: send ") - continue - receiver = parts[1] + mine_interval = 10 # seconds between auto-mine attempts + running = True + input_queue = asyncio.Queue() + stop_event = threading.Event() + + async def auto_mine_loop(): + """Background task for automatic mining.""" + while running: + if mining_enabled[0]: + # Create coinbase transaction for mining reward + coinbase_tx = Transaction( + sender=State.COINBASE_ADDRESS, + receiver=pk, + amount=50, + nonce=0, + data=None, + signature=None, + ) + + pending_txs = mempool.get_transactions_for_block() + all_txs = [coinbase_tx] + pending_txs + + block = Block( + index=chain.last_block.index + 1, + previous_hash=chain.last_block.hash, + transactions=all_txs, + ) + + try: + mined_block = mine_block(block, difficulty=4, timeout_seconds=5) + if chain.add_block(mined_block): + logger.info("⛏️ Mined block #%d! Hash: %s...", mined_block.index, mined_block.hash[:16]) + await network.broadcast_block(mined_block) + except Exception: + # Mining timeout or error - return transactions to mempool + for tx in pending_txs: + mempool.add_transaction(tx) + + await asyncio.sleep(mine_interval) + + def input_reader_thread(): + """Blocking input reader running in a daemon thread.""" + while not stop_event.is_set(): try: - amount = int(parts[2]) - except ValueError: - print(" Amount must be an integer.") - continue - - nonce = nonce_counter[0] - tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce) - tx.sign(sk) + raw = input("minichain> ") + except (EOFError, KeyboardInterrupt): + loop.call_soon_threadsafe(input_queue.put_nowait, None) + break + loop.call_soon_threadsafe(input_queue.put_nowait, raw) + + # Start background tasks + mine_task = asyncio.create_task(auto_mine_loop()) + input_thread = threading.Thread( + target=input_reader_thread, + name="minichain-input", + daemon=True, + ) + input_thread.start() - if mempool.add_transaction(tx): - nonce_counter[0] += 1 - await network.broadcast_transaction(tx) - print(f" ✅ Tx sent: {amount} coins → {receiver[:12]}...") - else: - print(" ❌ Transaction rejected (invalid sig, duplicate, or mempool full).") - - # ── mine ── - elif cmd == "mine": - mined = mine_and_process_block(chain, mempool, pk) - if mined: - await network.broadcast_block(mined) - # Sync local nonce from chain state - acc = chain.state.get_account(pk) - nonce_counter[0] = acc.get("nonce", 0) - - # ── peers ── - elif cmd == "peers": - print(f" Connected peers: {network.peer_count}") - - # ── connect ── - elif cmd == "connect": - if len(parts) < 2: - print(" Usage: connect :") - continue + try: + while True: try: - host, port_str = parts[1].rsplit(":", 1) - port = int(port_str) - except ValueError: - print(" Invalid format. Use host:port") + raw = await input_queue.get() + if raw is None: + break + except Exception: + break + + parts = raw.strip().split() + if not parts: continue - await network.connect_to_peer(host, port) - - # ── address ── - elif cmd == "address": - print(f" {pk}") - - # ── chain ── - elif cmd == "chain": - print(f" Chain length: {len(chain.chain)} blocks") - for b in chain.chain: - tx_count = len(b.transactions) if b.transactions else 0 - print(f" Block #{b.index} hash={b.hash[:16]}... txs={tx_count}") - - # ── help ── - elif cmd == "help": - print(HELP_TEXT) - - # ── quit ── - elif cmd in ("quit", "exit", "q"): - break + cmd = parts[0].lower() + + # ── balance ── + if cmd == "balance": + accounts = chain.state.accounts + if not accounts: + print(" (no accounts yet)") + for addr, acc in accounts.items(): + tag = " (you)" if addr == pk else "" + print(f" {addr[:12]}... balance={acc['balance']} nonce={acc['nonce']}{tag}") + + # ── send ── + elif cmd == "send": + if len(parts) < 3: + print(" Usage: send ") + continue + receiver = parts[1] + try: + amount = int(parts[2]) + except ValueError: + print(" Amount must be an integer.") + continue + + nonce = nonce_counter[0] + tx = Transaction(sender=pk, receiver=receiver, amount=amount, nonce=nonce) + tx.sign(sk) + + if mempool.add_transaction(tx): + nonce_counter[0] += 1 + await network.broadcast_transaction(tx) + print(f" ✅ Tx sent: {amount} coins → {receiver[:12]}...") + else: + print(" ❌ Transaction rejected (invalid sig, duplicate, or mempool full).") + + # ── mine ── + elif cmd == "mine": + if len(parts) > 1 and parts[1] == "toggle": + mining_enabled[0] = not mining_enabled[0] + print(f" Auto-mining: {'ON' if mining_enabled[0] else 'OFF'}") + else: + # Manual mine + mined = mine_and_process_block(chain, mempool, pk) + if mined: + await network.broadcast_block(mined) + # Sync local nonce from chain state + acc = chain.state.get_account(pk) + nonce_counter[0] = acc.get("nonce", 0) + print(f" Auto-mining: {'ON' if mining_enabled[0] else 'OFF'} (use 'mine toggle' to switch)") + + # ── peers ── + elif cmd == "peers": + print(f" Connected peers: {network.peer_count}") + + # ── connect ── + elif cmd == "connect": + if len(parts) < 2: + print(" Usage: connect :") + continue + try: + host, port_str = parts[1].rsplit(":", 1) + port = int(port_str) + except ValueError: + print(" Invalid format. Use host:port") + continue + await network.connect_to_peer(host, port) + + # ── address ── + elif cmd == "address": + print(f" {pk}") + + # ── chain ── + elif cmd == "chain": + print(f" Chain length: {len(chain.chain)} blocks") + for b in chain.chain: + tx_count = len(b.transactions) if b.transactions else 0 + print(f" Block #{b.index} hash={b.hash[:16]}... txs={tx_count}") + + # ── sync ── + elif cmd == "sync": + if network.peer_count == 0: + print(" No peers connected. Connect to a peer first.") + else: + print(f" Requesting chain from {network.peer_count} peer(s)...") + await network.request_chain() + + # ── help ── + elif cmd == "help": + print(HELP_TEXT) + + # ── quit ── + elif cmd in ("quit", "exit", "q"): + break - else: - print(f" Unknown command: {cmd}. Type 'help' for available commands.") + else: + print(f" Unknown command: {cmd}. Type 'help' for available commands.") + finally: + # Cleanup background tasks + running = False + stop_event.set() + try: + loop.call_soon_threadsafe(input_queue.put_nowait, None) + except Exception: + pass + mine_task.cancel() + try: + await mine_task + except asyncio.CancelledError: + pass # ────────────────────────────────────────────── # Main entry point # ────────────────────────────────────────────── -async def run_node(port: int, connect_to: str | None, fund: int): +async def run_node(port: int, connect_to: str | None, fund: int, auto_mine: bool = False): """Boot the node, optionally connect to a peer, then enter the CLI.""" sk, pk = create_wallet() @@ -257,19 +367,14 @@ async def run_node(port: int, connect_to: str | None, fund: int): mempool = Mempool() network = P2PNetwork() - handler = make_network_handler(chain, mempool) + handler = make_network_handler(chain, mempool, network) network.register_handler(handler) - # When a new peer connects, send our state so they can sync + # When a new peer connects, send our chain so they can sync async def on_peer_connected(writer): - import json as _json - sync_msg = _json.dumps({ - "type": "sync", - "data": {"accounts": chain.state.accounts} - }) + "\n" - writer.write(sync_msg.encode()) - await writer.drain() - logger.info("🔄 Sent state sync to new peer") + chain_data = chain.to_dict_list() + await network.send_chain(writer, chain_data) + logger.info("🔄 Sent chain (%d blocks) to new peer", len(chain_data)) network._on_peer_connected = on_peer_connected @@ -284,15 +389,23 @@ async def on_peer_connected(writer): if connect_to: try: host, peer_port = connect_to.rsplit(":", 1) - await network.connect_to_peer(host, int(peer_port)) + connected = await network.connect_to_peer(host, int(peer_port)) + if connected: + # Request chain sync from the peer we just connected to + await asyncio.sleep(0.5) # Brief delay to allow connection to stabilize + await network.request_chain() except ValueError: logger.error("Invalid --connect format. Use host:port") # Nonce counter kept as a mutable list so the CLI closure can mutate it nonce_counter = [0] + # Auto-mine flag as mutable list + mining_enabled = [auto_mine] + if auto_mine: + logger.info("⛏️ Auto-mining enabled") try: - await cli_loop(sk, pk, chain, mempool, network, nonce_counter) + await cli_loop(sk, pk, chain, mempool, network, nonce_counter, mining_enabled) finally: await network.stop() @@ -302,6 +415,7 @@ def main(): parser.add_argument("--port", type=int, default=9000, help="TCP port to listen on (default: 9000)") parser.add_argument("--connect", type=str, default=None, help="Peer address to connect to (host:port)") parser.add_argument("--fund", type=int, default=100, help="Initial coins to fund this wallet (default: 100)") + parser.add_argument("--mine", action="store_true", help="Enable automatic mining") args = parser.parse_args() logging.basicConfig( @@ -311,7 +425,7 @@ def main(): ) try: - asyncio.run(run_node(args.port, args.connect, args.fund)) + asyncio.run(run_node(args.port, args.connect, args.fund, args.mine)) except KeyboardInterrupt: print("\nNode shut down.") diff --git a/minichain/chain.py b/minichain/chain.py index 78ac73f..9b974eb 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -1,4 +1,5 @@ from .block import Block +from .transaction import Transaction from .state import State from .pow import calculate_hash import logging @@ -12,6 +13,9 @@ class Blockchain: Manages the blockchain, validates blocks, and commits state transitions. """ + # Expected genesis hash (all zeros) + GENESIS_HASH = "0" * 64 + def __init__(self): self.chain = [] self.state = State() @@ -27,7 +31,7 @@ def _create_genesis_block(self): previous_hash="0", transactions=[] ) - genesis_block.hash = "0" * 64 + genesis_block.hash = self.GENESIS_HASH self.chain.append(genesis_block) @property @@ -35,9 +39,15 @@ def last_block(self): """ Returns the most recent block in the chain. """ - with self._lock: # Acquire lock for thread-safe access + with self._lock: # Acquire lock for thread-safe access return self.chain[-1] + @property + def height(self): + """Returns the current chain height (number of blocks).""" + with self._lock: + return len(self.chain) + def add_block(self, block): """ Validates and adds a block to the chain if all transactions succeed. @@ -56,10 +66,19 @@ def add_block(self, block): return False # Verify block hash - if block.hash != calculate_hash(block.to_header_dict()): + computed_hash = calculate_hash(block.to_header_dict()) + if block.hash != computed_hash: logger.warning("Block %s rejected: Invalid hash %s", block.index, block.hash) return False + # Verify proof-of-work meets difficulty target + difficulty = block.difficulty or 0 + if difficulty > 0: + required_prefix = "0" * difficulty + if not computed_hash.startswith(required_prefix): + logger.warning("Block %s rejected: Hash does not meet difficulty %d", block.index, difficulty) + return False + # Validate transactions on a temporary state copy temp_state = self.state.copy() @@ -75,3 +94,169 @@ def add_block(self, block): self.state = temp_state self.chain.append(block) return True + + def validate_chain(self, chain_data: list) -> bool: + """ + Validate a chain received from a peer. + + Checks: + 1. Genesis block matches our expected genesis + 2. Each block's hash is valid + 3. Each block's previous_hash links correctly + 4. All transactions in each block are valid + + Args: + chain_data: List of block dictionaries + + Returns: + True if chain is valid, False otherwise + """ + if not chain_data: + return False + + # Validate genesis block + genesis = chain_data[0] + if genesis.get("hash") != self.GENESIS_HASH: + logger.warning("Chain validation failed: Invalid genesis hash") + return False + + if genesis.get("index") != 0: + logger.warning("Chain validation failed: Genesis index not 0") + return False + + # Validate each subsequent block + temp_state = State() # Fresh state for validation + + for i in range(1, len(chain_data)): + block_data = chain_data[i] + prev_block = chain_data[i - 1] + + # Check index linkage + if block_data.get("index") != prev_block.get("index") + 1: + logger.warning("Chain validation failed: Invalid index at block %d", i) + return False + + # Check previous hash linkage + if block_data.get("previous_hash") != prev_block.get("hash"): + logger.warning("Chain validation failed: Invalid previous_hash at block %d", i) + return False + + # Reconstruct block and verify hash + try: + transactions = [Transaction(**tx) for tx in block_data.get("transactions", [])] + block = Block( + index=block_data.get("index"), + previous_hash=block_data.get("previous_hash"), + transactions=transactions, + timestamp=block_data.get("timestamp"), + difficulty=block_data.get("difficulty") + ) + block.nonce = block_data.get("nonce", 0) + + # Verify hash matches + computed_hash = calculate_hash(block.to_header_dict()) + if block_data.get("hash") != computed_hash: + logger.warning("Chain validation failed: Invalid hash at block %d", i) + return False + + # Verify proof-of-work meets difficulty target + difficulty = block_data.get("difficulty", 0) or 0 + if difficulty > 0: + required_prefix = "0" * difficulty + if not computed_hash.startswith(required_prefix): + logger.warning("Chain validation failed: Hash does not meet difficulty %d at block %d", difficulty, i) + return False + + # Validate and apply transactions + for tx in transactions: + if not temp_state.validate_and_apply(tx): + logger.warning("Chain validation failed: Invalid tx in block %d", i) + return False + + except Exception as e: + logger.warning("Chain validation failed at block %d: %s", i, e) + return False + + return True + + def replace_chain(self, chain_data: list) -> bool: + """ + Replace the current chain with a longer valid chain. + + Uses "longest valid chain wins" rule. + + Args: + chain_data: List of block dictionaries from peer + + Returns: + True if chain was replaced, False otherwise + """ + with self._lock: + # Only replace if longer (or equal during initial sync) + if len(chain_data) < len(self.chain): + logger.info("Received chain shorter than ours (%d < %d)", + len(chain_data), len(self.chain)) + return False + + # If equal length, only replace if it validates (essentially a no-op for same chain) + if len(chain_data) == len(self.chain): + # Validate but don't bother replacing if identical + if self.validate_chain(chain_data): + logger.debug("Received chain same length as ours and valid") + return True # Consider it a successful sync + return False + + # Validate the received chain + if not self.validate_chain(chain_data): + logger.warning("Received chain failed validation") + return False + + # Build new chain and state locally for atomic replacement + logger.info("Replacing chain: %d -> %d blocks", len(self.chain), len(chain_data)) + + new_chain = [] + new_state = State() + + # Add genesis + genesis_block = Block( + index=0, + previous_hash="0", + transactions=[] + ) + genesis_block.hash = self.GENESIS_HASH + new_chain.append(genesis_block) + + # Add each subsequent block + for i in range(1, len(chain_data)): + block_data = chain_data[i] + transactions = [Transaction(**tx) for tx in block_data.get("transactions", [])] + + block = Block( + index=block_data.get("index"), + previous_hash=block_data.get("previous_hash"), + transactions=transactions, + timestamp=block_data.get("timestamp"), + difficulty=block_data.get("difficulty") + ) + block.nonce = block_data.get("nonce", 0) + block.hash = block_data.get("hash") + + # Apply transactions to new state + for tx in transactions: + if not new_state.validate_and_apply(tx): + logger.warning("Chain rebuild failed: Invalid tx in block %d", i) + return False + + new_chain.append(block) + + # Atomically assign new chain and state + self.chain = new_chain + self.state = new_state + + logger.info("Chain replaced successfully. New height: %d", len(self.chain)) + return True + + def to_dict_list(self) -> list: + """Export chain as list of block dictionaries.""" + with self._lock: + return [block.to_dict() for block in self.chain] diff --git a/minichain/mempool.py b/minichain/mempool.py index 06a60d0..8ddba2c 100644 --- a/minichain/mempool.py +++ b/minichain/mempool.py @@ -60,3 +60,16 @@ def get_transactions_for_block(self): self._seen_tx_ids.difference_update(confirmed_ids) return txs + + def get_pending_transactions(self): + """ + Returns a copy of pending transactions without clearing the pool. + Used for inspection/display purposes. + """ + with self._lock: + return self._pending_txs[:] + + def size(self): + """Returns the number of pending transactions.""" + with self._lock: + return len(self._pending_txs) diff --git a/minichain/p2p.py b/minichain/p2p.py index 81ff100..884b83a 100644 --- a/minichain/p2p.py +++ b/minichain/p2p.py @@ -113,7 +113,8 @@ async def _listen_to_peer(self, reader: asyncio.StreamReader, writer: asyncio.St if self._handler_callback: try: - await self._handler_callback(data) + # Pass writer so handler can respond directly to sender + await self._handler_callback(data, writer) except Exception: logger.exception("Network: Handler error for message from %s", addr) except asyncio.CancelledError: @@ -162,6 +163,31 @@ async def broadcast_block(self, block): logger.info("Network: Broadcasting Block #%d", block.index) await self._broadcast_raw({"type": "block", "data": block.to_dict()}) + async def request_chain(self): + """Request the full chain from all connected peers.""" + logger.info("Network: Requesting chain from %d peer(s)...", len(self._peers)) + await self._broadcast_raw({"type": "get_chain", "data": {}}) + + async def send_chain(self, writer: asyncio.StreamWriter, chain_data: list): + """Send the full chain to a specific peer.""" + try: + payload = {"type": "chain", "data": chain_data} + line = (json.dumps(payload) + "\n").encode() + writer.write(line) + await writer.drain() + logger.info("Network: Sent chain (%d blocks) to peer", len(chain_data)) + except Exception as e: + logger.error("Network: Failed to send chain: %s", e) + + async def send_to_peer(self, writer: asyncio.StreamWriter, payload: dict): + """Send a message to a specific peer.""" + try: + line = (json.dumps(payload) + "\n").encode() + writer.write(line) + await writer.drain() + except Exception as e: + logger.error("Network: Failed to send to peer: %s", e) + @property def peer_count(self) -> int: return len(self._peers) diff --git a/minichain/state.py b/minichain/state.py index ce9a6f0..87fd526 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -14,6 +14,7 @@ def __init__(self): self.contract_machine = ContractMachine(self) DEFAULT_MINING_REWARD = 50 + COINBASE_ADDRESS = "0" * 40 # Special address for coinbase transactions def get_account(self, address): if address not in self.accounts: @@ -26,6 +27,10 @@ def get_account(self, address): return self.accounts[address] def verify_transaction_logic(self, tx): + # Coinbase transactions don't need signature/balance/nonce validation + if tx.sender == self.COINBASE_ADDRESS: + return True + if not tx.verify(): logger.error(f"Error: Invalid signature for tx from {tx.sender[:8]}...") return False @@ -74,6 +79,12 @@ def apply_transaction(self, tx): if not self.verify_transaction_logic(tx): return False + # Coinbase transactions only credit the receiver, no deduction from sender + if tx.sender == self.COINBASE_ADDRESS: + receiver = self.get_account(tx.receiver) + receiver['balance'] += tx.amount + return True + sender = self.accounts[tx.sender] # Deduct funds and increment nonce @@ -161,3 +172,16 @@ def credit_mining_reward(self, miner_address, reward=None): reward = reward if reward is not None else self.DEFAULT_MINING_REWARD account = self.get_account(miner_address) account['balance'] += reward + + def update_contract_storage_partial(self, address, updates): + if address not in self.accounts: + raise KeyError(f"Contract address not found: {address}") + if isinstance(updates, dict): + self.accounts[address]['storage'].update(updates) + else: + raise ValueError("Updates must be a dictionary") + + def credit_mining_reward(self, miner_address, reward=None): + reward = reward if reward is not None else self.DEFAULT_MINING_REWARD + account = self.get_account(miner_address) + account['balance'] += reward From d5fd615d4f77c39db90bc06a9291331ccb3dc102 Mon Sep 17 00:00:00 2001 From: anuragShingare30 Date: Thu, 5 Mar 2026 21:46:43 +0530 Subject: [PATCH 2/2] feat: refactoring code --- COMMAND.md | 30 +++++++++++++++++++----------- main.py | 19 ++++++++++++------- minichain/chain.py | 44 +++++++++++++++++++++----------------------- minichain/mempool.py | 39 +++++++++++++++++++++++++++++++++++++++ minichain/p2p.py | 34 +++++++++++++++++++++++++++++++--- minichain/state.py | 13 ------------- 6 files changed, 122 insertions(+), 57 deletions(-) diff --git a/COMMAND.md b/COMMAND.md index 207a4c7..ac20654 100644 --- a/COMMAND.md +++ b/COMMAND.md @@ -1,31 +1,34 @@ +# Commands for testing + ## Commands for testing ### Test 1: Same Machine, Two Terminals + ```bash # activate the virtual environment source .venv/bin/activate # Terminal 1 -python cli.py --port 9000 +python main.py --port 9000 # Terminal 2 -python cli.py --port 9001 --peers 127.0.0.1:9000 +python main.py --port 9001 --connect 127.0.0.1:9000 ``` - ### Test 2: Two Machines, Same LAN + ```bash # Machine A (Ex: 192.168.1.10) -python3 cli.py --port 9000 --mine +python3 main.py --port 9000 --mine # Machine B (Ex: 192.168.1.20) -python3 cli.py --port 9001 --peers 192.168.1.10:9000 +python3 main.py --port 9001 --connect 192.168.1.10:9000 ``` - **Follow the below steps for test 2** **find the IP addresses** + ```bash # run this both on machine A and machine B ip addr | grep "inet " | grep -v 127.0.0.1 @@ -36,13 +39,15 @@ hostname -I ### Machine A (WSL + Windows Port Forwarding) **WSL: Start the node (miner)** + ```bash -cd ~/web2/minichain +cd /path/to/minichain # adjust to your project directory source .venv/bin/activate -python3 cli.py --port 9000 --mine +python3 main.py --port 9000 --mine ``` **Windows PowerShell (Admin): Forward port 9000 to WSL** + ```powershell $wslIp = wsl hostname -I $wslIp = $wslIp.Trim() @@ -54,11 +59,13 @@ netsh interface portproxy show all ### Machine B (Same LAN) **Test connectivity first**: + ```bash ping 192.168.137.10 ``` **Test port first:** + ```bash # linux nc -zv 192.168.137.10 9000 @@ -67,18 +74,19 @@ nc -zv 192.168.137.10 9000 Test-NetConnection -ComputerName 192.168.137.10 -Port 9000 ``` - **If ping works, start the node for (Linux/macOS):** + ```bash cd minichain source .venv/bin/activate -python3 cli.py --port 9001 --peers 192.168.137.10:9000 +python3 main.py --port 9001 --connect 192.168.137.10:9000 ``` **Windows:** + ```powershell cd minichain -python cli.py --port 9001 --peers 192.168.137.10:9000 +python main.py --port 9001 --connect 192.168.137.10:9000 ``` diff --git a/main.py b/main.py index 209272f..d0eb1bd 100644 --- a/main.py +++ b/main.py @@ -138,8 +138,9 @@ async def handler(data, writer=None): miner = payload.get("miner", BURN_ADDRESS) chain.state.credit_mining_reward(miner) - # Drain matching txs from mempool so they aren't re-mined - mempool.get_transactions_for_block() + # Remove only the transactions present in this block + tx_ids = [mempool.get_tx_id_from_dict(t) for t in txs_raw] + mempool.remove_transactions_by_ids(tx_ids) else: logger.warning("📥 Received Block #%s — rejected", block.index) @@ -183,6 +184,7 @@ async def auto_mine_loop(): """Background task for automatic mining.""" while running: if mining_enabled[0]: + pending_preview = mempool.get_pending_transactions() # Create coinbase transaction for mining reward coinbase_tx = Transaction( sender=State.COINBASE_ADDRESS, @@ -192,8 +194,10 @@ async def auto_mine_loop(): data=None, signature=None, ) - - pending_txs = mempool.get_transactions_for_block() + if pending_preview: + pending_txs = mempool.get_transactions_for_block() + else: + pending_txs = [] all_txs = [coinbase_tx] + pending_txs block = Block( @@ -209,8 +213,10 @@ async def auto_mine_loop(): await network.broadcast_block(mined_block) except Exception: # Mining timeout or error - return transactions to mempool - for tx in pending_txs: - mempool.add_transaction(tx) + if pending_txs: + result = mempool.restore_transactions(pending_txs) + if result.get("dropped"): + logger.warning("Mempool restore dropped %d txs", result["dropped"]) await asyncio.sleep(mine_interval) @@ -392,7 +398,6 @@ async def on_peer_connected(writer): connected = await network.connect_to_peer(host, int(peer_port)) if connected: # Request chain sync from the peer we just connected to - await asyncio.sleep(0.5) # Brief delay to allow connection to stabilize await network.request_chain() except ValueError: logger.error("Invalid --connect format. Use host:port") diff --git a/minichain/chain.py b/minichain/chain.py index 9b974eb..675ef18 100644 --- a/minichain/chain.py +++ b/minichain/chain.py @@ -95,7 +95,7 @@ def add_block(self, block): self.chain.append(block) return True - def validate_chain(self, chain_data: list) -> bool: + def validate_chain(self, chain_data: list): """ Validate a chain received from a peer. @@ -109,20 +109,24 @@ def validate_chain(self, chain_data: list) -> bool: chain_data: List of block dictionaries Returns: - True if chain is valid, False otherwise + Tuple (is_valid, validated_state) """ if not chain_data: - return False + return False, None # Validate genesis block genesis = chain_data[0] if genesis.get("hash") != self.GENESIS_HASH: logger.warning("Chain validation failed: Invalid genesis hash") - return False + return False, None if genesis.get("index") != 0: logger.warning("Chain validation failed: Genesis index not 0") - return False + return False, None + + if genesis.get("previous_hash") != "0": + logger.warning("Chain validation failed: Genesis previous_hash not '0'") + return False, None # Validate each subsequent block temp_state = State() # Fresh state for validation @@ -157,7 +161,7 @@ def validate_chain(self, chain_data: list) -> bool: computed_hash = calculate_hash(block.to_header_dict()) if block_data.get("hash") != computed_hash: logger.warning("Chain validation failed: Invalid hash at block %d", i) - return False + return False, None # Verify proof-of-work meets difficulty target difficulty = block_data.get("difficulty", 0) or 0 @@ -165,19 +169,19 @@ def validate_chain(self, chain_data: list) -> bool: required_prefix = "0" * difficulty if not computed_hash.startswith(required_prefix): logger.warning("Chain validation failed: Hash does not meet difficulty %d at block %d", difficulty, i) - return False + return False, None # Validate and apply transactions for tx in transactions: if not temp_state.validate_and_apply(tx): logger.warning("Chain validation failed: Invalid tx in block %d", i) - return False + return False, None except Exception as e: logger.warning("Chain validation failed at block %d: %s", i, e) - return False + return False, None - return True + return True, temp_state def replace_chain(self, chain_data: list) -> bool: """ @@ -189,7 +193,7 @@ def replace_chain(self, chain_data: list) -> bool: chain_data: List of block dictionaries from peer Returns: - True if chain was replaced, False otherwise + True if chain was replaced, False if not replaced """ with self._lock: # Only replace if longer (or equal during initial sync) @@ -198,16 +202,16 @@ def replace_chain(self, chain_data: list) -> bool: len(chain_data), len(self.chain)) return False - # If equal length, only replace if it validates (essentially a no-op for same chain) + # If equal length, validate but do not replace if len(chain_data) == len(self.chain): - # Validate but don't bother replacing if identical - if self.validate_chain(chain_data): + is_valid, _ = self.validate_chain(chain_data) + if is_valid: logger.debug("Received chain same length as ours and valid") - return True # Consider it a successful sync return False # Validate the received chain - if not self.validate_chain(chain_data): + is_valid, validated_state = self.validate_chain(chain_data) + if not is_valid or validated_state is None: logger.warning("Received chain failed validation") return False @@ -215,7 +219,7 @@ def replace_chain(self, chain_data: list) -> bool: logger.info("Replacing chain: %d -> %d blocks", len(self.chain), len(chain_data)) new_chain = [] - new_state = State() + new_state = validated_state # Add genesis genesis_block = Block( @@ -241,12 +245,6 @@ def replace_chain(self, chain_data: list) -> bool: block.nonce = block_data.get("nonce", 0) block.hash = block_data.get("hash") - # Apply transactions to new state - for tx in transactions: - if not new_state.validate_and_apply(tx): - logger.warning("Chain rebuild failed: Invalid tx in block %d", i) - return False - new_chain.append(block) # Atomically assign new chain and state diff --git a/minichain/mempool.py b/minichain/mempool.py index 8ddba2c..f742931 100644 --- a/minichain/mempool.py +++ b/minichain/mempool.py @@ -18,6 +18,10 @@ def _get_tx_id(self, tx): """ return calculate_hash(tx.to_dict()) + def get_tx_id_from_dict(self, tx_dict): + """Compute deterministic tx id from a transaction dictionary.""" + return calculate_hash(tx_dict) + def add_transaction(self, tx): """ Adds a transaction to the pool if: @@ -69,6 +73,41 @@ def get_pending_transactions(self): with self._lock: return self._pending_txs[:] + def restore_transactions(self, transactions): + """ + Restore transactions to the pool without duplicate/seen checks. + Returns a dict with counts for restored and dropped. + """ + restored = 0 + dropped = 0 + with self._lock: + for tx in transactions: + if len(self._pending_txs) >= self.max_size: + dropped += 1 + continue + self._pending_txs.append(tx) + self._seen_tx_ids.add(self._get_tx_id(tx)) + restored += 1 + return {"restored": restored, "dropped": dropped} + + def remove_transactions_by_ids(self, tx_ids): + """Remove transactions matching the provided tx_ids from the pool.""" + ids = set(tx_ids) + if not ids: + return 0 + with self._lock: + kept = [] + removed_ids = set() + for tx in self._pending_txs: + tx_id = self._get_tx_id(tx) + if tx_id in ids: + removed_ids.add(tx_id) + continue + kept.append(tx) + self._pending_txs = kept + self._seen_tx_ids.difference_update(removed_ids) + return len(removed_ids) + def size(self): """Returns the number of pending transactions.""" with self._lock: diff --git a/minichain/p2p.py b/minichain/p2p.py index 884b83a..6aec213 100644 --- a/minichain/p2p.py +++ b/minichain/p2p.py @@ -31,6 +31,7 @@ def __init__(self, handler_callback=None): self._port: int = 0 self._listen_tasks: list[asyncio.Task] = [] self._on_peer_connected = None # callback(writer) called when a new peer connects + self._ready_events: dict[str, asyncio.Event] = {} def register_handler(self, handler_callback): if not callable(handler_callback): @@ -75,8 +76,25 @@ async def connect_to_peer(self, host: str, port: int) -> bool: try: reader, writer = await asyncio.open_connection(host, port) self._peers.append((reader, writer)) - task = asyncio.create_task(self._listen_to_peer(reader, writer, f"{host}:{port}")) + addr = f"{host}:{port}" + ready_event = asyncio.Event() + self._ready_events[addr] = ready_event + task = asyncio.create_task(self._listen_to_peer(reader, writer, addr)) self._listen_tasks.append(task) + await self.send_to_peer(writer, {"type": "ready"}) + try: + await asyncio.wait_for(ready_event.wait(), timeout=5.0) + except asyncio.TimeoutError: + logger.error("Network: Ready handshake timeout for %s", addr) + self._ready_events.pop(addr, None) + try: + writer.close() + await writer.wait_closed() + except Exception: + pass + if (reader, writer) in self._peers: + self._peers.remove((reader, writer)) + return False logger.info("Network: Connected to peer %s:%d", host, port) return True except Exception as e: @@ -111,6 +129,16 @@ async def _listen_to_peer(self, reader: asyncio.StreamReader, writer: asyncio.St logger.warning("Network: Malformed message from %s", addr) continue + msg_type = data.get("type") + if msg_type == "ready": + await self.send_to_peer(writer, {"type": "ready_ack"}) + continue + if msg_type == "ready_ack": + event = self._ready_events.pop(addr, None) + if event: + event.set() + continue + if self._handler_callback: try: # Pass writer so handler can respond directly to sender @@ -177,7 +205,7 @@ async def send_chain(self, writer: asyncio.StreamWriter, chain_data: list): await writer.drain() logger.info("Network: Sent chain (%d blocks) to peer", len(chain_data)) except Exception as e: - logger.error("Network: Failed to send chain: %s", e) + logger.exception("Network: Failed to send chain: %s", e) async def send_to_peer(self, writer: asyncio.StreamWriter, payload: dict): """Send a message to a specific peer.""" @@ -186,7 +214,7 @@ async def send_to_peer(self, writer: asyncio.StreamWriter, payload: dict): writer.write(line) await writer.drain() except Exception as e: - logger.error("Network: Failed to send to peer: %s", e) + logger.exception("Network: Failed to send to peer: %s", e) @property def peer_count(self) -> int: diff --git a/minichain/state.py b/minichain/state.py index 87fd526..19f2c95 100644 --- a/minichain/state.py +++ b/minichain/state.py @@ -172,16 +172,3 @@ def credit_mining_reward(self, miner_address, reward=None): reward = reward if reward is not None else self.DEFAULT_MINING_REWARD account = self.get_account(miner_address) account['balance'] += reward - - def update_contract_storage_partial(self, address, updates): - if address not in self.accounts: - raise KeyError(f"Contract address not found: {address}") - if isinstance(updates, dict): - self.accounts[address]['storage'].update(updates) - else: - raise ValueError("Updates must be a dictionary") - - def credit_mining_reward(self, miner_address, reward=None): - reward = reward if reward is not None else self.DEFAULT_MINING_REWARD - account = self.get_account(miner_address) - account['balance'] += reward