From 05602eae0c49e5b076b32e8f2f9d8c1a8c899606 Mon Sep 17 00:00:00 2001 From: anuragShingare30 Date: Sat, 21 Feb 2026 14:27:47 +0530 Subject: [PATCH] feat: Added initial minichain prototype with CLI interface for demonstration --- Minichain_v0/.gitignore | 10 ++ Minichain_v0/README.md | 104 ++++++++++++++ Minichain_v0/block.py | 77 ++++++++++ Minichain_v0/blockchain.py | 133 ++++++++++++++++++ Minichain_v0/config.py | 31 +++++ Minichain_v0/consensus.py | 47 +++++++ Minichain_v0/main.py | 255 ++++++++++++++++++++++++++++++++++ Minichain_v0/mempool.py | 66 +++++++++ Minichain_v0/minichain | 54 +++++++ Minichain_v0/network.py | 187 +++++++++++++++++++++++++ Minichain_v0/requirements.txt | 3 + Minichain_v0/state.py | 70 ++++++++++ Minichain_v0/transaction.py | 102 ++++++++++++++ 13 files changed, 1139 insertions(+) create mode 100644 Minichain_v0/.gitignore create mode 100644 Minichain_v0/README.md create mode 100644 Minichain_v0/block.py create mode 100644 Minichain_v0/blockchain.py create mode 100644 Minichain_v0/config.py create mode 100644 Minichain_v0/consensus.py create mode 100644 Minichain_v0/main.py create mode 100644 Minichain_v0/mempool.py create mode 100755 Minichain_v0/minichain create mode 100644 Minichain_v0/network.py create mode 100644 Minichain_v0/requirements.txt create mode 100644 Minichain_v0/state.py create mode 100644 Minichain_v0/transaction.py diff --git a/Minichain_v0/.gitignore b/Minichain_v0/.gitignore new file mode 100644 index 0000000..7661e2e --- /dev/null +++ b/Minichain_v0/.gitignore @@ -0,0 +1,10 @@ +# Wallet keys (private keys - never commit!) +*.key + +# Python +venv/ +__pycache__/ +*.pyc + +# Environment +.env \ No newline at end of file diff --git a/Minichain_v0/README.md b/Minichain_v0/README.md new file mode 100644 index 0000000..689529d --- /dev/null +++ b/Minichain_v0/README.md @@ -0,0 +1,104 @@ +# MiniChain v0 - Educational Blockchain + +A minimal, educational blockchain implementation in Python + +## Quick Start + +```bash +cd Minichain_v0 +pip install -r requirements.txt + +# Using the launcher (recommended) +./minichain start 8000 # Bootstrap node +./minichain start 8001 8000 # Connect to port 8000 +./minichain start 8002 8000 # Third node + +# Or using Python directly +python3 main.py --port 8000 +python3 main.py --port 8001 --connect localhost:8000 +``` + +## Test Scenarios + +### 1. Mining for rewards +``` +mine # Mine block (earns 50 coins) +balance # Check balance +``` + +### 2. Distribute from treasury +``` +treasury # Check treasury balance +faucet 1000 +mine # Confirm transaction +``` + +### 3. Send coins +``` +address # Show your address (copy recipient's) +send 100 +mempool # View mempool +mine # Confirm +``` + +## Commands + +|---------|----------|-------------| +| `balance` | `b` | Show your balance | +| `address` | `a` | Show your wallet address | +| `send ` | - | Send coins | +| `mine` | `m` | Mine block (+50 reward) | +| `faucet ` | - | Treasury send | +| `treasury` | `t` | Show treasury balance | +| `chain` | `c` | Show blockchain | +| `peers` | `p` | Show connected peers | +| `mempool` | `mp` | Show pending transactions | +| `quit` | `q` | Exit | + + +## File and folder structure + +| File | Lines | Description | +|------|-------|-------------| +| `config.py` | 19 | Configuration constants | +| `transaction.py` | 91 | Signed transactions (Ed25519) | +| `state.py` | 70 | Account balances and nonces | +| `block.py` | 80 | Block structure | +| `blockchain.py` | 133 | Chain validation and storage | +| `mempool.py` | 66 | Pending transaction pool | +| `consensus.py` | 40 | Proof-of-Work mining | +| `network.py` | 154 | P2P networking (TCP sockets) | +| `main.py` | 185 | CLI interface | +| `minichain` | - | Bash launcher script | + + +## What Users/Dev Learn + +1. **Transactions** - Ed25519 digital signatures for authentication +2. **State** - Account-based ledger (balances + nonces) +3. **Blocks** - Linking transactions with hashes +4. **Consensus** - Proof-of-Work mining with rewards +5. **Networking** - P2P communication with TCP sockets + + +## Architecture + +``` +Transaction (signed) → Mempool → Block → Blockchain + ↑ + Consensus (PoW) + ↓ + Network → Peers +``` + + +## Not Included (v0 Simplifications) + +- ❌ Merkle trees (optimization) +- ❌ State snapshots (optimization) +- ❌ Persistence (in-memory only) +- ❌ GossipSub (using simpler streams) + +## Progression Path + +`v0` (currently we are here) → `v1` (optimizations) → `v2` (smart contracts) \ No newline at end of file diff --git a/Minichain_v0/block.py b/Minichain_v0/block.py new file mode 100644 index 0000000..a6d8c4c --- /dev/null +++ b/Minichain_v0/block.py @@ -0,0 +1,77 @@ +""" +block.py - Block structure for MiniChain. +A block contains transactions and links to the previous block via hash. +""" + +import json +import hashlib +import time +from typing import List +from transaction import Transaction, create_genesis_tx +from config import GENESIS_TIMESTAMP, TREASURY_ADDRESS, TREASURY_BALANCE + + +class Block: + """A block in the blockchain containing transactions.""" + + def __init__(self, index: int, prev_hash: str, transactions: List[Transaction], + timestamp: float = None, nonce: int = 0): + self.index = index + self.prev_hash = prev_hash + self.transactions = transactions + self.timestamp = timestamp or time.time() + self.nonce = nonce + self.hash = self.compute_hash() + + def compute_hash(self) -> str: + """Compute SHA-256 hash of block header.""" + header = { + "index": self.index, + "prev_hash": self.prev_hash, + "tx_hashes": [tx.hash() for tx in self.transactions], + "timestamp": self.timestamp, + "nonce": self.nonce + } + header_bytes = json.dumps(header, sort_keys=True).encode() + return hashlib.sha256(header_bytes).hexdigest() + + def to_dict(self) -> dict: + """Convert block to dictionary for serialization.""" + return { + "index": self.index, + "prev_hash": self.prev_hash, + "transactions": [tx.to_dict() for tx in self.transactions], + "timestamp": self.timestamp, + "nonce": self.nonce, + "hash": self.hash + } + + @staticmethod + def from_dict(data: dict) -> "Block": + """Create block from dictionary.""" + txs = [Transaction.from_dict(tx) for tx in data["transactions"]] + block = Block( + index=data["index"], + prev_hash=data["prev_hash"], + transactions=txs, + timestamp=data["timestamp"], + nonce=data["nonce"] + ) + return block + + def __repr__(self): + return f"Block(#{self.index}, txs={len(self.transactions)}, hash={self.hash[:8]})" + + +def create_genesis_block() -> Block: + """Create the genesis (first) block with treasury funds.""" + # Genesis funds go to the fixed treasury address + genesis_txs = [create_genesis_tx(TREASURY_ADDRESS, TREASURY_BALANCE)] + + return Block( + index=0, + prev_hash="0" * 64, + transactions=genesis_txs, + timestamp=GENESIS_TIMESTAMP, + nonce=0 + ) diff --git a/Minichain_v0/blockchain.py b/Minichain_v0/blockchain.py new file mode 100644 index 0000000..b595869 --- /dev/null +++ b/Minichain_v0/blockchain.py @@ -0,0 +1,133 @@ +""" +blockchain.py - Chain management for MiniChain. +Stores blocks, validates new blocks, and handles longest-chain rule. +""" + +from typing import List +from block import Block, create_genesis_block +from state import State, apply_tx +from config import DIFFICULTY + + +class Blockchain: + """The blockchain: a list of validated blocks.""" + + def __init__(self): + genesis = create_genesis_block() + self.chain: List[Block] = [genesis] + self.difficulty = DIFFICULTY + + @property + def latest_block(self) -> Block: + """Get the most recent block.""" + return self.chain[-1] + + @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(state, tx) + return state + + def validate_block(self, block: Block, prev_block: Block) -> bool: + """Check if a block is valid (structure, PoW, transactions).""" + # Check index is sequential + if block.index != prev_block.index + 1: + print(f"Invalid index: expected {prev_block.index + 1}, got {block.index}") + return False + + # Check previous hash links correctly + if block.prev_hash != prev_block.hash: + print("Invalid previous hash") + return False + + # Check hash is correct + if block.hash != block.compute_hash(): + print("Invalid hash") + return False + + # Check proof-of-work + if not block.hash.startswith("0" * self.difficulty): + print("Hash does not meet difficulty") + return False + + return True + + def validate_block_transactions(self, block: Block, state: State) -> bool: + """Validate all transactions in block against given state.""" + try: + for tx in block.transactions: + state = apply_tx(state, tx) + return True + except ValueError as e: + print(f"Transaction validation failed: {e}") + return False + + def add_block(self, block: Block) -> bool: + """Add a new block to the chain if valid.""" + # Validate block structure and PoW + if not self.validate_block(block, self.latest_block): + return False + + # Validate transactions against current state + current_state = self.get_state() + if not self.validate_block_transactions(block, current_state): + return False + + self.chain.append(block) + 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: + return False + + # Validate each block + state = State() + for i, block in enumerate(chain): + # Apply transactions + try: + for tx in block.transactions: + state = apply_tx(state, tx) + except ValueError: + return False + + # Validate structure (skip genesis) + if i > 0 and not self.validate_block(block, chain[i - 1]): + return False + + return True + + def replace_chain(self, new_chain: List[Block]) -> bool: + """Replace chain if new one is longer and valid (longest-chain rule).""" + if len(new_chain) <= len(self.chain): + print("New chain is not longer") + return False + + if not self.is_valid_chain(new_chain): + print("New chain is invalid") + return False + + self.chain = new_chain + print(f"Chain replaced with {len(new_chain)} blocks") + return True + + def to_dict(self) -> List[dict]: + """Serialize chain for network transmission.""" + return [block.to_dict() for block in self.chain] + + @staticmethod + def from_dict(data: List[dict]) -> List[Block]: + """Deserialize chain from network.""" + return [Block.from_dict(b) for b in data] diff --git a/Minichain_v0/config.py b/Minichain_v0/config.py new file mode 100644 index 0000000..d8ef6be --- /dev/null +++ b/Minichain_v0/config.py @@ -0,0 +1,31 @@ +""" +config.py - MiniChain configuration constants. +Simple settings for the blockchain. +""" + +# 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 = 10000000 + +# 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 +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" diff --git a/Minichain_v0/consensus.py b/Minichain_v0/consensus.py new file mode 100644 index 0000000..4557811 --- /dev/null +++ b/Minichain_v0/consensus.py @@ -0,0 +1,47 @@ +""" +consensus.py - Proof-of-Work mining for MiniChain. +Finds a nonce that makes the block hash start with required zeros. +""" + +import time +from typing import List +from block import Block +from transaction import Transaction, create_coinbase_tx +from config import DIFFICULTY, MINING_REWARD + + +def mine_block(prev_block: Block, transactions: List[Transaction], + miner_address: str, difficulty: int = DIFFICULTY) -> Block: + """ + Mine a new block using Proof-of-Work. + Includes coinbase transaction as mining reward. + Increments nonce until hash has required leading zeros. + """ + index = prev_block.index + 1 + prev_hash = prev_block.hash + timestamp = time.time() + nonce = 0 + + # Add coinbase (mining reward) as first transaction + coinbase = create_coinbase_tx(miner_address, MINING_REWARD, index) + all_txs = [coinbase] + transactions + + print(f"⛏️ Mining block #{index} with {len(all_txs)} transactions (including coinbase)...") + print(f"💰 Mining reward: {MINING_REWARD} coins") + start = time.time() + + # Try nonces until we find valid hash + while True: + block = Block(index, prev_hash, all_txs, timestamp, nonce) + + if block.hash.startswith("0" * difficulty): + elapsed = time.time() - start + print(f"✅ Mined! Hash: {block.hash}") + print(f"⏱️ Time: {elapsed:.2f}s, Nonce: {nonce}") + return block + + nonce += 1 + + # Progress indicator + if nonce % 50000 == 0: + print(f" Trying nonce {nonce}...") diff --git a/Minichain_v0/main.py b/Minichain_v0/main.py new file mode 100644 index 0000000..f4daa11 --- /dev/null +++ b/Minichain_v0/main.py @@ -0,0 +1,255 @@ +""" +main.py - CLI interface for MiniChain v0. +Simple command-line tool to run a blockchain node. +""" + +import asyncio +import argparse +import os +from nacl.signing import SigningKey +from nacl.encoding import HexEncoder + +from blockchain import Blockchain +from mempool import Mempool +from network import Network +from transaction import Transaction +from consensus import mine_block +from config import TREASURY_PRIVATE_KEY, TREASURY_ADDRESS + + +def get_wallet_file(port: int) -> str: + """Get port-specific wallet filename.""" + return f"wallet_{port}.key" + + +def create_wallet(port: int) -> SigningKey: + """Generate a new Ed25519 keypair and save to port-specific file.""" + wallet_file = get_wallet_file(port) + + if os.path.exists(wallet_file): + # Load existing wallet + with open(wallet_file, "rb") as f: + return SigningKey(f.read()) + + # Create new wallet + key = SigningKey.generate() + with open(wallet_file, "wb") as f: + f.write(key.encode()) + + address = key.verify_key.encode(encoder=HexEncoder).decode() + print(f"✅ New wallet created for port {port}") + print(f"📍 Your address: {address}") + return key + + +def get_address(key: SigningKey) -> str: + """Get address from signing key.""" + return key.verify_key.encode(encoder=HexEncoder).decode() + + +def print_banner(port: int, address: str, is_bootstrap: bool): + """Print clean startup banner.""" + print() + print("╔══════════════════════════════════════════════════════════════╗") + print("║ 🔗 MINICHAIN v0 ║") + print("║ Educational Blockchain Node ║") + print("╠══════════════════════════════════════════════════════════════╣") + print(f"║ Port: {port:<52}║") + print(f"║ Address: {address[:20]}...{address[-8:]:<21}║") + if is_bootstrap: + print("║ Role: Bootstrap Node (Treasury Access) ║") + else: + print("║ Role: Peer Node ║") + print("╚══════════════════════════════════════════════════════════════╝") + print() + + +def print_help(): + """Print organized help menu.""" + print() + print("┌─────────────────── COMMANDS ───────────────────┐") + print("│ │") + print("│ 💰 WALLET │") + print("│ balance (b) - Show your balance │") + print("│ address (a) - Show your wallet address │") + print("│ │") + print("│ 💸 TRANSACTIONS │") + print("│ send - Send coins │") + print("│ faucet - Treasury send │") + print("│ mempool (mp) - View pending transactions │") + print("│ │") + print("│ ⛏️ MINING │") + print("│ mine (m) - Mine block (+50 reward) │") + print("│ │") + print("│ 🔍 INFO │") + print("│ chain (c) - Show blockchain │") + print("│ peers (p) - Show connected peers │") + print("│ treasury (t) - Show treasury balance │") + print("│ │") + print("│ 🚪 EXIT │") + print("│ quit (q) - Exit node │") + print("│ │") + print("└────────────────────────────────────────────────┘") + print() + + +async def run_node(args): + """Run the blockchain node.""" + # Create/load port-specific wallet + wallet = create_wallet(args.port) + address = get_address(wallet) + + # Initialize blockchain (all nodes share same genesis with treasury) + blockchain = Blockchain() + mempool = Mempool() + network = Network(blockchain, mempool) + + # Start network + await network.start(args.port) + + is_bootstrap = not args.connect + + # Connect to bootstrap peer if provided (format: host:port) + if args.connect: + try: + host, port = args.connect.rsplit(":", 1) + await network.connect(host, int(port)) + except ValueError: + print("❌ Invalid peer format. Use host:port, e.g., localhost:8001") + + # Print clean startup banner + print_banner(args.port, address, is_bootstrap) + + # Show help on startup + print("Type 'help' or 'h' for commands\n") + + while True: + try: + cmd = await asyncio.get_event_loop().run_in_executor(None, input, "minichain> ") + cmd = cmd.strip().lower() + + if cmd == "quit" or cmd == "q": + print("👋 Goodbye!") + break + + elif cmd == "balance" or cmd == "b": + state = blockchain.get_state() + bal = state.get_balance(address) + nonce = state.get_nonce(address) + print(f"💰 Balance: {bal}") + print(f"🔢 Nonce: {nonce}") + + elif cmd.startswith("send "): + parts = cmd.split() + if len(parts) != 3: + print("Usage: send
") + continue + + receiver = parts[1] + amount = int(parts[2]) + state = blockchain.get_state() + + tx = Transaction(address, receiver, amount, state.get_nonce(address)) + tx.sign(wallet.encode(encoder=HexEncoder).decode()) + + if mempool.add(tx, state): + print(f"✅ Transaction added to mempool") + await network.broadcast_tx(tx) + print(f"📡 Broadcasted to peers") + else: + print("❌ Transaction rejected (check balance/nonce)") + + elif cmd == "mine" or cmd == "m": + pending = mempool.get_pending() + # Can mine even with no pending transactions (just for reward) + + block = mine_block(blockchain.latest_block, pending, address) + + if blockchain.add_block(block): + mempool.remove(pending) + await network.broadcast_block(block) + print(f"📡 Block broadcasted") + + elif cmd.startswith("faucet "): + # Treasury sends coins to an address (only works on bootstrap node) + parts = cmd.split() + if len(parts) != 3: + print("Usage: faucet
") + continue + + receiver = parts[1] + amount = int(parts[2]) + state = blockchain.get_state() + + # Create transaction from treasury + treasury_key = SigningKey(TREASURY_PRIVATE_KEY.encode(), encoder=HexEncoder) + treasury_nonce = state.get_nonce(TREASURY_ADDRESS) + + tx = Transaction(TREASURY_ADDRESS, receiver, amount, treasury_nonce) + tx.sign(TREASURY_PRIVATE_KEY) + + if mempool.add(tx, state): + print(f"✅ Faucet transaction added to mempool") + print(f" {amount} coins → {receiver[:16]}...") + await network.broadcast_tx(tx) + print(f"📡 Broadcasted. Mine a block to confirm!") + else: + print("❌ Faucet failed (check treasury balance)") + + elif cmd == "chain" or cmd == "c": + print(f"\n⛓️ Blockchain ({blockchain.height} blocks)") + print("-" * 40) + for block in blockchain.chain: + print(f"Block #{block.index}") + print(f" Hash: {block.hash[:16]}...") + print(f" PrevHash: {block.prev_hash[:16]}...") + print(f" Txs: {len(block.transactions)}") + print() + + elif cmd == "peers" or cmd == "p": + if not network.peers: + print("No connected peers") + else: + for host, port in network.peers: + print(f"👤 {host}:{port}") + + elif cmd == "mempool" or cmd == "mp": + print(f"📋 Mempool: {len(mempool)} pending transactions") + for tx in mempool.transactions: + print(f" {tx}") + + elif cmd == "address" or cmd == "addr" or cmd == "a": + print(f"📍 Your address: {address}") + + elif cmd == "treasury" or cmd == "t": + state = blockchain.get_state() + bal = state.get_balance(TREASURY_ADDRESS) + print(f"🏦 Treasury address: {TREASURY_ADDRESS}") + print(f"💰 Treasury balance: {bal}") + + elif cmd == "help" or cmd == "h": + print_help() + + elif cmd: + print("Unknown command. Type 'help' for commands.") + + except KeyboardInterrupt: + print("\n👋 Goodbye!") + break + except EOFError: + break + except Exception as e: + print(f"Error: {e}") + + +def main(): + parser = argparse.ArgumentParser(description="MiniChain v0 - Educational Blockchain") + parser.add_argument("--port", type=int, default=8000, help="P2P port (default: 8000)") + parser.add_argument("--connect", type=str, help="Bootstrap peer address to connect") + + args = parser.parse_args() + asyncio.run(run_node(args)) + + +if __name__ == "__main__": + main() diff --git a/Minichain_v0/mempool.py b/Minichain_v0/mempool.py new file mode 100644 index 0000000..6464712 --- /dev/null +++ b/Minichain_v0/mempool.py @@ -0,0 +1,66 @@ +""" +mempool.py - Transaction pool for MiniChain. +Holds pending transactions before they are mined into a block. +""" + +from typing import List, Set +from transaction import Transaction +from state import State +from config import MAX_TXS_PER_BLOCK + + +class Mempool: + """Pool of pending transactions waiting to be mined.""" + + def __init__(self): + self.transactions: List[Transaction] = [] + self._seen_hashes: Set[str] = set() # Prevent duplicates + + def add(self, tx: Transaction, state: State) -> bool: + """Add transaction to mempool if valid. Returns True if added.""" + tx_hash = tx.hash() + + # Reject duplicates + if tx_hash in self._seen_hashes: + return False + + # Validate signature + if not tx.verify(): + return False + + # Validate against current state (skip genesis txs) + if tx.sender != "0" * 64: + # Check sender exists + if not state.exists(tx.sender): + return False + + # Check nonce + if tx.nonce != state.get_nonce(tx.sender): + return False + + # Check balance + if state.get_balance(tx.sender) < tx.amount: + return False + + # Add to pool + self.transactions.append(tx) + self._seen_hashes.add(tx_hash) + return True + + def get_pending(self, max_count: int = MAX_TXS_PER_BLOCK) -> List[Transaction]: + """Get pending transactions for mining (FIFO order).""" + return self.transactions[:max_count] + + def remove(self, txs: List[Transaction]): + """Remove transactions (after they are mined).""" + hashes_to_remove = {tx.hash() for tx in txs} + self.transactions = [tx for tx in self.transactions if tx.hash() not in hashes_to_remove] + self._seen_hashes -= hashes_to_remove + + def clear(self): + """Clear all pending transactions.""" + self.transactions.clear() + self._seen_hashes.clear() + + def __len__(self): + return len(self.transactions) diff --git a/Minichain_v0/minichain b/Minichain_v0/minichain new file mode 100755 index 0000000..ea0206f --- /dev/null +++ b/Minichain_v0/minichain @@ -0,0 +1,54 @@ +#!/bin/bash +# +# MiniChain v0 - Simple Node Launcher +# Usage: +# ./minichain start 8000 # Start bootstrap node +# ./minichain start 8001 8000 # Start peer connecting to port 8000 +# ./minichain clean # Remove all wallet files +# + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +# Activate virtual environment if exists +if [ -d "venv" ]; then + source venv/bin/activate +fi + +case "$1" in + start) + PORT="${2:-8000}" + if [ -n "$3" ]; then + python3 main.py --port "$PORT" --connect "localhost:$3" + else + python3 main.py --port "$PORT" + fi + ;; + clean) + rm -f wallet_*.key + echo "✅ All wallet files removed" + ;; + help|--help|-h|"") + echo "" + echo "╔════════════════════════════════════════════════════════╗" + echo "║ 🔗 MINICHAIN v0 LAUNCHER ║" + echo "╠════════════════════════════════════════════════════════╣" + echo "║ ║" + echo "║ USAGE: ║" + echo "║ ./minichain start [connect_port] ║" + echo "║ ./minichain clean ║" + echo "║ ./minichain help ║" + echo "║ ║" + echo "║ EXAMPLES: ║" + echo "║ ./minichain start 8000 # Bootstrap node ║" + echo "║ ./minichain start 8001 8000 # Connect to 8000 ║" + echo "║ ./minichain start 8002 8000 # Another peer ║" + echo "║ ║" + echo "╚════════════════════════════════════════════════════════╝" + echo "" + ;; + *) + echo "Unknown command: $1" + echo "Run './minichain help' for usage" + ;; +esac diff --git a/Minichain_v0/network.py b/Minichain_v0/network.py new file mode 100644 index 0000000..5597e02 --- /dev/null +++ b/Minichain_v0/network.py @@ -0,0 +1,187 @@ +""" +network.py - Simple P2P networking for MiniChain. +Uses basic TCP sockets for educational clarity. +For production, consider upgrading to libp2p. +""" + +import asyncio +import json +from typing import Callable, Optional, List + +from blockchain import Blockchain +from mempool import Mempool +from transaction import Transaction +from block import Block +from config import PROTOCOL_ID + + +class Network: + """Simple TCP-based P2P network node for MiniChain.""" + + def __init__(self, blockchain: Blockchain, mempool: Mempool): + self.blockchain = blockchain + self.mempool = mempool + self.server = None + self.peers: List[tuple] = [] # List of (host, port) + self.port = None + + # Callbacks for new blocks/transactions + self.on_block: Optional[Callable] = None + self.on_transaction: Optional[Callable] = None + + async def start(self, port: int): + """Start the P2P server.""" + self.port = port + self.server = await asyncio.start_server( + self._handle_connection, + '0.0.0.0', + port + ) + print(f"🌐 Node started on port {port}") + print(f"🔗 Address: localhost:{port}") + + # Start the server + asyncio.create_task(self._run_server()) + + async def _run_server(self): + """Run the server in background.""" + async with self.server: + await self.server.serve_forever() + + async def connect(self, host: str, port: int): + """Connect to a peer.""" + try: + if (host, port) not in self.peers: + self.peers.append((host, port)) + print(f"✅ Added peer {host}:{port}") + + # Register ourselves with the peer so they add us back + await self._send_to_peer(host, port, { + "type": "register", + "data": {"port": self.port} + }) + + # Request their chain + await self._request_chain(host, port) + except Exception as e: + print(f"❌ Connection failed: {e}") + + async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + """Handle incoming connection.""" + try: + # Get peer address + peer_addr = writer.get_extra_info('peername') + + data = await reader.read(1048576) # 1MB max + if not data: + return + + msg = json.loads(data.decode()) + response = await self._handle_message(msg, peer_addr) + + if response: + writer.write(json.dumps(response).encode()) + await writer.drain() + + except Exception as e: + print(f"⚠️ Connection error: {e}") + finally: + writer.close() + await writer.wait_closed() + + async def _handle_message(self, msg: dict, peer_addr: tuple = None) -> Optional[dict]: + """Handle incoming message and return response if needed.""" + msg_type = msg.get("type") + + if msg_type == "tx": + await self._handle_tx(msg["data"]) + return None + + elif msg_type == "block": + await self._handle_block(msg["data"]) + return None + + elif msg_type == "request_chain": + return {"type": "chain", "data": self.blockchain.to_dict()} + + elif msg_type == "chain": + await self._handle_chain(msg["data"]) + return None + + elif msg_type == "register": + # Peer is telling us their listening port + peer_port = msg["data"]["port"] + peer_host = peer_addr[0] if peer_addr else "localhost" + if (peer_host, peer_port) not in self.peers: + self.peers.append((peer_host, peer_port)) + print(f"📥 Peer registered: {peer_host}:{peer_port}") + return None + + return None + + async def _handle_tx(self, tx_data: dict): + """Handle received transaction.""" + tx = Transaction.from_dict(tx_data) + state = self.blockchain.get_state() + + if self.mempool.add(tx, state): + print(f"📥 Received tx: {tx}") + if self.on_transaction: + self.on_transaction(tx) + + async def _handle_block(self, block_data: dict): + """Handle received block.""" + block = Block.from_dict(block_data) + + if self.blockchain.add_block(block): + print(f"📥 Added block #{block.index}") + self.mempool.remove(block.transactions) + if self.on_block: + self.on_block(block) + + async def _handle_chain(self, chain_data: list): + """Handle received chain (for sync).""" + new_chain = [Block.from_dict(b) for b in chain_data] + if self.blockchain.replace_chain(new_chain): + self.mempool.clear() + + async def _send_to_peer(self, host: str, port: int, msg: dict) -> Optional[dict]: + """Send message to a peer and optionally receive response.""" + try: + reader, writer = await asyncio.open_connection(host, port) + writer.write(json.dumps(msg).encode()) + await writer.drain() + + # Wait for response if expecting one + if msg["type"] == "request_chain": + data = await reader.read(1048576) + writer.close() + await writer.wait_closed() + if data: + return json.loads(data.decode()) + else: + writer.close() + await writer.wait_closed() + + return None + except Exception as e: + print(f"⚠️ Failed to send to {host}:{port}: {e}") + return None + + async def _request_chain(self, host: str, port: int): + """Request full chain from a peer.""" + response = await self._send_to_peer(host, port, {"type": "request_chain"}) + if response and response.get("type") == "chain": + await self._handle_chain(response["data"]) + + async def broadcast_tx(self, tx: Transaction): + """Broadcast transaction to all peers.""" + msg = {"type": "tx", "data": tx.to_dict()} + for host, port in self.peers: + await self._send_to_peer(host, port, msg) + + async def broadcast_block(self, block: Block): + """Broadcast block to all peers.""" + msg = {"type": "block", "data": block.to_dict()} + for host, port in self.peers: + await self._send_to_peer(host, port, msg) diff --git a/Minichain_v0/requirements.txt b/Minichain_v0/requirements.txt new file mode 100644 index 0000000..b48735e --- /dev/null +++ b/Minichain_v0/requirements.txt @@ -0,0 +1,3 @@ +# MiniChain v0 Dependencies +# Cryptography: Ed25519 signatures +pynacl>=1.5.0 diff --git a/Minichain_v0/state.py b/Minichain_v0/state.py new file mode 100644 index 0000000..4c1d5aa --- /dev/null +++ b/Minichain_v0/state.py @@ -0,0 +1,70 @@ +""" +state.py - Account state management for MiniChain. +Tracks balances and nonces for all accounts. +""" + +from transaction import Transaction + + +class State: + """The current state of all accounts (balances and nonces).""" + + def __init__(self): + self.accounts = {} # {address: {"balance": int, "nonce": int}} + + 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} + + +def apply_tx(state: State, tx: Transaction) -> State: + """ + Apply transaction to state and return new state. + Raises ValueError if transaction is invalid. + This is the core state transition function of the blockchain. + """ + # Genesis transaction: just create receiver account + if tx.sender == "0" * 64: + state.create_account(tx.receiver, tx.amount) + return state + + # Validate sender exists + if not state.exists(tx.sender): + raise ValueError(f"Sender {tx.sender[:8]} does not exist") + + # Validate signature + if not tx.verify(): + raise ValueError("Invalid signature") + + # Validate nonce (prevents replay attacks) + expected_nonce = state.get_nonce(tx.sender) + if tx.nonce != expected_nonce: + raise ValueError(f"Bad nonce: expected {expected_nonce}, got {tx.nonce}") + + # Validate balance + if state.get_balance(tx.sender) < tx.amount: + raise ValueError(f"Insufficient balance") + + # Apply changes + state.accounts[tx.sender]["balance"] -= tx.amount + state.accounts[tx.sender]["nonce"] += 1 + + # Create receiver if needed + if not state.exists(tx.receiver): + state.create_account(tx.receiver) + state.accounts[tx.receiver]["balance"] += tx.amount + + return state diff --git a/Minichain_v0/transaction.py b/Minichain_v0/transaction.py new file mode 100644 index 0000000..826041d --- /dev/null +++ b/Minichain_v0/transaction.py @@ -0,0 +1,102 @@ +""" +transaction.py - Signed transactions for MiniChain. +Uses Ed25519 signatures via PyNaCl for authentication. +""" + +import json +import hashlib +from nacl.signing import SigningKey, VerifyKey +from nacl.encoding import HexEncoder + + +class Transaction: + """A signed transfer of value from sender to receiver""" + + def __init__(self, sender: str, receiver: str, amount: int, nonce: int, signature: str = None): + self.sender = sender # Public key (hex) of sender + self.receiver = receiver # Public key (hex) of receiver + self.amount = amount # Amount to transfer + self.nonce = nonce # Sender's transaction count (replay protection) + self.signature = signature # Ed25519 signature (hex) + + def to_bytes(self) -> bytes: + """Serialize transaction for signing (deterministic JSON).""" + data = { + "sender": self.sender, + "receiver": self.receiver, + "amount": self.amount, + "nonce": self.nonce + } + return json.dumps(data, sort_keys=True).encode() + + def hash(self) -> str: + """Get unique hash of this transaction.""" + return hashlib.sha256(self.to_bytes()).hexdigest() + + def sign(self, private_key_hex: str): + """Sign transaction with sender's private key.""" + signing_key = SigningKey(private_key_hex.encode(), encoder=HexEncoder) + signed = signing_key.sign(self.to_bytes()) + self.signature = signed.signature.hex() + + def verify(self) -> bool: + """Verify signature is valid. Genesis transactions (sender=0x00...) are always valid.""" + # Genesis transactions need no signature + if self.sender == "0" * 64: + return True + + if not self.signature: + return False + + try: + verify_key = VerifyKey(self.sender.encode(), encoder=HexEncoder) + verify_key.verify(self.to_bytes(), bytes.fromhex(self.signature)) + return True + except Exception: + return False + + def to_dict(self) -> dict: + """Convert to dictionary for serialization.""" + return { + "sender": self.sender, + "receiver": self.receiver, + "amount": self.amount, + "nonce": self.nonce, + "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"], + signature=data.get("signature") + ) + + def __repr__(self): + return f"Tx({self.sender[:8]}→{self.receiver[:8]}, {self.amount})" + + +def create_genesis_tx(receiver: str, amount: int) -> Transaction: + """Create a genesis transaction (no signature needed).""" + return Transaction( + sender="0" * 64, # System address + receiver=receiver, + amount=amount, + nonce=0, + signature=None + ) + + +def create_coinbase_tx(miner_address: str, reward: int, block_index: int) -> Transaction: + """Create a coinbase (mining reward) transaction.""" + return Transaction( + sender="0" * 64, # System address (coins created from nothing) + receiver=miner_address, + amount=reward, + nonce=block_index, # Use block index as nonce to ensure uniqueness + signature=None + )