├── Functionalities.jpg ├── Functionalities.pdf ├── Starter_Code_New ├── Dockerfile ├── utils.py ├── socket_server.py ├── inv_message.py ├── config.json ├── peer_discovery.py ├── dashboard.py ├── peer_manager.py ├── transaction.py ├── block_handler.py ├── docker-compose.yml ├── node.py ├── message_handler.py └── outbox.py ├── Blockchain_Process.jpg ├── Blockchain_Architecture.jpg ├── Functionality_Relationship.jpg └── README.md /Functionalities.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUSTech-HPCLab/CS305-2025Spring-FinalProject/HEAD/Functionalities.jpg -------------------------------------------------------------------------------- /Functionalities.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUSTech-HPCLab/CS305-2025Spring-FinalProject/HEAD/Functionalities.pdf -------------------------------------------------------------------------------- /Starter_Code_New/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | RUN pip install flask 7 | -------------------------------------------------------------------------------- /Blockchain_Process.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUSTech-HPCLab/CS305-2025Spring-FinalProject/HEAD/Blockchain_Process.jpg -------------------------------------------------------------------------------- /Blockchain_Architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUSTech-HPCLab/CS305-2025Spring-FinalProject/HEAD/Blockchain_Architecture.jpg -------------------------------------------------------------------------------- /Functionality_Relationship.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SUSTech-HPCLab/CS305-2025Spring-FinalProject/HEAD/Functionality_Relationship.jpg -------------------------------------------------------------------------------- /Starter_Code_New/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import uuid 3 | 4 | def generate_message_id(data=None): 5 | if data: 6 | return hashlib.sha256(data.encode()).hexdigest() 7 | else: 8 | return str(uuid.uuid4()) -------------------------------------------------------------------------------- /Starter_Code_New/socket_server.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | import time 4 | import json 5 | from message_handler import dispatch_message 6 | 7 | RECV_BUFFER = 4096 8 | 9 | def start_socket_server(self_id, self_ip, port): 10 | 11 | def listen_loop(): 12 | # TODO: Create a TCP socket and bind it to the peer’s IP address and port. 13 | 14 | # TODO: Start listening on the socket for receiving incoming messages. 15 | 16 | # TODO: When receiving messages, pass the messages to the function `dispatch_message` in `message_handler.py`. 17 | 18 | pass 19 | 20 | # ✅ Run listener in background 21 | threading.Thread(target=listen_loop, daemon=True).start() 22 | 23 | -------------------------------------------------------------------------------- /Starter_Code_New/inv_message.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | from utils import generate_message_id 4 | from outbox import gossip_message 5 | 6 | def create_inv(sender_id, block_ids): 7 | # TODO: * Define the JSON format of an `INV` message, which should include `{message type, sender's ID, sending blocks' IDs, message ID}`. 8 | # Note that `INV` messages are sent before sending blocks. 9 | # `sending blocks' IDs` is the ID of blocks that the sender want to send. 10 | # `message ID` can be a random number generated by `generate_message_id` in `util.py`. 11 | pass 12 | 13 | def get_inventory(): 14 | # TODO: Return the block ID of all blocks in the local blockchain. 15 | pass 16 | 17 | def broadcast_inventory(self_id): 18 | # TODO: Create an `INV` message with all block IDs in the local blockchain. 19 | 20 | # TODO: Broadcast the `INV` message to known peers using the function `gossip_message` in `outbox.py` to synchronize the blockchain with known peers. 21 | 22 | pass 23 | 24 | 25 | -------------------------------------------------------------------------------- /Starter_Code_New/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "peers": { 3 | "5000": { 4 | "ip": "172.28.0.10", 5 | "port": 5000, 6 | "fanout":1, 7 | "localnetworkid":1 8 | }, 9 | "5001": { 10 | "ip": "172.28.0.11", 11 | "port": 5001, 12 | "fanout":1, 13 | "localnetworkid":1 14 | }, 15 | "5002": { 16 | "ip": "172.28.0.12", 17 | "port": 5002, 18 | "fanout":1, 19 | "nat": true, 20 | "localnetworkid":1 21 | }, 22 | "5003": { 23 | "ip": "172.28.0.13", 24 | "port": 5003, 25 | "fanout":1, 26 | "nat": true, 27 | "localnetworkid":1 28 | }, 29 | "5004": { 30 | "ip": "172.28.0.14", 31 | "port": 5004, 32 | "fanout":1 33 | }, 34 | "5005": { 35 | "ip": "172.28.0.15", 36 | "port": 5005, 37 | "fanout":1 38 | }, 39 | "5006": { 40 | "ip": "172.28.0.16", 41 | "port": 5006, 42 | "fanout":1, 43 | "nat": true, 44 | "light": true, 45 | "localnetworkid":2 46 | }, 47 | "5007": { 48 | "ip": "172.28.0.17", 49 | "port": 5007, 50 | "fanout":1, 51 | "nat": true, 52 | "localnetworkid":2 53 | }, 54 | "5008": { 55 | "ip": "172.28.0.18", 56 | "port": 5008, 57 | "fanout":1, 58 | "light": true, 59 | "localnetworkid":2 60 | }, 61 | "5009": { 62 | "ip": "172.28.0.19", 63 | "port": 5009, 64 | "fanout":1, 65 | "localnetworkid":2 66 | }, 67 | "5010": { 68 | "ip": "172.28.0.20", 69 | "port": 5010, 70 | "fanout":1 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Starter_Code_New/peer_discovery.py: -------------------------------------------------------------------------------- 1 | import json, time, threading 2 | from utils import generate_message_id 3 | 4 | 5 | known_peers = {} # { peer_id: (ip, port) } 6 | peer_flags = {} # { peer_id: { 'nat': True/False, 'light': True/False } } 7 | reachable_by = {} # { peer_id: { set of peer_ids who can reach this peer }} 8 | peer_config={} 9 | 10 | def start_peer_discovery(self_id, self_info): 11 | from outbox import enqueue_message 12 | def loop(): 13 | # TODO: Define the JSON format of a `hello` message, which should include: `{message type, sender’s ID, IP address, port, flags, and message ID}`. 14 | # A `sender’s ID` can be `peer_port`. 15 | # The `flags` should indicate whether the peer is `NATed or non-NATed`, and `full or lightweight`. 16 | # The `message ID` can be a random number. 17 | 18 | # TODO: Send a `hello` message to all reachable peers and put the messages into the outbox queue. 19 | # Tips: A NATed peer can only say hello to peers in the same local network. 20 | # If a peer and a NATed peer are not in the same local network, they cannot say hello to each other. 21 | pass 22 | threading.Thread(target=loop, daemon=True).start() 23 | 24 | def handle_hello_message(msg, self_id): 25 | new_peers = [] 26 | 27 | # TODO: Read information in the received `hello` message. 28 | 29 | # TODO: If the sender is unknown, add it to the list of known peers (`known_peer`) and record their flags (`peer_flags`). 30 | 31 | # TODO: Update the set of reachable peers (`reachable_by`). 32 | 33 | pass 34 | 35 | return new_peers 36 | 37 | 38 | -------------------------------------------------------------------------------- /Starter_Code_New/dashboard.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify 2 | from threading import Thread 3 | from peer_manager import peer_status, rtt_tracker 4 | from transaction import get_recent_transactions 5 | from link_simulator import rate_limiter 6 | from message_handler import get_redundancy_stats 7 | from peer_discovery import known_peers 8 | import json 9 | from block_handler import received_blocks 10 | 11 | app = Flask(__name__) 12 | blockchain_data_ref = None 13 | known_peers_ref = None 14 | 15 | def start_dashboard(peer_id, port): 16 | global blockchain_data_ref, known_peers_ref 17 | blockchain_data_ref = received_blocks 18 | known_peers_ref = known_peers 19 | def run(): 20 | app.run(host="0.0.0.0", port=port) 21 | Thread(target=run, daemon=True).start() 22 | 23 | @app.route('/') 24 | def home(): 25 | return "Block P2P Network Simulation" 26 | 27 | @app.route('/blocks') 28 | def blocks(): 29 | # TODO: display the blocks in the local blockchain. 30 | pass 31 | 32 | @app.route('/peers') 33 | def peers(): 34 | # TODO: display the information of known peers, including `{peer's ID, IP address, port, status, NATed or non-NATed, lightweight or full}`. 35 | pass 36 | 37 | @app.route('/transactions') 38 | def transactions(): 39 | # TODO: display the transactions in the local pool `tx_pool`. 40 | pass 41 | 42 | @app.route('/latency') 43 | def latency(): 44 | # TODO: display the transmission latency between peers. 45 | pass 46 | 47 | @app.route('/capacity') 48 | def capacity(): 49 | # TODO: display the sending capacity of the peer. 50 | pass 51 | 52 | @app.route('/orphans') 53 | def orphan_blocks(): 54 | # TODO: display the orphaned blocks. 55 | pass 56 | 57 | @app.route('/queue') 58 | def message_queue(): 59 | # TODO: display the messages in the outbox queue. 60 | pass 61 | 62 | @app.route('/redundancy') 63 | def redundancy_stats(): 64 | # TODO: display the number of redundant messages received. 65 | pass 66 | 67 | @app.route('/blacklist') 68 | def blacklist_display(): 69 | # TODO: display the blacklist. 70 | pass 71 | 72 | 73 | -------------------------------------------------------------------------------- /Starter_Code_New/peer_manager.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import json 4 | from collections import defaultdict 5 | 6 | 7 | peer_status = {} # {peer_id: 'ALIVE', 'UNREACHABLE' or 'UNKNOWN'} 8 | last_ping_time = {} # {peer_id: timestamp} 9 | rtt_tracker = {} # {peer_id: transmission latency} 10 | 11 | # === Check if peers are alive === 12 | 13 | def start_ping_loop(self_id, peer_table): 14 | from outbox import enqueue_message 15 | def loop(): 16 | # TODO: Define the JSON format of a `ping` message, which should include `{message typy, sender's ID, timestamp}`. 17 | 18 | # TODO: Send a `ping` message to each known peer periodically. 19 | pass 20 | threading.Thread(target=loop, daemon=True).start() 21 | 22 | def create_pong(sender, recv_ts): 23 | # TODO: Create the JSON format of a `pong` message, which should include `{message type, sender's ID, timestamp in the received ping message}`. 24 | pass 25 | 26 | def handle_pong(msg): 27 | # TODO: Read the information in the received `pong` message. 28 | 29 | # TODO: Update the transmission latenty between the peer and the sender (`rtt_tracker`). 30 | pass 31 | 32 | 33 | def start_peer_monitor(): 34 | import threading 35 | def loop(): 36 | # TODO: Check the latest time to receive `ping` or `pong` message from each peer in `last_ping_time`. 37 | 38 | # TODO: If the latest time is earlier than the limit, mark the peer's status in `peer_status` as `UNREACHABLE` or otherwise `ALIVE`. 39 | 40 | pass 41 | threading.Thread(target=loop, daemon=True).start() 42 | 43 | def update_peer_heartbeat(peer_id): 44 | # TODO: Update the `last_ping_time` of a peer when receiving its `ping` or `pong` message. 45 | pass 46 | 47 | 48 | # === Blacklist Logic === 49 | 50 | blacklist = set() # The set of banned peers 51 | 52 | peer_offense_counts = {} # The offence times of peers 53 | 54 | def record_offense(peer_id): 55 | # TODO: Record the offence times of a peer when malicious behaviors are detected. 56 | 57 | # TODO: Add a peer to `blacklist` if its offence times exceed 3. 58 | 59 | pass 60 | 61 | -------------------------------------------------------------------------------- /Starter_Code_New/transaction.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import hashlib 4 | import random 5 | import threading 6 | from peer_discovery import known_peers 7 | from outbox import gossip_message 8 | 9 | class TransactionMessage: 10 | def __init__(self, sender, receiver, amount, timestamp=None): 11 | self.type = "TX" 12 | self.from_peer = sender 13 | self.to_peer = receiver 14 | self.amount = amount 15 | self.timestamp = timestamp if timestamp else time.time() 16 | self.id = self.compute_hash() 17 | 18 | def compute_hash(self): 19 | tx_data = { 20 | "type": self.type, 21 | "from": self.from_peer, 22 | "to": self.to_peer, 23 | "amount": self.amount, 24 | "timestamp": self.timestamp 25 | } 26 | return hashlib.sha256(json.dumps(tx_data, sort_keys=True).encode()).hexdigest() 27 | 28 | def to_dict(self): 29 | return { 30 | "type": self.type, 31 | "id": self.id, 32 | "from": self.from_peer, 33 | "to": self.to_peer, 34 | "amount": self.amount, 35 | "timestamp": self.timestamp 36 | } 37 | 38 | @staticmethod 39 | def from_dict(data): 40 | return TransactionMessage( 41 | sender=data["from"], 42 | receiver=data["to"], 43 | amount=data["amount"], 44 | timestamp=data["timestamp"] 45 | ) 46 | 47 | tx_pool = [] # local transaction pool 48 | tx_ids = set() # the set of IDs of transactions in the local pool 49 | 50 | def transaction_generation(self_id, interval=15): 51 | def loop(): 52 | # TODO: Randomly choose a peer from `known_peers` and generate a transaction to transfer arbitrary amount of money to the peer. 53 | 54 | # TODO: Add the transaction to local `tx_pool` using the function `add_transaction`. 55 | 56 | # TODO: Broadcast the transaction to `known_peers` using the function `gossip_message` in `outbox.py`. 57 | 58 | pass 59 | threading.Thread(target=loop, daemon=True).start() 60 | 61 | def add_transaction(tx): 62 | # TODO: Add a transaction to the local `tx_pool` if it is in the pool. 63 | 64 | # TODO: Add the transaction ID to `tx_ids`. 65 | pass 66 | 67 | def get_recent_transactions(): 68 | # TODO: Return all transactions in the local `tx_pool`. 69 | pass 70 | 71 | def clear_pool(): 72 | # Remove all transactions in `tx_pool` and transaction IDs in `tx_ids`. 73 | 74 | pass -------------------------------------------------------------------------------- /Starter_Code_New/block_handler.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | import hashlib 4 | import json 5 | import threading 6 | from transaction import get_recent_transactions, clear_pool 7 | from peer_discovery import known_peers, peer_config 8 | 9 | from outbox import enqueue_message, gossip_message 10 | from utils import generate_message_id 11 | from peer_manager import record_offense 12 | 13 | received_blocks = [] # The local blockchain. The blocks are added linearly at the end of the set. 14 | header_store = [] # The header of blocks in the local blockchain. Used by lightweight peers. 15 | orphan_blocks = {} # The block whose previous block is not in the local blockchain. Waiting for the previous block. 16 | 17 | def request_block_sync(self_id): 18 | # TODO: Define the JSON format of a `GET_BLOCK_HEADERS`, which should include `{message type, sender's ID}`. 19 | 20 | # TODO: Send a `GET_BLOCK_HEADERS` message to each known peer and put the messages in the outbox queue. 21 | pass 22 | 23 | def block_generation(self_id, MALICIOUS_MODE, interval=20): 24 | from inv_message import create_inv 25 | def mine(): 26 | 27 | # TODO: Create a new block periodically using the function `create_dummy_block`. 28 | 29 | # TODO: Create an `INV` message for the new block using the function `create_inv` in `inv_message.py`. 30 | 31 | # TODO: Broadcast the `INV` message to known peers using the function `gossip` in `outbox.py`. 32 | pass 33 | threading.Thread(target=mine, daemon=True).start() 34 | 35 | def create_dummy_block(peer_id, MALICIOUS_MODE): 36 | 37 | # TODO: Define the JSON format of a `block`, which should include `{message type, peer's ID, timestamp, block ID, previous block's ID, and transactions}`. 38 | # The `block ID` is the hash value of block structure except for the item `block ID`. 39 | # `previous block` is the last block in the blockchain, to which the new block will be linked. 40 | # If the block generator is malicious, it can generate random block ID. 41 | 42 | # TODO: Read the transactions in the local `tx_pool` using the function `get_recent_transactions` in `transaction.py`. 43 | 44 | # TODO: Create a new block with the transactions and generate the block ID using the function `compute_block_hash`. 45 | 46 | # TODO: Clear the local transaction pool and add the new block into the local blockchain (`receive_block`). 47 | pass 48 | return block 49 | 50 | def compute_block_hash(block): 51 | # TODO: Compute the hash of a block except for the term `block ID`. 52 | pass 53 | 54 | def handle_block(msg, self_id): 55 | # TODO: Check the correctness of `block ID` in the received block. If incorrect, drop the block and record the sender's offence. 56 | 57 | # TODO: Check if the block exists in the local blockchain. If yes, drop the block. 58 | 59 | # TODO: Check if the previous block of the block exists in the local blockchain. If not, add the block to the list of orphaned blocks (`orphan_blocks`). If yes, add the block to the local blockchain. 60 | 61 | # TODO: Check if the block is the previous block of blocks in `orphan_blocks`. If yes, add the orphaned blocks to the local blockchain. 62 | pass 63 | 64 | 65 | def create_getblock(sender_id, requested_ids): 66 | # TODO: Define the JSON format of a `GETBLOCK` message, which should include `{message type, sender's ID, requesting block IDs}`. 67 | pass 68 | 69 | 70 | def get_block_by_id(block_id): 71 | # TODO: Return the block in the local blockchain based on the block ID. 72 | pass 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /Starter_Code_New/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | peer5000: 3 | build: . 4 | container_name: peer5000 5 | hostname: peer5000 6 | ports: 7 | - "6000:5000" # Map peer TCP: 6000 -> 5000 8 | - "8000:7000" # Map dashboard: 8000 -> 7000 9 | command: ["python", "node.py", "--id", "5000", "--config", "config.json"] 10 | networks: 11 | p2pnet: 12 | ipv4_address: 172.28.0.10 13 | 14 | peer5001: 15 | build: . 16 | container_name: peer5001 17 | hostname: peer5001 18 | ports: 19 | - "6001:5001" 20 | - "8001:7001" 21 | command: ["python", "node.py", "--id", "5001", "--config", "config.json", "--mode", "malicious"] 22 | networks: 23 | p2pnet: 24 | ipv4_address: 172.28.0.11 25 | 26 | peer5002: 27 | build: . 28 | container_name: peer5002 29 | hostname: peer5002 30 | ports: 31 | - "6002:5002" 32 | - "8002:7002" 33 | command: ["python", "node.py", "--id", "5002", "--config", "config.json"] 34 | networks: 35 | p2pnet: 36 | ipv4_address: 172.28.0.12 37 | 38 | peer5003: 39 | build: . 40 | container_name: peer5003 41 | hostname: peer5003 42 | ports: 43 | - "6003:5003" 44 | - "8003:7003" 45 | command: ["python", "node.py", "--id", "5003", "--config", "config.json"] 46 | networks: 47 | p2pnet: 48 | ipv4_address: 172.28.0.13 49 | 50 | peer5004: 51 | build: . 52 | container_name: peer5004 53 | hostname: peer5004 54 | ports: 55 | - "6004:5004" 56 | - "8004:7004" 57 | command: ["python", "node.py", "--id", "5004", "--config", "config.json", "--mode", "malicious"] 58 | networks: 59 | p2pnet: 60 | ipv4_address: 172.28.0.14 61 | 62 | peer5005: 63 | build: . 64 | container_name: peer5005 65 | hostname: peer5005 66 | ports: 67 | - "6005:5005" 68 | - "8005:7005" 69 | command: ["python", "node.py", "--id", "5005", "--config", "config.json"] 70 | networks: 71 | p2pnet: 72 | ipv4_address: 172.28.0.15 73 | 74 | peer5006: 75 | build: . 76 | container_name: peer5006 77 | hostname: peer5006 78 | ports: 79 | - "6006:5006" 80 | - "8006:7006" 81 | command: ["python", "node.py", "--id", "5006", "--config", "config.json"] 82 | networks: 83 | p2pnet: 84 | ipv4_address: 172.28.0.16 85 | 86 | peer5007: 87 | build: . 88 | container_name: peer5007 89 | hostname: peer5007 90 | ports: 91 | - "6007:5007" 92 | - "8007:7007" 93 | command: ["python", "node.py", "--id", "5007", "--config", "config.json"] 94 | networks: 95 | p2pnet: 96 | ipv4_address: 172.28.0.17 97 | 98 | peer5008: 99 | build: . 100 | container_name: peer5008 101 | hostname: peer5008 102 | ports: 103 | - "6008:5008" 104 | - "8008:7008" 105 | command: ["python", "node.py", "--id", "5008", "--config", "config.json"] 106 | networks: 107 | p2pnet: 108 | ipv4_address: 172.28.0.18 109 | 110 | peer5009: 111 | build: . 112 | container_name: peer5009 113 | hostname: peer5009 114 | ports: 115 | - "6009:5009" 116 | - "8009:7009" 117 | command: ["python", "node.py", "--id", "5009", "--config", "config.json"] 118 | networks: 119 | p2pnet: 120 | ipv4_address: 172.28.0.19 121 | 122 | 123 | peer5010: 124 | build: . 125 | container_name: peer5010 126 | hostname: peer5010 127 | ports: 128 | - "6010:5010" 129 | - "8010:7010" 130 | command: ["python", "node.py", "--id", "5010", "--config", "config.json"] 131 | networks: 132 | p2pnet: 133 | ipv4_address: 172.28.0.20 134 | 135 | networks: 136 | p2pnet: 137 | driver: bridge 138 | ipam: 139 | config: 140 | - subnet: 172.28.0.0/16 141 | -------------------------------------------------------------------------------- /Starter_Code_New/node.py: -------------------------------------------------------------------------------- 1 | print("=== NODE.PY LOADED ===", flush=True) 2 | 3 | import json 4 | import threading 5 | import argparse 6 | import time 7 | import traceback 8 | from peer_discovery import start_peer_discovery, known_peers, peer_flags, peer_config 9 | from block_handler import block_generation, request_block_sync 10 | from message_handler import cleanup_seen_messages 11 | from socket_server import start_socket_server 12 | from dashboard import start_dashboard 13 | from peer_manager import start_peer_monitor, start_ping_loop 14 | from outbox import send_from_queue 15 | from link_simulator import start_dynamic_capacity_adjustment 16 | from inv_message import broadcast_inventory 17 | from transaction import transaction_generation 18 | 19 | def main(): 20 | 21 | # Import the peer's configuration from command line 22 | parser = argparse.ArgumentParser() 23 | parser.add_argument("--id", required=True) 24 | parser.add_argument("--config", default="config.json") 25 | parser.add_argument("--fanout", type=int, help="Override fanout for this peer") 26 | parser.add_argument("--mode", default="normal", help="Node mode: normal or malicious") 27 | args = parser.parse_args() 28 | MALICIOUS_MODE = args.mode == 'malicious' 29 | 30 | self_id = args.id 31 | 32 | print(f"[{self_id}] Starting node...", flush=True) 33 | 34 | with open(args.config) as f: 35 | config = json.load(f) 36 | 37 | self_info = config["peers"][self_id] 38 | 39 | peer_flags[self_id] = { 40 | "nat": self_info.get("nat", False), 41 | "light": self_info.get("light", False) 42 | } 43 | 44 | for peer_id, peer_info in config["peers"].items(): 45 | known_peers[peer_id] = (peer_info["ip"], peer_info["port"]) 46 | peer_config = config["peers"] 47 | 48 | if args.fanout: 49 | peer_config[self_id]["fanout"] = args.fanout 50 | print(f"[{self_id}] Overriding fanout to {args.fanout}", flush=True) 51 | 52 | ip = self_info["ip"] 53 | port = self_info["port"] 54 | 55 | # Start socket and listen for incoming messages 56 | print(f"[{self_id}] Starting socket server on {ip}:{port}", flush=True) 57 | start_socket_server(self_id, ip, port) 58 | 59 | # Peer Discovery 60 | print(f"[{self_id}] Starting peer discovery", flush=True) 61 | start_peer_discovery(self_id, self_info) 62 | 63 | print(f"[{self_id}] Starting ping loop", flush=True) 64 | start_ping_loop(self_id, known_peers) 65 | 66 | print(f"[{self_id}] Starting peer monitor", flush=True) 67 | start_peer_monitor() 68 | 69 | # Block and Transaction Generation and Verification 70 | print(f"[{self_id}] Starting block sync thread", flush=True) 71 | threading.Thread(target=request_block_sync, args=(self_id,), daemon=True).start() 72 | 73 | if not self_info.get('light', False): 74 | print(f"[{self_id}] Starting transaction and block generation", flush=True) 75 | transaction_generation(self_id) 76 | block_generation(self_id, MALICIOUS_MODE) 77 | 78 | print(f"[{self_id}] Starting broadcast inventory thread", flush=True) 79 | threading.Thread(target=broadcast_inventory, args=(self_id,), daemon=True).start() 80 | 81 | # Sending Message Processing 82 | print(f"[{self_id}] Starting outbound queue", flush=True) 83 | send_from_queue(self_id) 84 | 85 | print(f"[{self_id}] Starting dynamic capacity adjustment", flush=True) 86 | start_dynamic_capacity_adjustment() 87 | 88 | # Start dashboard 89 | time.sleep(2) 90 | print(f"[{self_id}] Known peers before dashboard start: {known_peers}", flush=True) 91 | print(f"[{self_id}] Starting dashboard on port {port + 2000}", flush=True) 92 | start_dashboard(self_id, port + 2000) 93 | 94 | print(f"[{self_id}] Node is now running at {ip}:{port}", flush=True) 95 | while True: 96 | print(f"[{self_id}] Still alive at {time.strftime('%X')}", flush=True) 97 | time.sleep(60) 98 | 99 | if __name__ == "__main__": 100 | try: 101 | main() 102 | except Exception as e: 103 | print(f"[FATAL] Exception in main(): {e}", flush=True) 104 | traceback.print_exc() -------------------------------------------------------------------------------- /Starter_Code_New/message_handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import threading 3 | import time 4 | import hashlib 5 | import random 6 | from collections import defaultdict 7 | from peer_discovery import handle_hello_message, known_peers, peer_config 8 | from block_handler import handle_block, get_block_by_id, create_getblock, received_blocks, header_store 9 | from inv_message import create_inv, get_inventory 10 | from block_handler import create_getblock 11 | from peer_manager import update_peer_heartbeat, record_offense, create_pong, handle_pong 12 | from transaction import add_transaction 13 | from outbox import enqueue_message, gossip_message 14 | 15 | 16 | # === Global State === 17 | SEEN_EXPIRY_SECONDS = 600 # 10 minutes 18 | seen_message_ids = {} 19 | seen_txs = set() 20 | redundant_blocks = 0 21 | redundant_txs = 0 22 | message_redundancy = 0 23 | peer_inbound_timestamps = defaultdict(list) 24 | 25 | 26 | # === Inbound Rate Limiting === 27 | INBOUND_RATE_LIMIT = 10 28 | INBOUND_TIME_WINDOW = 10 # seconds 29 | 30 | def is_inbound_limited(peer_id): 31 | # TODO: Record the timestamp when receiving message from a sender. 32 | 33 | # TODO: Check if the number of messages sent by the sender exceeds `INBOUND_RATE_LIMIT` during the `INBOUND_TIME_WINDOW`. If yes, return `TRUE`. If not, return `FALSE`. 34 | 35 | pass 36 | 37 | # === Redundancy Tracking === 38 | 39 | def get_redundancy_stats(): 40 | # TODO: Return the times of receiving duplicated messages (`message_redundancy`). 41 | pass 42 | 43 | # === Main Message Dispatcher === 44 | def dispatch_message(msg, self_id, self_ip): 45 | 46 | msg_type = msg.get["type"] 47 | 48 | # TODO: Read the message. 49 | 50 | # TODO: Check if the message has been seen in `seen_message_ids` to prevent replay attacks. If yes, drop the message and add one to `message_redundancy`. If not, add the message ID to `seen_message_ids`. 51 | 52 | # TODO: Check if the sender sends message too frequently using the function `in_bound_limited`. If yes, drop the message. 53 | 54 | # TODO: Check if the sender exists in the `blacklist` of `peer_manager.py`. If yes, drop the message. 55 | 56 | 57 | if msg_type == "RELAY": 58 | 59 | # TODO: Check if the peer is the target peer. 60 | # If yes, extract the payload and recall the function `dispatch_message` to process the payload. 61 | # If not, forward the message to target peer using the function `enqueue_message` in `outbox.py`. 62 | pass 63 | 64 | elif msg_type == "HELLO": 65 | # TODO: Call the function `handle_hello_message` in `peer_discovery.py` to process the message. 66 | pass 67 | 68 | elif msg_type == "BLOCK": 69 | # TODO: Check the correctness of block ID. If incorrect, record the sender's offence using the function `record_offence` in `peer_manager.py`. 70 | 71 | # TODO: Call the function `handle_block` in `block_handler.py` to process the block. 72 | 73 | # TODO: Call the function `create_inv` to create an `INV` message for the block. 74 | 75 | # TODO: Broadcast the `INV` message to known peers using the function `gossip_message` in `outbox.py`. 76 | 77 | pass 78 | 79 | 80 | elif msg_type == "TX": 81 | 82 | # TODO: Check the correctness of transaction ID. If incorrect, record the sender's offence using the function `record_offence` in `peer_manager.py`. 83 | 84 | # TODO: Add the transaction to `tx_pool` using the function `add_transaction` in `transaction.py`. 85 | 86 | # TODO: Broadcast the transaction to known peers using the function `gossip_message` in `outbox.py`. 87 | 88 | pass 89 | 90 | elif msg_type == "PING": 91 | 92 | # TODO: Update the last ping time using the function `update_peer_heartbeat` in `peer_manager.py`. 93 | 94 | # TODO: Create a `pong` message using the function `create_pong` in `peer_manager.py`. 95 | 96 | # TODO: Send the `pong` message to the sender using the function `enqueue_message` in `outbox.py`. 97 | 98 | pass 99 | 100 | elif msg_type == "PONG": 101 | 102 | # TODO: Update the last ping time using the function `update_peer_heartbeat` in `peer_manager.py`. 103 | 104 | # TODO: Call the function `handle_pong` in `peer_manager.py` to handle the message. 105 | 106 | pass 107 | 108 | elif msg_type == "INV": 109 | 110 | # TODO: Read all blocks IDs in the local blockchain using the function `get_inventory` in `block_handler.py`. 111 | 112 | # TODO: Compare the local block IDs with those in the message. 113 | 114 | # TODO: If there are missing blocks, create a `GETBLOCK` message to request the missing blocks from the sender. 115 | 116 | # TODO: Send the `GETBLOCK` message to the sender using the function `enqueue_message` in `outbox.py`. 117 | 118 | pass 119 | 120 | elif msg_type == "GETBLOCK": 121 | 122 | # TODO: Extract the block IDs from the message. 123 | 124 | # TODO: Get the blocks from the local blockchain according to the block IDs using the function `get_block_by_id` in `block_handler.py`. 125 | 126 | # TODO: If the blocks are not in the local blockchain, create a `GETBLOCK` message to request the missing blocks from known peers. 127 | 128 | # TODO: Send the `GETBLOCK` message to known peers using the function `enqueue_message` in `outbox.py`. 129 | 130 | # TODO: Retry getting the blocks from the local blockchain. If the retry times exceed 3, drop the message. 131 | 132 | # TODO: If the blocks exist in the local blockchain, send the blocks one by one to the requester using the function `enqueue_message` in `outbox.py`. 133 | 134 | pass 135 | 136 | elif msg_type == "GET_BLOCK_HEADERS": 137 | 138 | # TODO: Read all block header in the local blockchain and store them in `headers`. 139 | 140 | # TODO: Create a `BLOCK_HEADERS` message, which should include `{message type, sender's ID, headers}`. 141 | 142 | # TODO: Send the `BLOCK_HEADERS` message to the requester using the function `enqueue_message` in `outbox.py`. 143 | 144 | pass 145 | 146 | elif msg_type == "BLOCK_HEADERS": 147 | 148 | # TODO: Check if the previous block of each block exists in the local blockchain or the received block headers. 149 | 150 | # TODO: If yes and the peer is lightweight, add the block headers to the local blockchain. 151 | 152 | # TODO: If yes and the peer is full, check if there are missing blocks in the local blockchain. If there are missing blocks, create a `GET_BLOCK` message and send it to the sender. 153 | 154 | # TODO: If not, drop the message since there are orphaned blocks in the received message and, thus, the message is invalid. 155 | 156 | pass 157 | 158 | 159 | else: 160 | print(f"[{self_id}] Unknown message type: {msg_type}", flush=True) -------------------------------------------------------------------------------- /Starter_Code_New/outbox.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | import time 4 | import json 5 | import random 6 | from collections import defaultdict, deque 7 | from threading import Lock 8 | 9 | # === Per-peer Rate Limiting === 10 | RATE_LIMIT = 10 # max messages 11 | TIME_WINDOW = 10 # per seconds 12 | peer_send_timestamps = defaultdict(list) # the timestamps of sending messages to each peer 13 | 14 | MAX_RETRIES = 3 15 | RETRY_INTERVAL = 5 # seconds 16 | QUEUE_LIMIT = 50 17 | 18 | # Priority levels 19 | PRIORITY_HIGH = {"PING", "PONG", "BLOCK", "INV", "GETDATA"} 20 | PRIORITY_MEDIUM = {"TX", "HELLO"} 21 | PRIORITY_LOW = {"RELAY"} 22 | 23 | DROP_PROB = 0.05 24 | LATENCY_MS = (20, 100) 25 | SEND_RATE_LIMIT = 5 # messages per second 26 | 27 | drop_stats = { 28 | "BLOCK": 0, 29 | "TX": 0, 30 | "HELLO": 0, 31 | "PING": 0, 32 | "PONG": 0, 33 | "OTHER": 0 34 | } 35 | 36 | # Queues per peer and priority 37 | queues = defaultdict(lambda: defaultdict(deque)) 38 | retries = defaultdict(int) 39 | lock = threading.Lock() 40 | 41 | # === Sending Rate Limiter === 42 | class RateLimiter: 43 | def __init__(self, rate=SEND_RATE_LIMIT): 44 | self.capacity = rate # Max burst size 45 | self.tokens = rate # Start full 46 | self.refill_rate = rate # Tokens added per second 47 | self.last_check = time.time() 48 | self.lock = Lock() 49 | 50 | def allow(self): 51 | with self.lock: 52 | now = time.time() 53 | elapsed = now - self.last_check 54 | self.tokens += elapsed * self.refill_rate 55 | self.tokens = min(self.tokens, self.capacity) 56 | self.last_check = now 57 | 58 | if self.tokens >= 1: 59 | self.tokens -= 1 60 | return True 61 | return False 62 | 63 | rate_limiter = RateLimiter() 64 | 65 | def enqueue_message(target_id, ip, port, message): 66 | from peer_manager import blacklist, rtt_tracker 67 | 68 | # TODO: Check if the peer sends message to the receiver too frequently using the function `is_rate_limited`. If yes, drop the message. 69 | 70 | # TODO: Check if the receiver exists in the `blacklist`. If yes, drop the message. 71 | 72 | # TODO: Classify the priority of the sending messages based on the message type using the function `classify_priority`. 73 | 74 | # TODO: Add the message to the queue (`queues`) if the length of the queue is within the limit `QUEUE_LIMIT`, or otherwise, drop the message. 75 | pass 76 | 77 | 78 | def is_rate_limited(peer_id): 79 | # TODO:Check how many messages were sent from the peer to a target peer during the `TIME_WINDOW` that ends now. 80 | 81 | # TODO: If the sending frequency exceeds the sending rate limit `RATE_LIMIT`, return `TRUE`; otherwise, record the current sending time into `peer_send_timestamps`. 82 | pass 83 | 84 | def classify_priority(message): 85 | # TODO: Classify the priority of a message based on the message type. 86 | pass 87 | 88 | 89 | def send_from_queue(self_id): 90 | def worker(): 91 | 92 | # TODO: Read the message in the queue. Each time, read one message with the highest priority of a target peer. After sending the message, read the message of the next target peer. This ensures the fairness of sending messages to different target peers. 93 | 94 | # TODO: Send the message using the function `relay_or_direct_send`, which will decide whether to send the message to target peer directly or through a relaying peer. 95 | 96 | # TODO: Retry a message if it is sent unsuccessfully and drop the message if the retry times exceed the limit `MAX_RETRIES`. 97 | 98 | pass 99 | threading.Thread(target=worker, daemon=True).start() 100 | 101 | def relay_or_direct_send(self_id, dst_id, message): 102 | from peer_discovery import known_peers, peer_flags 103 | 104 | # TODO: Check if the target peer is NATed. 105 | 106 | # TODO: If the target peer is NATed, use the function `get_relay_peer` to find the best relaying peer. 107 | # Define the JSON format of a `RELAY` message, which should include `{message type, sender's ID, target peer's ID, `payload`}`. 108 | # `payload` is the sending message. 109 | # Send the `RELAY` message to the best relaying peer using the function `send_message`. 110 | 111 | # TODO: If the target peer is non-NATed, send the message to the target peer using the function `send_message`. 112 | 113 | pass 114 | 115 | def get_relay_peer(self_id, dst_id): 116 | from peer_manager import rtt_tracker 117 | from peer_discovery import known_peers, reachable_by 118 | 119 | # TODO: Find the set of relay candidates reachable from the target peer in `reachable_by` of `peer_discovery.py`. 120 | 121 | # TODO: Read the transmission latency between the sender and other peers in `rtt_tracker` in `peer_manager.py`. 122 | 123 | # TODO: Select and return the best relaying peer with the smallest transmission latency. 124 | pass 125 | 126 | return best_peer # (peer_id, ip, port) or None 127 | 128 | def send_message(ip, port, message): 129 | 130 | # TODO: Send the message to the target peer. 131 | # Wrap the function `send_message` with the dynamic network condition in the function `apply_network_condition` of `link_simulator.py`. 132 | pass 133 | 134 | send_message = apply_network_conditions(send_message) 135 | 136 | def apply_network_conditions(send_func): 137 | def wrapper(ip, port, message): 138 | 139 | # TODO: Use the function `rate_limiter.allow` to check if the peer's sending rate is out of limit. 140 | # If yes, drop the message and update the drop states (`drop_stats`). 141 | 142 | # TODO: Generate a random number. If it is smaller than `DROP_PROB`, drop the message to simulate the random message drop in the channel. 143 | # Update the drop states (`drop_stats`). 144 | 145 | # TODO: Add a random latency before sending the message to simulate message transmission delay. 146 | 147 | # TODO: Send the message using the function `send_func`. 148 | pass 149 | return wrapper 150 | 151 | def start_dynamic_capacity_adjustment(): 152 | def adjust_loop(): 153 | # TODO: Peridically change the peer's sending capacity in `rate_limiter` within the range [2, 10]. 154 | pass 155 | threading.Thread(target=adjust_loop, daemon=True).start() 156 | 157 | 158 | def gossip_message(self_id, message, fanout=3): 159 | 160 | from peer_discovery import known_peers, peer_config 161 | 162 | # TODO: Read the configuration `fanout` of the peer in `peer_config` of `peer_discovery.py`. 163 | 164 | # TODO: Randomly select the number of target peer from `known_peers`, which is equal to `fanout`. If the gossip message is a transaction, skip the lightweight peers in the `know_peers`. 165 | 166 | # TODO: Send the message to the selected target peer and put them in the outbox queue. 167 | pass 168 | 169 | def get_outbox_status(): 170 | # TODO: Return the message in the outbox queue. 171 | pass 172 | 173 | 174 | def get_drop_stats(): 175 | # TODO: Return the drop states (`drop_stats`). 176 | pass 177 | 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS305 2025 Spring Final Project - Blockchain Network Simulation 2 | 3 | **We will grade all the projects based on the latest version of this specification. Please read this project specification carefully and keep track of the updates!!!**. 4 | 5 | **IMPORTANT NOTE: We try our best to make this specification as clear as possible and cover all the problems we met during our testing. However, it is not uncommon for us to still miss important details in this specification. If anything is unclear, you should submit issues in this repository or contact the instructors and SAs immediately rather than guessing what you must do. Again, if you have any questions, please confirm with the instructors and SAs before starting**. 6 | 7 | 8 | ## 1. Introduction 9 | 10 | Bitcoin (BTC) and Ethereum (ETH) are among the most widely known and secure cryptocurrency systems. They allow users to transfer digital coins (e.g., BTC and ETH) without revealing their identities or relying on centralized platforms like banks or Alipay. These capabilities are made possible by **blockchain technology**. 11 | 12 | A blockchain is a decentralized ledger that stores an ever-expanding sequence of records, known as **blocks**, which are securely connected through cryptographic hashes. Each block contains a hash of the previous block, a timestamp, and a list of transactions (usually organized as a Merkle tree with the transactions at the leaves). This design forms a chain-like structure, where each block is linked to its predecessor, similar to a linked list. Because each block depends on the integrity of the one before it, altering data retroactively would require modifying all subsequent blocks and achieving **consensus** across the network. This makes blockchain systems highly resistant to tampering and helps prevent issues like double-spending, counterfeiting, fraud, and unauthorized creation of assets. 13 | 14 | We take Ethereum as an example. Ethereum is a public blockchain, where any peer can join or leave freely. Entities in the system are called blockchain peers (**peers** later), which can not only generate transactions but also verify transactions generated by other peers and package valid transactions in the form of blocks. These blocks are then verified by all peers in the system and linked to the blockchain if most peers accept them. This process is called **blockchain consensus** (i.e., block generation and verification). Here, each peer stores a copy of the blockchain locally. Thus, a block being linked to the blockchain means that each peer stores the block in their local blockchains. The blockchain consensus ensures that all peers store the same copy of the blockchain. 15 | 16 |
19 |
20 | Figure 1: How a blockchain system operates (nodes = peers).
21 | 22 |
49 |
50 | Figure 2: Functionalities to complete in this project.
51 | 52 |
61 |
62 | Figure 3: How different functionalities interact.
63 | 64 |