From f6ae2166dd560299b15f545c18902969fbc26996 Mon Sep 17 00:00:00 2001 From: anuragShingare30 Date: Sat, 21 Feb 2026 19:34:34 +0530 Subject: [PATCH] feat: folder refactor, CLI integration for easy testing --- .gitignore | 124 ++++++ cli.py | 659 +++++++++++++++++++++++++++++ consensus/__init__.py | 3 - core/__init__.py | 13 - core/chain.py | 77 ---- main.py | 5 +- minichain.sh | 4 + minichain/.gitignore | 4 + minichain/__init__.py | 32 ++ {core => minichain}/block.py | 37 +- minichain/chain.py | 160 +++++++ minichain/config.py | 33 ++ {core => minichain}/contract.py | 0 {node => minichain}/mempool.py | 18 +- minichain/p2p.py | 376 ++++++++++++++++ {consensus => minichain}/pow.py | 0 {core => minichain}/state.py | 97 ++++- {core => minichain}/transaction.py | 64 +++ network/__init__.py | 3 - network/p2p.py | 89 ---- node/__init__.py | 3 - tests/test_contract.py | 2 +- tests/test_core.py | 5 +- 23 files changed, 1610 insertions(+), 198 deletions(-) create mode 100755 cli.py delete mode 100644 consensus/__init__.py delete mode 100644 core/__init__.py delete mode 100644 core/chain.py create mode 100755 minichain.sh create mode 100644 minichain/.gitignore create mode 100644 minichain/__init__.py rename {core => minichain}/block.py (68%) create mode 100644 minichain/chain.py create mode 100644 minichain/config.py rename {core => minichain}/contract.py (100%) rename {node => minichain}/mempool.py (76%) create mode 100644 minichain/p2p.py rename {consensus => minichain}/pow.py (100%) rename {core => minichain}/state.py (59%) rename {core => minichain}/transaction.py (51%) delete mode 100644 network/__init__.py delete mode 100644 network/p2p.py delete mode 100644 node/__init__.py diff --git a/.gitignore b/.gitignore index 9308a4b..d9547b3 100644 --- a/.gitignore +++ b/.gitignore @@ -324,3 +324,127 @@ TSWLatexianTemp* # option is specified. Footnotes are the stored in a file with suffix Notes.bib. # Uncomment the next line to have this generated file ignored. #*Notes.bib + +# MiniChain wallet keys (security - never commit private keys) +wallets/ +*.key + + +# ================================ +# Python / MiniChain Project +# ================================ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# Environment +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log +logs/ + +# Local development +.env.local +.env.*.local diff --git a/cli.py b/cli.py new file mode 100755 index 0000000..62083da --- /dev/null +++ b/cli.py @@ -0,0 +1,659 @@ +#!/usr/bin/env python3 +""" +MiniChain CLI - Node-based blockchain interface with P2P networking. + +Usage: + python3 cli.py --port 8000 # Start node on port 8000 + python3 cli.py --port 8001 --connect 8000 # Start and connect to peer +""" + +import argparse +import json +import os +import sys +import logging +import time +from typing import Optional + +from nacl.signing import SigningKey +from nacl.encoding import HexEncoder + +from minichain import ( + Block, + Blockchain, + Transaction, + Mempool, + mine_block, + calculate_hash, +) +from minichain.p2p import P2PNetwork +from minichain.config import TREASURY_ADDRESS, TREASURY_PRIVATE_KEY, DIFFICULTY + +# Logging - quieter by default +logging.basicConfig(level=logging.WARNING, format='%(levelname)s: %(message)s') +logger = logging.getLogger(__name__) + +WALLET_DIR = "wallets" + +# ANSI Colors +class Colors: + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + BOLD = '\033[1m' + DIM = '\033[2m' + RESET = '\033[0m' + +def c(text, color): + """Colorize text.""" + return f"{color}{text}{Colors.RESET}" + + +class Node: + """Blockchain node with wallet, chain, mempool, and P2P.""" + + def __init__(self, port: int): + self.port = port + self.blockchain = Blockchain() + self.mempool = Mempool() + self.p2p = P2PNetwork(self, port) + + # Load or create wallet for this port + self.wallet_name = f"wallet_{port}" + self.signing_key, self.address = self._load_or_create_wallet() + + def _load_or_create_wallet(self): + """Load existing wallet or create new one for this port.""" + os.makedirs(WALLET_DIR, exist_ok=True) + path = os.path.join(WALLET_DIR, f"{self.wallet_name}.key") + + if os.path.exists(path): + with open(path) as f: + sk = SigningKey(bytes.fromhex(f.read().strip())) + logger.info(f"Loaded wallet from {path}") + else: + sk = SigningKey.generate() + with open(path, "w") as f: + f.write(sk.encode().hex()) + logger.info(f"Created new wallet: {path}") + + pk = sk.verify_key.encode(encoder=HexEncoder).decode() + return sk, pk + + @property + def chain(self): + return self.blockchain + + +class MiniChainCLI: + """Node-based CLI for MiniChain with P2P networking.""" + + def __init__(self, port: int, connect_port: Optional[int] = None): + self.node = Node(port) + self.connect_port = connect_port + self.nonce_map = {} + + # === NODE INFO === + + def cmd_address(self, args): + """address - Show node wallet address""" + addr = self.node.address + return ( + f"{c('Address:', Colors.CYAN)} {addr}\n" + f"{c('Short:', Colors.DIM)} {addr[:16]}...{addr[-8:]}" + ) + + def cmd_peers(self, args): + """peers - Show connected peers""" + peers = self.node.p2p.peer_list + if not peers: + return c("No connected peers", Colors.DIM) + header = c(f"Connected Peers ({len(peers)}):", Colors.CYAN) + peer_list = "\n".join(f" • {p}" for p in peers) + return f"{header}\n{peer_list}" + + def cmd_status(self, args): + """status - Show node status overview""" + bal = self.node.blockchain.state.get_balance(self.node.address) + peers = len(self.node.p2p.peer_list) + height = self.node.blockchain.height + mempool = len(self.node.mempool._pending_txs) + + return ( + f"{c('═══ Node Status ═══', Colors.BOLD)}\n" + f" Port: {c(self.node.port, Colors.CYAN)}\n" + f" Balance: {c(f'{bal:,}', Colors.GREEN)} coins\n" + f" Peers: {c(peers, Colors.YELLOW)}\n" + f" Height: {c(height, Colors.BLUE)} blocks\n" + f" Mempool: {c(mempool, Colors.DIM)} pending tx" + ) + + def cmd_treasury(self, args): + """treasury - Show treasury info""" + bal = self.node.blockchain.state.get_balance(TREASURY_ADDRESS) + return ( + f"{c('═══ Treasury ═══', Colors.BOLD)}\n" + f" Address: {TREASURY_ADDRESS[:24]}...\n" + f" Balance: {c(f'{bal:,}', Colors.GREEN)} coins" + ) + + def cmd_faucet(self, args): + """faucet [amount] - Request coins from treasury (default: 100)""" + try: + amt = int(args[0]) if args else 100 + except ValueError: + return c("Amount must be a number", Colors.RED) + + if amt > 10000: + return c("Maximum faucet amount is 10,000 coins", Colors.RED) + + # Check treasury balance + treasury_bal = self.node.blockchain.state.get_balance(TREASURY_ADDRESS) + if treasury_bal < amt: + return c(f"Treasury has insufficient funds ({treasury_bal})", Colors.RED) + + # Create transaction from treasury to node wallet + nonce = self.node.blockchain.state.get_nonce(TREASURY_ADDRESS) + tx = Transaction( + sender=TREASURY_ADDRESS, + receiver=self.node.address, + amount=amt, + nonce=nonce + ) + tx.sign_with_hex(TREASURY_PRIVATE_KEY) + + if self.node.mempool.add_transaction(tx): + return ( + f"{c('✓', Colors.GREEN)} Faucet request: {c(f'{amt}', Colors.GREEN)} coins\n" + f" To: {self.node.address[:24]}...\n" + f" {c('→ Run', Colors.DIM)} {c('mine', Colors.YELLOW)} {c('to confirm', Colors.DIM)}" + ) + return c("✗ Faucet request rejected", Colors.RED) + + # === BLOCKCHAIN === + + def cmd_chain(self, args): + """chain - Show blockchain info""" + last = self.node.blockchain.last_block + height = self.node.blockchain.height + return ( + f"{c('═══ Blockchain ═══', Colors.BOLD)}\n" + f" Height: {c(height, Colors.CYAN)} blocks\n" + f" Last Block: {c(f'#{last.index}', Colors.BLUE)}\n" + f" Last Hash: {last.hash[:32]}...\n" + f" Difficulty: {c(DIFFICULTY, Colors.YELLOW)}" + ) + + def cmd_block(self, args): + """block [n] - Show block details (default: latest)""" + if args: + try: + idx = int(args[0]) + except ValueError: + return c("Index must be a number", Colors.RED) + else: + idx = self.node.blockchain.last_block.index + + if idx < 0 or idx >= len(self.node.blockchain.chain): + return c(f"Block {idx} not found", Colors.RED) + + b = self.node.blockchain.chain[idx] + tx_count = len(b.transactions) + + lines = [ + f"{c(f'═══ Block #{b.index} ═══', Colors.BOLD)}", + f" Hash: {b.hash[:32]}...", + f" Prev Hash: {b.previous_hash[:32]}...", + f" Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(b.timestamp/1000))}", + f" Nonce: {b.nonce}", + f" Txs: {c(tx_count, Colors.CYAN)}", + ] + + if tx_count > 0: + lines.append(f"\n {c('Transactions:', Colors.DIM)}") + for i, tx in enumerate(b.transactions[:5]): + sender = tx.sender[:8] if tx.sender else "COINBASE" + receiver = tx.receiver[:8] if tx.receiver else "CONTRACT" + lines.append(f" {i+1}. {sender}... → {receiver}... ({tx.amount})") + if tx_count > 5: + lines.append(f" ... and {tx_count - 5} more") + + return "\n".join(lines) + + def cmd_mine(self, args): + """mine - Mine pending transactions""" + miner = self.node.address + txs = self.node.mempool.get_transactions_for_block() + + print(c("Mining...", Colors.DIM), end=" ", flush=True) + + block = Block( + index=self.node.blockchain.last_block.index + 1, + previous_hash=self.node.blockchain.last_block.hash, + transactions=txs, + difficulty=DIFFICULTY, + ) + + try: + start = time.time() + mined = mine_block(block, difficulty=DIFFICULTY) + elapsed = time.time() - start + + if self.node.blockchain.add_block(mined): + self.node.blockchain.state.credit_mining_reward(miner) + for tx in txs: + self._sync_nonce(tx.sender) + + # Broadcast to peers + self.node.p2p.broadcast_block_sync(mined) + + new_bal = self.node.blockchain.state.get_balance(miner) + + return ( + f"\r{c('✓ Block Mined!', Colors.GREEN)} \n" + f" Block: {c(f'#{mined.index}', Colors.CYAN)}\n" + f" Hash: {mined.hash[:24]}...\n" + f" Txs: {len(txs)} processed\n" + f" Time: {elapsed:.2f}s\n" + f" Reward: {c('+50', Colors.GREEN)} coins\n" + f" Balance: {c(f'{new_bal:,}', Colors.GREEN)} coins" + ) + return c("\r✗ Block rejected", Colors.RED) + except Exception as e: + return c(f"\r✗ Mining failed: {e}", Colors.RED) + + # === TRANSACTIONS === + + def cmd_send(self, args): + """send - Send coins""" + if len(args) < 2: + return f"Usage: {c('send
', Colors.YELLOW)}" + + to = args[0] + try: + amt = int(args[1]) + except ValueError: + return c("Amount must be a number", Colors.RED) + + # Check balance + bal = self.node.blockchain.state.get_balance(self.node.address) + if bal < amt: + return c(f"Insufficient balance ({bal} < {amt})", Colors.RED) + + sender = self.node.address + nonce = self._get_nonce(sender) + + tx = Transaction(sender=sender, receiver=to, amount=amt, nonce=nonce) + tx.sign(self.node.signing_key) + + if self.node.mempool.add_transaction(tx): + # Broadcast to peers + self.node.p2p.broadcast_transaction_sync(tx) + + return ( + f"{c('✓', Colors.GREEN)} Transaction submitted\n" + f" From: {self.node.wallet_name}\n" + f" To: {to[:24]}...\n" + f" Amount: {c(amt, Colors.YELLOW)} coins\n" + f" {c('→ Run', Colors.DIM)} {c('mine', Colors.YELLOW)} {c('to confirm', Colors.DIM)}" + ) + return c("✗ Transaction rejected", Colors.RED) + + def cmd_deploy(self, args): + """deploy - Deploy a smart contract""" + print(f"{c('Enter contract code (empty line to finish):', Colors.CYAN)}") + lines = [] + while True: + try: + line = input(c(" | ", Colors.DIM)) + if line == "": + break + lines.append(line) + except EOFError: + break + + if not lines: + return c("No code entered", Colors.RED) + + code = "\n".join(lines) + sender = self.node.address + nonce = self._get_nonce(sender) + + tx = Transaction(sender=sender, receiver=None, amount=0, nonce=nonce, data=code) + tx.sign(self.node.signing_key) + + if self.node.mempool.add_transaction(tx): + addr = self.node.blockchain.state.derive_contract_address(sender, nonce + 1) + return ( + f"{c('✓', Colors.GREEN)} Contract deployment submitted\n" + f" Expected address: {addr[:32]}...\n" + f" {c('→ Run', Colors.DIM)} {c('mine', Colors.YELLOW)} {c('to deploy', Colors.DIM)}" + ) + return c("✗ Deployment rejected", Colors.RED) + + def cmd_call(self, args): + """call - Call contract""" + if len(args) < 2: + return f"Usage: {c('call ', Colors.YELLOW)}" + + contract, data = args[0], " ".join(args[1:]) + sender = self.node.address + nonce = self._get_nonce(sender) + + tx = Transaction(sender=sender, receiver=contract, amount=0, nonce=nonce, data=data) + tx.sign(self.node.signing_key) + + if self.node.mempool.add_transaction(tx): + return ( + f"{c('✓', Colors.GREEN)} Contract call submitted\n" + f" {c('→ Run', Colors.DIM)} {c('mine', Colors.YELLOW)} {c('to execute', Colors.DIM)}" + ) + return c("✗ Call rejected", Colors.RED) + + # === ACCOUNT === + + def cmd_balance(self, args): + """balance [address] - Check balance""" + if args: + addr = args[0] + label = addr[:16] + "..." + else: + addr = self.node.address + label = self.node.wallet_name + + bal = self.node.blockchain.state.get_balance(addr) + return f"{c(label, Colors.CYAN)}: {c(f'{bal:,}', Colors.GREEN)} coins" + + def cmd_account(self, args): + """account [address] - Full account info""" + if args: + addr = args[0] + else: + addr = self.node.address + + acc = self.node.blockchain.state.get_account(addr) + is_contract = acc["code"] is not None + balance = acc["balance"] + nonce = acc["nonce"] + + lines = [ + f"{c('═══ Account ═══', Colors.BOLD)}", + f" Address: {addr[:32]}...", + f" Balance: {c(f'{balance:,}', Colors.GREEN)} coins", + f" Nonce: {nonce}", + f" Type: {c('Contract', Colors.HEADER) if is_contract else 'Wallet'}", + ] + + if is_contract and acc.get("storage"): + lines.append(f"\n {c('Storage:', Colors.DIM)}") + for k, v in list(acc["storage"].items())[:3]: + lines.append(f" {k}: {v}") + + return "\n".join(lines) + + # === MEMPOOL === + + def cmd_mempool(self, args): + """mempool - Show pending transactions""" + txs = self.node.mempool._pending_txs + if not txs: + return c("Mempool: empty", Colors.DIM) + + lines = [f"{c(f'═══ Mempool ({len(txs)} pending) ═══', Colors.BOLD)}"] + for i, tx in enumerate(txs[:8]): + sender = tx.sender[:8] if tx.sender else "COINBASE" + to = tx.receiver[:8] + "..." if tx.receiver else "CONTRACT" + lines.append(f" {i+1}. {sender}... → {to} ({c(tx.amount, Colors.YELLOW)})") + if len(txs) > 8: + lines.append(c(f" ... and {len(txs) - 8} more", Colors.DIM)) + return "\n".join(lines) + + # === DEMO & HELP === + + def cmd_demo(self, args): + """demo - Run a quick demo workflow""" + steps = [ + ("Checking status...", "status", []), + ("Requesting 500 coins from faucet...", "faucet", ["500"]), + ("Mining block to confirm faucet...", "mine", []), + ("Checking balance...", "balance", []), + ("Viewing latest block...", "block", []), + ] + + print(f"\n{c('═══ MiniChain Demo ═══', Colors.BOLD)}") + print(c("Running through basic workflow...\n", Colors.DIM)) + + for desc, cmd, cmd_args in steps: + print(f"{c('→', Colors.CYAN)} {desc}") + time.sleep(0.3) + + method = getattr(self, f"cmd_{cmd}") + result = method(cmd_args) + if result: + # Indent the result + for line in result.split('\n'): + print(f" {line}") + print() + time.sleep(0.5) + + print(f"{c('Demo complete!', Colors.GREEN)} Your node is ready.") + print(c("Try: send
, then mine", Colors.DIM)) + return None + + def cmd_quickstart(self, args): + """quickstart - Show quick start guide""" + return f""" +{c('═══ Quick Start Guide ═══', Colors.BOLD)} + +{c('1. Get coins:', Colors.CYAN)} + faucet 1000 {c('# Request 1000 coins from treasury', Colors.DIM)} + mine {c('# Mine to confirm the transaction', Colors.DIM)} + +{c('2. Check your balance:', Colors.CYAN)} + balance {c('# Shows your current balance', Colors.DIM)} + status {c('# Full node status overview', Colors.DIM)} + +{c('3. Send coins:', Colors.CYAN)} + send 100 {c('# Send 100 coins to address', Colors.DIM)} + mine {c('# Mine to confirm', Colors.DIM)} + +{c('4. Multi-node testing:', Colors.CYAN)} + {c('Terminal 1:', Colors.DIM)} python3 cli.py --port 8000 + {c('Terminal 2:', Colors.DIM)} python3 cli.py --port 8001 --connect 8000 + +{c('5. Useful commands:', Colors.CYAN)} + peers {c('# List connected peers', Colors.DIM)} + chain {c('# Blockchain info', Colors.DIM)} + block [n] {c('# View block details', Colors.DIM)} + mempool {c('# Pending transactions', Colors.DIM)} + demo {c('# Run automated demo', Colors.DIM)} +""" + + # === HELPERS === + + def _get_nonce(self, address): + acc_nonce = self.node.blockchain.state.get_account(address).get("nonce", 0) + local = self.nonce_map.get(address, acc_nonce) + nonce = max(acc_nonce, local) + self.nonce_map[address] = nonce + 1 + return nonce + + def _sync_nonce(self, address): + self.nonce_map[address] = self.node.blockchain.state.get_account(address).get("nonce", 0) + + # === MAIN === + + def run(self): + """Run interactive CLI with P2P networking.""" + # Start P2P server in background thread + try: + self.node.p2p.start_background() + except Exception as e: + print(c(f"Failed to start P2P: {e}", Colors.RED)) + return + + # Connect to peer if specified + if self.connect_port: + print(c(f"Connecting to peer on port {self.connect_port}...", Colors.DIM)) + self.node.p2p.connect_to_peer_sync("127.0.0.1", self.connect_port) + + self._print_banner() + + cmds = { + # Help & Demo + "help": self.cmd_help, + "?": self.cmd_help, + "quickstart": self.cmd_quickstart, + "demo": self.cmd_demo, + + # Exit + "exit": self._exit, + "quit": self._exit, + "q": self._exit, + + # Node info + "address": self.cmd_address, + "addr": self.cmd_address, + "peers": self.cmd_peers, + "status": self.cmd_status, + "treasury": self.cmd_treasury, + "faucet": self.cmd_faucet, + + # Blockchain + "chain": self.cmd_chain, + "block": self.cmd_block, + "mine": self.cmd_mine, + + # Transactions + "send": self.cmd_send, + "deploy": self.cmd_deploy, + "call": self.cmd_call, + + # Account + "balance": self.cmd_balance, + "bal": self.cmd_balance, + "account": self.cmd_account, + "mempool": self.cmd_mempool, + "pool": self.cmd_mempool, + } + + while True: + try: + bal = self.node.blockchain.state.get_balance(self.node.address) + prompt = f"{c(f'[:{self.node.port}]', Colors.CYAN)} {c(f'({bal})', Colors.GREEN)} › " + line = input(prompt).strip() + if not line: + continue + + parts = line.split() + cmd, args = parts[0].lower(), parts[1:] + + if cmd in cmds: + result = cmds[cmd](args) + if result: + print(result) + print() + else: + print(c(f"Unknown command: {cmd}", Colors.RED)) + print(c("Type 'help' for available commands.\n", Colors.DIM)) + + except KeyboardInterrupt: + print(c("\n(Use 'exit' or 'q' to quit)", Colors.DIM)) + except EOFError: + break + except Exception as e: + print(c(f"Error: {e}", Colors.RED)) + print() + + self._cleanup() + + def _exit(self, args): + print(c("\nShutting down...", Colors.DIM)) + self._cleanup() + sys.exit(0) + + def _cleanup(self): + self.node.p2p.stop_background() + + def _print_banner(self): + bal = self.node.blockchain.state.get_balance(self.node.address) + treasury = self.node.blockchain.state.get_balance(TREASURY_ADDRESS) + peers = len(self.node.p2p.peer_list) + + print(f""" +{c('╔════════════════════════════════════════════════════════╗', Colors.CYAN)} +{c('║', Colors.CYAN)} {c('⛓️ MiniChain Node', Colors.BOLD)} {c('║', Colors.CYAN)} +{c('║', Colors.CYAN)} {c('Educational Blockchain Implementation', Colors.DIM)} {c('║', Colors.CYAN)} +{c('╚════════════════════════════════════════════════════════╝', Colors.CYAN)} + + {c('Port:', Colors.DIM)} {c(self.node.port, Colors.CYAN)} + {c('Wallet:', Colors.DIM)} {self.node.wallet_name} + {c('Address:', Colors.DIM)} {self.node.address[:32]}... + {c('Balance:', Colors.DIM)} {c(f'{bal:,}', Colors.GREEN)} coins + {c('Peers:', Colors.DIM)} {c(peers, Colors.YELLOW)} + {c('Treasury:', Colors.DIM)} {c(f'{treasury:,}', Colors.DIM)} coins available + + Type {c('help', Colors.YELLOW)} for commands or {c('quickstart', Colors.YELLOW)} for a guide. + Type {c('demo', Colors.YELLOW)} to run an automated demo. +""") + + def cmd_help(self, args): + """Show help""" + return f""" +{c('═══════════════════════════════════════════════════════════', Colors.BOLD)} +{c(' MINICHAIN COMMANDS', Colors.BOLD)} +{c('═══════════════════════════════════════════════════════════', Colors.BOLD)} + +{c('GETTING STARTED', Colors.CYAN)} + {c('help, ?', Colors.YELLOW)} Show this help + {c('quickstart', Colors.YELLOW)} Show quick start guide + {c('demo', Colors.YELLOW)} Run automated demo + {c('status', Colors.YELLOW)} Node status overview + +{c('WALLET & FUNDS', Colors.CYAN)} + {c('address, addr', Colors.YELLOW)} Show wallet address + {c('balance, bal', Colors.YELLOW)} Check balance [address] + {c('account', Colors.YELLOW)} Account details [address] + {c('faucet', Colors.YELLOW)} Get coins from treasury [amount] + +{c('BLOCKCHAIN', Colors.CYAN)} + {c('chain', Colors.YELLOW)} Blockchain info + {c('block', Colors.YELLOW)} View block details [index] + {c('mine', Colors.YELLOW)} Mine pending transactions + +{c('TRANSACTIONS', Colors.CYAN)} + {c('send', Colors.YELLOW)} Send coins
+ {c('mempool, pool', Colors.YELLOW)} View pending transactions + +{c('SMART CONTRACTS', Colors.CYAN)} + {c('deploy', Colors.YELLOW)} Deploy a contract + {c('call', Colors.YELLOW)} Call contract
+ +{c('NETWORK', Colors.CYAN)} + {c('peers', Colors.YELLOW)} List connected peers + {c('treasury', Colors.YELLOW)} Treasury info + +{c('EXIT', Colors.CYAN)} + {c('exit, quit, q', Colors.YELLOW)} Quit the CLI + +{c('═══════════════════════════════════════════════════════════', Colors.DIM)} +""" + + +def main(): + parser = argparse.ArgumentParser(description="MiniChain Node CLI") + parser.add_argument("--port", "-p", type=int, default=8000, + help="Port to listen on (default: 8000)") + parser.add_argument("--connect", "-c", type=int, default=None, + help="Port of peer to connect to") + args = parser.parse_args() + + cli = MiniChainCLI(port=args.port, connect_port=args.connect) + cli.run() + + +if __name__ == "__main__": + main() diff --git a/consensus/__init__.py b/consensus/__init__.py deleted file mode 100644 index 119baf4..0000000 --- a/consensus/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .pow import mine_block, calculate_hash, MiningExceededError - -__all__ = ["mine_block", "calculate_hash", "MiningExceededError"] \ No newline at end of file diff --git a/core/__init__.py b/core/__init__.py deleted file mode 100644 index ce204c7..0000000 --- a/core/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from .block import Block -from .chain import Blockchain -from .transaction import Transaction -from .state import State -from .contract import ContractMachine - -__all__ = [ - "Block", - "Blockchain", - "Transaction", - "State", - "ContractMachine", -] diff --git a/core/chain.py b/core/chain.py deleted file mode 100644 index 9545864..0000000 --- a/core/chain.py +++ /dev/null @@ -1,77 +0,0 @@ -from core.block import Block -from core.state import State -from consensus import calculate_hash -import logging -import threading - -logger = logging.getLogger(__name__) - - -class Blockchain: - """ - Manages the blockchain, validates blocks, and commits state transitions. - """ - - def __init__(self): - self.chain = [] - self.state = State() - self._lock = threading.RLock() - self._create_genesis_block() - - def _create_genesis_block(self): - """ - Creates the genesis block with a fixed hash. - """ - genesis_block = Block( - index=0, - previous_hash="0", - transactions=[] - ) - genesis_block.hash = "0" * 64 - self.chain.append(genesis_block) - - @property - def last_block(self): - """ - Returns the most recent block in the chain. - """ - with self._lock: # Acquire lock for thread-safe access - return self.chain[-1] - - 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. - """ - - with self._lock: - # Check previous hash linkage - if block.previous_hash != self.last_block.hash: - logger.warning("Block %s rejected: Invalid previous hash %s != %s", block.index, block.previous_hash, self.last_block.hash) - return False - - # Check index linkage - if block.index != self.last_block.index + 1: - logger.warning("Block %s rejected: Invalid index %s != %s", block.index, block.index, self.last_block.index + 1) - return False - - # Verify block hash - if block.hash != calculate_hash(block.to_header_dict()): - logger.warning("Block %s rejected: Invalid hash %s", block.index, block.hash) - return False - - # Validate transactions on a temporary state copy - temp_state = self.state.copy() - - for tx in block.transactions: - result = temp_state.validate_and_apply(tx) - - # Reject block if any transaction fails - if not result: - logger.warning("Block %s rejected: Transaction failed validation", block.index) - return False - - # All transactions valid → commit state and append block - self.state = temp_state - self.chain.append(block) - return True diff --git a/main.py b/main.py index d9670c0..ca39584 100644 --- a/main.py +++ b/main.py @@ -4,10 +4,7 @@ from nacl.signing import SigningKey from nacl.encoding import HexEncoder -from core import Transaction, Blockchain, Block, State -from node import Mempool -from network import P2PNetwork -from consensus import mine_block +from minichain import Transaction, Blockchain, Block, State, Mempool, P2PNetwork, mine_block logger = logging.getLogger(__name__) diff --git a/minichain.sh b/minichain.sh new file mode 100755 index 0000000..c110d03 --- /dev/null +++ b/minichain.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# MiniChain CLI launcher +cd "$(dirname "$0")" +python3 cli.py "$@" diff --git a/minichain/.gitignore b/minichain/.gitignore new file mode 100644 index 0000000..ea09685 --- /dev/null +++ b/minichain/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so \ No newline at end of file diff --git a/minichain/__init__.py b/minichain/__init__.py new file mode 100644 index 0000000..9bd5eb8 --- /dev/null +++ b/minichain/__init__.py @@ -0,0 +1,32 @@ +# Core modules +from .block import Block +from .chain import Blockchain +from .transaction import Transaction +from .state import State +from .contract import ContractMachine + +# Consensus +from .pow import mine_block, calculate_hash, MiningExceededError + +# Network +from .p2p import P2PNetwork + +# Node +from .mempool import Mempool + +__all__ = [ + # Core + "Block", + "Blockchain", + "Transaction", + "State", + "ContractMachine", + # Consensus + "mine_block", + "calculate_hash", + "MiningExceededError", + # Network + "P2PNetwork", + # Node + "Mempool", +] diff --git a/core/block.py b/minichain/block.py similarity index 68% rename from core/block.py rename to minichain/block.py index 23f7536..bbb557d 100644 --- a/core/block.py +++ b/minichain/block.py @@ -2,7 +2,8 @@ import hashlib import json from typing import List, Optional -from core.transaction import Transaction +from minichain.transaction import Transaction, create_genesis_tx +from minichain.config import GENESIS_TIMESTAMP, TREASURY_ADDRESS, TREASURY_BALANCE def _sha256(data: str) -> str: @@ -103,3 +104,37 @@ def compute_hash(self) -> str: sort_keys=True ) return _sha256(header_string) + + @staticmethod + def from_dict(data: dict) -> "Block": + """Create block from dictionary.""" + txs = [Transaction.from_dict(tx) for tx in data.get("transactions", [])] + block = Block( + index=data["index"], + previous_hash=data["previous_hash"], + transactions=txs, + timestamp=data.get("timestamp"), + difficulty=data.get("difficulty"), + ) + block.nonce = data.get("nonce", 0) + block.hash = data.get("hash") + return block + + def __repr__(self): + return f"Block(#{self.index}, txs={len(self.transactions)}, hash={self.hash[:8] if self.hash else 'None'})" + + +def create_genesis_block() -> "Block": + """Create the genesis (first) block with treasury funds.""" + genesis_txs = [create_genesis_tx(TREASURY_ADDRESS, TREASURY_BALANCE)] + + block = Block( + index=0, + previous_hash="0" * 64, + transactions=genesis_txs, + timestamp=GENESIS_TIMESTAMP * 1000, # Convert to ms + difficulty=None, + ) + block.nonce = 0 + block.hash = block.compute_hash() + return block diff --git a/minichain/chain.py b/minichain/chain.py new file mode 100644 index 0000000..c1e14e4 --- /dev/null +++ b/minichain/chain.py @@ -0,0 +1,160 @@ +from minichain.block import Block, create_genesis_block +from minichain.state import State +from minichain.pow import calculate_hash +from minichain.config import DIFFICULTY +from typing import List +import logging +import threading + +logger = logging.getLogger(__name__) + + +class Blockchain: + """ + Manages the blockchain, validates blocks, and commits state transitions. + """ + + def __init__(self): + self.chain: List[Block] = [] + self.difficulty = DIFFICULTY + self._lock = threading.RLock() + self._create_genesis_block() + + def _create_genesis_block(self): + """ + Creates the genesis block with treasury funds. + """ + genesis_block = create_genesis_block() + self.chain.append(genesis_block) + + @property + def last_block(self) -> Block: + """Returns the most recent block in the chain.""" + with self._lock: + return self.chain[-1] + + @property + def latest_block(self) -> Block: + """Alias for last_block.""" + return self.last_block + + @property + def height(self) -> int: + """Get chain length.""" + return len(self.chain) + + def get_state(self) -> State: + """Recompute current state by replaying all transactions from genesis.""" + state = State() + for block in self.chain: + for tx in block.transactions: + state.apply_tx(tx) + return state + + @property + def state(self) -> State: + """Get current state (computed from chain).""" + return self.get_state() + + def add_block(self, block: Block) -> bool: + """ + Validates and adds a block to the chain if all transactions succeed. + """ + with self._lock: + # Check previous hash linkage + if block.previous_hash != self.last_block.hash: + logger.warning(f"Block {block.index} rejected: Invalid previous hash (expected {self.last_block.hash[:16]}..., got {block.previous_hash[:16]}...)") + return False + + # Check index linkage + if block.index != self.last_block.index + 1: + logger.warning("Block %s rejected: Invalid index", block.index) + return False + + # Verify block hash + if block.hash != calculate_hash(block.to_header_dict()): + logger.warning("Block %s rejected: Invalid hash", block.index) + return False + + # Check proof-of-work (skip for genesis) + if block.index > 0 and not block.hash.startswith("0" * self.difficulty): + logger.warning("Block %s rejected: Hash does not meet difficulty", block.index) + return False + + # Validate transactions + current_state = self.get_state() + for tx in block.transactions: + try: + current_state.apply_tx(tx) + except ValueError as e: + logger.warning("Block %s rejected: Transaction failed - %s", block.index, e) + return False + + self.chain.append(block) + return True + + def to_dict(self) -> dict: + """Serialize chain for network transmission.""" + return {"blocks": [block.to_dict() for block in self.chain]} + + def replace_chain(self, new_chain_data: list) -> bool: + """Replace chain if new one is longer and valid (longest-chain rule). + + Args: + new_chain_data: List of block dicts from network + """ + with self._lock: + if len(new_chain_data) <= len(self.chain): + logger.info("New chain is not longer") + return False + + # Convert dicts to Block objects + new_chain = [] + for block_data in new_chain_data: + try: + new_chain.append(Block.from_dict(block_data)) + except Exception as e: + logger.warning(f"Failed to deserialize block: {e}") + return False + + # Validate the new chain + if not self._is_valid_chain(new_chain): + logger.warning("New chain is invalid") + return False + + self.chain = new_chain + logger.info("Chain replaced with %d blocks", len(new_chain)) + return True + + def _is_valid_chain(self, chain: List[Block]) -> bool: + """Validate an entire chain from genesis.""" + if not chain: + return False + + # Check genesis block matches expected + expected_genesis = create_genesis_block() + if chain[0].hash != expected_genesis.hash: + logger.warning(f"Genesis mismatch: expected {expected_genesis.hash[:16]}..., got {chain[0].hash[:16]}...") + return False + + # Validate each block + state = State() + for i, block in enumerate(chain): + # Apply transactions + for tx in block.transactions: + try: + state.apply_tx(tx) + except ValueError: + return False + + # Validate structure (skip genesis) + if i > 0: + prev = chain[i - 1] + if block.previous_hash != prev.hash: + return False + if block.index != prev.index + 1: + return False + if not block.hash.startswith("0" * self.difficulty): + return False + + return True diff --git a/minichain/config.py b/minichain/config.py new file mode 100644 index 0000000..e50e7ea --- /dev/null +++ b/minichain/config.py @@ -0,0 +1,33 @@ +""" +config.py - MiniChain configuration constants. +""" + +# Proof-of-Work difficulty (number of leading zeros required) +DIFFICULTY = 4 + +# Genesis block timestamp (fixed for reproducibility) +GENESIS_TIMESTAMP = 1704067200.0 + +# Initial treasury balance (distributed via faucet to nodes) +TREASURY_BALANCE = 10_000_000 + +# Mining reward per block +MINING_REWARD = 50 + +# Maximum transactions per block +MAX_TXS_PER_BLOCK = 100 + +# Network protocol ID +PROTOCOL_ID = "/minichain/1.0.0" + +# Special addresses (64 hex chars = 32 bytes) +COINBASE_SENDER = "0" * 64 # Mining rewards come from "nowhere" + +# Pre-generated treasury keypair (Ed25519, hex-encoded) +# This is a FIXED keypair for educational/testing purposes +# In production, this would be securely managed +TREASURY_PRIVATE_KEY = "b705c5f56f218a2003f940f3d7d825ee7369c504ba3ad5fda8a2303f4b3c5e26" +TREASURY_ADDRESS = "6b97d4ed320c6a8d1400dc034e183fd4678c4aa3f6301edf92e1cb4bd6337f44" + +# Default P2P port +DEFAULT_PORT = 8000 diff --git a/core/contract.py b/minichain/contract.py similarity index 100% rename from core/contract.py rename to minichain/contract.py diff --git a/node/mempool.py b/minichain/mempool.py similarity index 76% rename from node/mempool.py rename to minichain/mempool.py index 8bb941a..27796e9 100644 --- a/node/mempool.py +++ b/minichain/mempool.py @@ -1,4 +1,4 @@ -from consensus.pow import calculate_hash +from minichain.pow import calculate_hash import logging import threading @@ -60,3 +60,19 @@ def get_transactions_for_block(self): self._seen_tx_ids.difference_update(confirmed_ids) return txs + + def remove_transaction(self, tx): + """ + Remove a specific transaction from the pool (e.g., when included in a block from peer). + """ + tx_id = self._get_tx_id(tx) + + with self._lock: + # Remove from seen set + self._seen_tx_ids.discard(tx_id) + + # Remove from pending list + self._pending_txs = [ + t for t in self._pending_txs + if self._get_tx_id(t) != tx_id + ] diff --git a/minichain/p2p.py b/minichain/p2p.py new file mode 100644 index 0000000..37c1283 --- /dev/null +++ b/minichain/p2p.py @@ -0,0 +1,376 @@ +""" +TCP-based P2P networking for Minichain. +Supports peer registration, chain sync, and tx/block broadcasting. +""" +import asyncio +import json +import logging +import threading +from typing import Callable, Dict, List, Optional, Set + +logger = logging.getLogger(__name__) + + +class P2PNetwork: + """ + TCP-based peer-to-peer network for blockchain synchronization. + + Message types: + - "register": Register a new peer + - "tx": Broadcast transaction + - "block": Broadcast new block + - "request_chain": Request full chain from peer + - "chain": Response with full chain data + """ + + def __init__(self, node, port: int = 8000): + """ + Initialize P2P network. + + Args: + node: The node instance (has chain, mempool, etc.) + port: Port to listen on + """ + self.node = node + self.port = port + self.peers: Set[tuple] = set() # Set of (host, port) tuples + self.server: Optional[asyncio.AbstractServer] = None + self._running = False + self._loop: Optional[asyncio.AbstractEventLoop] = None + self._thread: Optional[threading.Thread] = None + self.on_block_received = None # Callback for block notifications + self.on_tx_received = None # Callback for tx notifications + + @property + def peer_list(self) -> List[str]: + """Get list of connected peers as strings.""" + return [f"{host}:{port}" for host, port in self.peers] + + def start_background(self): + """Start the P2P server in a background thread.""" + self._loop = asyncio.new_event_loop() + self._thread = threading.Thread(target=self._run_event_loop, daemon=True) + self._thread.start() + # Wait a bit for server to start + import time + time.sleep(0.1) + + def _run_event_loop(self): + """Run the asyncio event loop in background thread.""" + asyncio.set_event_loop(self._loop) + self._loop.run_until_complete(self._start_server()) + self._loop.run_forever() + + async def _start_server(self): + """Internal: start the TCP server.""" + try: + self.server = await asyncio.start_server( + self._handle_connection, + host='0.0.0.0', + port=self.port + ) + self._running = True + logger.info(f"P2P server listening on port {self.port}") + asyncio.create_task(self.server.serve_forever()) + except OSError as e: + logger.error(f"Failed to start P2P server on port {self.port}: {e}") + raise + + async def start(self): + """Start the P2P server (async version).""" + await self._start_server() + + def stop_background(self): + """Stop the P2P server running in background thread.""" + self._running = False + if self._loop and self._loop.is_running(): + # Schedule proper shutdown in the event loop + async def _shutdown(): + if self.server: + self.server.close() + await self.server.wait_closed() + # Cancel all pending tasks + tasks = [t for t in asyncio.all_tasks(self._loop) + if t is not asyncio.current_task()] + for task in tasks: + task.cancel() + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + future = asyncio.run_coroutine_threadsafe(_shutdown(), self._loop) + try: + future.result(timeout=2.0) + except Exception: + pass + self._loop.call_soon_threadsafe(self._loop.stop) + if self._thread: + self._thread.join(timeout=2.0) + logger.info("P2P server stopped") + + async def stop(self): + """Stop the P2P server.""" + self._running = False + if self.server: + self.server.close() + await self.server.wait_closed() + logger.info("P2P server stopped") + + def connect_to_peer_sync(self, host: str, port: int) -> bool: + """Connect to peer synchronously (for use from main thread).""" + if self._loop: + future = asyncio.run_coroutine_threadsafe( + self.connect_to_peer(host, port), self._loop + ) + return future.result(timeout=10.0) + return False + + async def connect_to_peer(self, host: str, port: int) -> bool: + """ + Connect to a peer and register with them. + + Args: + host: Peer hostname/IP + port: Peer port + + Returns: + True if connection successful + """ + if (host, port) in self.peers: + return True + + try: + # Send registration message + await self._send_message(host, port, { + "type": "register", + "data": {"port": self.port} + }) + self.peers.add((host, port)) + logger.info(f"Connected to peer {host}:{port}") + + # Request their chain to sync + await self.request_chain(host, port) + return True + + except Exception as e: + logger.warning(f"Failed to connect to {host}:{port}: {e}") + return False + + async def request_chain(self, host: str, port: int): + """Request full chain from a peer.""" + await self._send_message(host, port, { + "type": "request_chain", + "data": {"port": self.port} + }) + + def broadcast_transaction_sync(self, tx): + """Broadcast transaction synchronously (for use from main thread).""" + if self._loop: + future = asyncio.run_coroutine_threadsafe( + self.broadcast_transaction(tx), self._loop + ) + try: + future.result(timeout=5.0) + except Exception as e: + logger.warning(f"Broadcast tx failed: {e}") + + def broadcast_block_sync(self, block): + """Broadcast block synchronously (for use from main thread).""" + if self._loop: + future = asyncio.run_coroutine_threadsafe( + self.broadcast_block(block), self._loop + ) + try: + future.result(timeout=5.0) + except Exception as e: + logger.warning(f"Broadcast block failed: {e}") + + async def broadcast_transaction(self, tx): + """Broadcast a transaction to all peers.""" + logger.info(f"Broadcasting tx from {tx.sender[:8]}...") + await self._broadcast({ + "type": "tx", + "data": tx.to_dict() + }) + + async def broadcast_block(self, block): + """Broadcast a new block to all peers.""" + logger.info(f"Broadcasting block #{block.index}") + await self._broadcast({ + "type": "block", + "data": block.to_dict() + }) + + async def _broadcast(self, message: dict): + """Broadcast a message to all connected peers.""" + tasks = [] + for host, port in list(self.peers): + tasks.append(self._send_message(host, port, message)) + + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + async def _send_message(self, host: str, port: int, message: dict): + """Send a message to a specific peer.""" + try: + reader, writer = await asyncio.wait_for( + asyncio.open_connection(host, port), + timeout=5.0 + ) + + data = json.dumps(message).encode() + b'\n' + writer.write(data) + await writer.drain() + writer.close() + await writer.wait_closed() + + except asyncio.TimeoutError: + logger.warning(f"Timeout connecting to {host}:{port}") + self.peers.discard((host, port)) + except ConnectionRefusedError: + logger.warning(f"Connection refused by {host}:{port}") + self.peers.discard((host, port)) + except Exception as e: + logger.warning(f"Error sending to {host}:{port}: {e}") + + async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + """Handle incoming connection from a peer.""" + peer_addr = writer.get_extra_info('peername') + + try: + data = await asyncio.wait_for(reader.readline(), timeout=30.0) + if not data: + return + + message = json.loads(data.decode().strip()) + await self._handle_message(message, peer_addr) + + except json.JSONDecodeError as e: + logger.warning(f"Invalid JSON from {peer_addr}: {e}") + except asyncio.TimeoutError: + logger.warning(f"Timeout reading from {peer_addr}") + except Exception as e: + logger.warning(f"Error handling connection from {peer_addr}: {e}") + finally: + writer.close() + try: + await writer.wait_closed() + except Exception: + pass + + async def _handle_message(self, message: dict, peer_addr: tuple): + """ + Process incoming P2P message. + + Args: + message: Parsed JSON message with 'type' and 'data' + peer_addr: Peer address tuple (host, port) + """ + msg_type = message.get("type") + msg_data = message.get("data", {}) + + if msg_type == "register": + # Register peer with their listening port + peer_port = msg_data.get("port", peer_addr[1]) + self.peers.add((peer_addr[0], peer_port)) + logger.info(f"Registered peer: {peer_addr[0]}:{peer_port}") + print(f"\n[P2P] Peer connected: {peer_addr[0]}:{peer_port}") + + elif msg_type == "tx": + # Handle incoming transaction + await self._handle_incoming_tx(msg_data) + + elif msg_type == "block": + # Handle incoming block + await self._handle_incoming_block(msg_data) + + elif msg_type == "request_chain": + # Send our chain to the requesting peer + peer_port = msg_data.get("port", peer_addr[1]) + await self._send_chain(peer_addr[0], peer_port) + + elif msg_type == "chain": + # Handle incoming chain (for sync) + await self._handle_incoming_chain(msg_data) + + else: + logger.warning(f"Unknown message type: {msg_type}") + + async def _handle_incoming_tx(self, tx_data: dict): + """Process incoming transaction from peer.""" + from minichain.transaction import Transaction + + try: + tx = Transaction.from_dict(tx_data) + if self.node.mempool.add_transaction(tx): + logger.info(f"Added tx from peer: {tx.sender[:8]}...") + print(f"\n[P2P] New transaction received!") + print(f" From: {tx.sender[:16]}...") + print(f" To: {tx.receiver[:16] if tx.receiver else 'CONTRACT'}...") + print(f" Amount: {tx.amount}") + # Notify callback if set + if self.on_tx_received: + self.on_tx_received(tx) + except Exception as e: + logger.warning(f"Invalid tx from peer: {e}") + + async def _handle_incoming_block(self, block_data: dict): + """Process incoming block from peer.""" + from minichain.block import Block + + try: + block = Block.from_dict(block_data) + + # Check if this extends our chain + if block.index == self.node.chain.height: + if self.node.chain.add_block(block): + logger.info(f"Added block #{block.index} from peer") + print(f"\n[P2P] New block #{block.index} received and added!") + print(f" Hash: {block.hash[:32]}...") + print(f" Transactions: {len(block.transactions)}") + # Notify callback if set + if self.on_block_received: + self.on_block_received(block) + # Remove mined transactions from mempool + for tx in block.transactions: + self.node.mempool.remove_transaction(tx) + elif block.index > self.node.chain.height: + # We're behind, request full chain + logger.info(f"We're behind (have {self.node.chain.height - 1}, got #{block.index})") + print(f"\n[P2P] Behind on blocks, syncing...") + # Request chain from a peer + for host, port in list(self.peers): + await self.request_chain(host, port) + break + + except Exception as e: + logger.warning(f"Invalid block from peer: {e}") + + async def _send_chain(self, host: str, port: int): + """Send our full chain to a peer.""" + await self._send_message(host, port, { + "type": "chain", + "data": self.node.chain.to_dict() + }) + + async def _handle_incoming_chain(self, chain_data): + """Process incoming chain from peer (for sync).""" + try: + # Handle both old format (list) and new format (dict with 'blocks' key) + if isinstance(chain_data, list): + incoming_blocks = chain_data + elif isinstance(chain_data, dict): + incoming_blocks = chain_data.get("blocks", []) + else: + logger.warning(f"Invalid chain data type: {type(chain_data)}") + return + + # If incoming chain is longer and valid, replace ours + if len(incoming_blocks) > self.node.chain.height: + if self.node.chain.replace_chain(incoming_blocks): + logger.info(f"Synced chain: now at height {self.node.chain.height}") + print(f"\n[P2P] Chain synced! Now at height {self.node.chain.height}") + else: + logger.warning("Rejected incoming chain (invalid)") + + except Exception as e: + logger.warning(f"Error processing incoming chain: {e}") diff --git a/consensus/pow.py b/minichain/pow.py similarity index 100% rename from consensus/pow.py rename to minichain/pow.py diff --git a/core/state.py b/minichain/state.py similarity index 59% rename from core/state.py rename to minichain/state.py index 17bc68c..ac4ac2c 100644 --- a/core/state.py +++ b/minichain/state.py @@ -1,6 +1,7 @@ from nacl.hash import sha256 from nacl.encoding import HexEncoder -from core.contract import ContractMachine +from minichain.contract import ContractMachine +from minichain.transaction import COINBASE_SENDER import copy import logging @@ -15,6 +16,32 @@ def __init__(self): DEFAULT_MINING_REWARD = 50 + # ========================================================================= + # BASIC ACCOUNT METHODS (compatible with Minichain_v0) + # ========================================================================= + + def get_balance(self, address: str) -> int: + """Get account balance (0 if account doesn't exist).""" + return self.accounts.get(address, {"balance": 0})["balance"] + + def get_nonce(self, address: str) -> int: + """Get account nonce (0 if account doesn't exist).""" + return self.accounts.get(address, {"nonce": 0})["nonce"] + + def exists(self, address: str) -> bool: + """Check if account exists.""" + return address in self.accounts + + def create_account(self, address: str, balance: int = 0): + """Create new account with given balance.""" + if address not in self.accounts: + self.accounts[address] = { + "balance": balance, + "nonce": 0, + "code": None, + "storage": {} + } + def get_account(self, address): if address not in self.accounts: self.accounts[address] = { @@ -25,6 +52,74 @@ def get_account(self, address): } return self.accounts[address] + # ========================================================================= + # TRANSACTION APPLICATION + # ========================================================================= + + def apply_tx(self, tx) -> "State": + """ + Apply transaction to state (compatible with Minichain_v0). + Returns self for chaining. Raises ValueError if transaction is invalid. + """ + # Coinbase transaction: just create/credit receiver account + if tx.sender == COINBASE_SENDER: + self.create_account(tx.receiver, 0) + self.accounts[tx.receiver]["balance"] += tx.amount + return self + + # Validate signature + if not tx.verify(): + raise ValueError("Invalid signature") + + # Validate sender exists + if not self.exists(tx.sender): + raise ValueError(f"Sender {tx.sender[:8]} does not exist") + + # Validate nonce (prevents replay attacks) + expected_nonce = self.get_nonce(tx.sender) + if tx.nonce != expected_nonce: + raise ValueError(f"Bad nonce: expected {expected_nonce}, got {tx.nonce}") + + # Validate balance + if self.get_balance(tx.sender) < tx.amount: + raise ValueError("Insufficient balance") + + # Apply changes + self.accounts[tx.sender]["balance"] -= tx.amount + self.accounts[tx.sender]["nonce"] += 1 + + # Handle contract deployment (receiver is None) + if tx.receiver is None or tx.receiver == "": + if tx.data: + contract_address = self.derive_contract_address(tx.sender, tx.nonce) + self.create_contract(contract_address, tx.data, initial_balance=tx.amount) + return self + + # Create receiver if needed + if not self.exists(tx.receiver): + self.create_account(tx.receiver) + + # Handle contract call + if tx.data and self.accounts.get(tx.receiver, {}).get("code"): + self.accounts[tx.receiver]["balance"] += tx.amount + success = self.contract_machine.execute( + contract_address=tx.receiver, + sender_address=tx.sender, + payload=tx.data, + amount=tx.amount + ) + if not success: + # Rollback + self.accounts[tx.receiver]["balance"] -= tx.amount + self.accounts[tx.sender]["balance"] += tx.amount + self.accounts[tx.sender]["nonce"] -= 1 + raise ValueError("Contract execution failed") + return self + + # Regular transfer + self.accounts[tx.receiver]["balance"] += tx.amount + return self + def verify_transaction_logic(self, tx): if not tx.verify(): logger.error(f"Error: Invalid signature for tx from {tx.sender[:8]}...") diff --git a/core/transaction.py b/minichain/transaction.py similarity index 51% rename from core/transaction.py rename to minichain/transaction.py index cdf4d99..3b8e04a 100644 --- a/core/transaction.py +++ b/minichain/transaction.py @@ -1,9 +1,13 @@ import json import time +import hashlib from nacl.signing import SigningKey, VerifyKey from nacl.encoding import HexEncoder from nacl.exceptions import BadSignatureError, CryptoError +# Coinbase sender address (for genesis and mining rewards) +COINBASE_SENDER = "0" * 64 + class Transaction: def __init__(self, sender, receiver, amount, nonce, data=None, signature=None, timestamp=None): @@ -26,6 +30,23 @@ def to_dict(self): "signature": self.signature, } + @staticmethod + def from_dict(data: dict) -> "Transaction": + """Create transaction from dictionary.""" + return Transaction( + sender=data["sender"], + receiver=data["receiver"], + amount=data["amount"], + nonce=data["nonce"], + data=data.get("data"), + signature=data.get("signature"), + timestamp=data.get("timestamp") / 1000 if data.get("timestamp") else None, + ) + + def hash(self) -> str: + """Get unique hash of this transaction.""" + return hashlib.sha256(self.hash_payload).hexdigest() + @property def hash_payload(self): """Returns the bytes to be signed.""" @@ -46,7 +67,21 @@ def sign(self, signing_key: SigningKey): signed = signing_key.sign(self.hash_payload) self.signature = signed.signature.hex() + def sign_with_hex(self, private_key_hex: str): + """Sign transaction with hex-encoded private key.""" + signing_key = SigningKey(private_key_hex.encode(), encoder=HexEncoder) + signed = signing_key.sign(self.hash_payload) + self.signature = signed.signature.hex() + + def is_coinbase(self) -> bool: + """Check if this is a coinbase (genesis/reward) transaction.""" + return self.sender == COINBASE_SENDER + def verify(self): + # Coinbase transactions (genesis, mining rewards) need no signature + if self.is_coinbase(): + return True + if not self.signature: return False @@ -61,3 +96,32 @@ def verify(self): # - Malformed public key hex # - Invalid hex in signature return False + + def __repr__(self): + return f"Tx({self.sender[:8]}→{(self.receiver or 'CONTRACT')[:8]}, {self.amount})" + + +from minichain.config import GENESIS_TIMESTAMP + + +def create_genesis_tx(receiver: str, amount: int) -> Transaction: + """Create a genesis transaction (no signature needed).""" + return Transaction( + sender=COINBASE_SENDER, + receiver=receiver, + amount=amount, + nonce=0, + signature=None, + timestamp=GENESIS_TIMESTAMP, # Deterministic timestamp + ) + + +def create_coinbase_tx(miner_address: str, reward: int, block_index: int) -> Transaction: + """Create a coinbase (mining reward) transaction.""" + return Transaction( + sender=COINBASE_SENDER, + receiver=miner_address, + amount=reward, + nonce=block_index, # Use block index as nonce to ensure uniqueness + signature=None, + ) diff --git a/network/__init__.py b/network/__init__.py deleted file mode 100644 index 742fbe2..0000000 --- a/network/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .p2p import P2PNetwork - -__all__ = ["P2PNetwork"] \ No newline at end of file diff --git a/network/p2p.py b/network/p2p.py deleted file mode 100644 index ef0a9dd..0000000 --- a/network/p2p.py +++ /dev/null @@ -1,89 +0,0 @@ -import json -import logging - -logger = logging.getLogger(__name__) - - -class P2PNetwork: - """ - A minimal abstraction for Peer-to-Peer networking. - - Expected incoming message interface for handle_message(): - msg must have attribute: - - data: bytes (JSON-encoded payload) - - JSON structure: - { - "type": "tx" | "block", - "data": {...} - } - """ - - def __init__(self, handler_callback): - if not callable(handler_callback): - raise ValueError("handler_callback must be callable") - self.handler_callback = handler_callback - self.pubsub = None # Will be set in real implementation - - async def start(self): - logger.info("Network: Listening on /ip4/0.0.0.0/tcp/0") - # In real libp2p, we would await host.start() here - - async def _broadcast_message(self, topic, msg_type, payload): - msg = json.dumps({"type": msg_type, "data": payload}) - if self.pubsub: - try: - await self.pubsub.publish(topic, msg.encode()) - except Exception as e: - logger.error("Network: Publish failed: %s", e) - else: - logger.debug("Network: pubsub not initialized (mock mode)") - - async def broadcast_transaction(self, tx): - sender = getattr(tx, "sender", "") - logger.info("Network: Broadcasting Tx from %s...", sender[:5]) - try: - payload = tx.to_dict() - except (TypeError, ValueError) as e: - logger.error("Network: Failed to serialize tx: %s", e) - return - await self._broadcast_message("minichain-global", "tx", payload) - - async def broadcast_block(self, block): - logger.info("Network: Broadcasting Block #%d", block.index) - await self._broadcast_message("minichain-global", "block", block.to_dict()) - - async def handle_message(self, msg): - """ - Callback when a p2p message is received. - """ - - try: - if not hasattr(msg, "data"): - raise TypeError("Incoming message missing 'data' attribute") - - if not isinstance(msg.data, (bytes, bytearray)): - raise TypeError("msg.data must be bytes") - - if len(msg.data) > 1024 * 1024: # 1MB limit - logger.warning("Network: Message too large") - return - - try: - decoded = msg.data.decode('utf-8') - except UnicodeDecodeError as e: - logger.warning("Network Error: UnicodeDecodeError during message decode: %s", e) - return - data = json.loads(decoded) - - if not isinstance(data, dict) or "type" not in data or "data" not in data: - raise ValueError("Invalid message format") - - except (TypeError, ValueError, json.JSONDecodeError) as e: - logger.warning("Network Error parsing message: %s", e) - return - - try: - await self.handler_callback(data) - except Exception: - logger.exception("Error in network handler callback for data: %s", data) diff --git a/node/__init__.py b/node/__init__.py deleted file mode 100644 index 434db3e..0000000 --- a/node/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .mempool import Mempool - -__all__ = ["Mempool"] \ No newline at end of file diff --git a/tests/test_contract.py b/tests/test_contract.py index 111222d..2ac6e9f 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -2,7 +2,7 @@ import sys import os -from core import State, Transaction +from minichain import State, Transaction from nacl.signing import SigningKey from nacl.encoding import HexEncoder diff --git a/tests/test_core.py b/tests/test_core.py index 1b7a189..56f542d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2,7 +2,7 @@ from nacl.signing import SigningKey from nacl.encoding import HexEncoder -from core import Transaction, Blockchain, State # Removed unused imports +from minichain import Transaction, Blockchain, State class TestCore(unittest.TestCase): def setUp(self): @@ -21,7 +21,8 @@ def test_genesis_block(self): """Check if genesis block is created correctly.""" self.assertEqual(len(self.chain.chain), 1) self.assertEqual(self.chain.last_block.index, 0) - self.assertEqual(self.chain.last_block.previous_hash, "0") + # Genesis block has 64-character zero hash (compatible with Minichain_v0) + self.assertEqual(self.chain.last_block.previous_hash, "0" * 64) def test_transaction_signature(self): """Check that valid signatures pass and invalid ones fail."""