├── 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 |
17 | 18 | Description 19 | 20 |

Figure 1: How a blockchain system operates (nodes = peers).

21 | 22 |
23 | 24 | In each block period (i.e., the interval for generating one block), the basic operation is: 25 | 26 | 1. Peers generate new transactions and broadcast them to all peers in the system. 27 | 2. One peer is selected as a block generator to package transactions as a block. 28 | 3. The new block is broadcast to all peers in the system. 29 | 4. Peers verify the validity of the block. 30 | 5. If most peers accept the block, the block is appended to the blockchain. As such, transactions in the block are stored in the blockchain permanently. A malicious peer intending to modify a block in the blockchain will change all following blocks, which requires all peers to reverify these blocks; this is considered impossible in Bitcoin. 31 | 32 | This project focuses on simulating the peer-to-peer (P2P) communication in such a blockchain system. The core functions to simulate include: 33 | 34 | - Peer Initialization 35 | - Peer Discovery 36 | - Message Sending/Receiving 37 | - Transaction and Block Generation 38 | - Dashboard Monitoring 39 | 40 | The functionalities to be realized are summarized in Figure 2. 41 | 42 | 45 | 46 |
47 | 48 | Description 49 | 50 |

Figure 2: Functionalities to complete in this project.

51 | 52 |
53 | 54 | ------------------- 55 | 56 | ## 2. Functionality of the Blockchain P2P Network 57 | 58 |
59 | 60 | Description 61 | 62 |

Figure 3: How different functionalities interact.

63 | 64 |
65 | 66 | Figure 3 shows the relationship between different functionalities. Each core component of the system is described below. 67 | 68 | ----- 69 | 70 | ### Part 1: Peer Initialization 71 | 72 | Upon joining the network, a peer: 73 | * Configures its `IP address`, `port`, and `gossip fanout`, and `localnetworkid`. 74 | * Chooses its role: `normal` or `malicious`, `lightweight` or `full`, `NATed` or `non-NATed`. 75 | * Initializes a TCP socket to receive incoming messages. 76 | 77 | **Key Terms:** 78 | * `gossip fanout`: In blockchains, peers usually adopt the gossip protocol while broadcasting blocks and transactions. That is, each peer sends blocks or transactions to a random subset instead of all of its known peers. This can reduce redundant messages in the network. Here, `gossip fanout` indicates the number of target peers while broadcasting blocks and transactions. 79 | * `normal` or `malicious` peer: A normal peer always generates correct transactions and blocks. Instead, a malicious peer can generate incorrect transactions and blocks (e.g., with the wrong block ID). 80 | * `lightweight` or `full` peer: In the introduction, we introduce that all peers verify the block and store a copy of the blockchain, which is called full peers. However, in practice, there are some resource-limited devices (e.g., mobile phones and laptops), which do not have enough computing and storage capacities to verify and store all blocks. To solve this issue, Ethereum allows peers to act as lightweight peers, which do not verify blocks and store all blocks. Instead, lightweight peers store the header of blocks without transactions. 81 | * `NATed` or `non-NATed` peer: This project considers network address translation (NAT). A NATed peer is generally located in a local network and cannot interact directly with peers outside the local network. Instead, non-NATed peers in the local network act as NAT routers or relaying peers between NATed peers and peers outside the local network. Typically, when forwarding external messages to a peer in a local network, a relaying peer must find the destination peer's IP address in the local network based on the NAT translation table. Here, to reduce the complexity, we only simulate the logic of NAT and ignore the NAT translation table; that is, a NATed peer has only one IP address across the network. 82 | * `localnetworkid`: Denote the ID of the peer's local network. 83 | 84 | ----- 85 | 86 | ### Part 2: Peer Discovery 87 | 88 | After creating the TCP socket, the peer informs known peers of its existence in order to exchange data. To do so, a peer must acquire some peers' IP addresses and ports before joining the network. 89 | 90 | Moreover, a peer needs to periodically check if the known peers are alive. 91 | 92 | The procedure of peer discovery is as follows: 93 | 94 | * Say `hello` to its known peers while joining the network. 95 | * When receiving a `hello` message, check if the sender is known. If not, add the sender to the list of known peers. 96 | * Periodically send `ping` messages to all known peers and wait for their replies, i.e., `pong` messages. 97 | * When receiving `pong` messages, update the state of known peers. Moreover, calculate the time difference between sending `ping` messages and receiving `pong` messages, which is the transmission latency between peers. 98 | * Remove unresponsive peers if no `pong` messages are received before the timeout. 99 | 100 | ------ 101 | 102 | ### Part 3: Block and Transaction Generation and Verification 103 | 104 | After initializing a peer and finding the known peers, a full peer starts generating and verifying transactions and blocks. 105 | 106 | In this project, each full peer periodically generates one transaction and broadcasts it to other full peers for verification. **A transaction is valid if the transaction ID is correct**. These transactions are also stored in the peer's local transaction pool, `tx_pool`. 107 | 108 | Since we only focus on transaction and block exchange in the blockchain P2P network, we simplify block generation here. Instead of selecting a block generator to generate a block, in each block period, each peer packages transactions in their `tx_pool` into a block independently and broadcasts it to other peers for verification. **A block is valid if the `block ID` is correct**. 109 | 110 | The procedure for transaction/block generation and verification is as follows: 111 | 112 | * Synchronize the latest blockchain from known peers while joining the network in order to link new blocks to the latest blockchain. 113 | * Start generating transactions. 114 | * Broadcast the transactions to known peers for verification. 115 | * Add the valid transactions to the local `tx_pool`. 116 | * Package the transactions in the local `tx_pool` into a new block. The transactions are removed from the local `tx_pool` after being packaged into the new block. 117 | * Broadcast the block to known peers for verification. 118 | * Add the valid block to the local blockchain. 119 | 120 | **Tips:** 121 | * When a peer sends a block to another, the sender usually sends an `INV` message with the block ID instead of the block itself. If the receiver finds that it has not yet received the block, the receiver will reply with a `GETBLOCK` message to request the block. This can reduce the network overhead. 122 | 123 | ------ 124 | 125 | ### Part 4: Sending Messages Processing 126 | 127 | To simulate the process of sending messages (e.g., transactions and blocks), all sending messages must be put into an outbox queue and sent one by one. The procedure for sending messages is as follows: 128 | 129 | * When sending a message, add the message to the outbox queue. 130 | * Read a message from the queue based on their priorities. 131 | * If the message destination is in the same local network or a non-NATed peer (while the peer is also non-NATed), send the message to the destination directly. 132 | * If the message destination is a NATed peer and in different local network, find the best relaying peer and send the message to the relaying peer. 133 | 134 | -------- 135 | 136 | ### Part 5: Receiving Messages Processing 137 | 138 | When receiving messages from other peers, the messages must be dispatched and processed based on the message type. The receiving messages processing is as follows: 139 | 140 | * Check whether the message sender is banned. Drop the message if the sender is banned. 141 | * Check whether the number of messages sent by the sender is within the limit. Drop the message if the sender sends messages too frequently. This is to prevent denial-of-service (DoS) attacks. 142 | * Check the message type and process the messages accordingly: 143 | 144 | * msg.type=`TX`, 145 | * Check the validity of the transaction. If invalid, drop the transaction and record the sender's offence.(**A transaction is valid if the transaction ID is correct**.) 146 | * Check whether the transaction has been received. If yes, drop the transaction to prevent replay attacks. 147 | * Record the count of redundant transactions if they have been received. 148 | * Add the new transaction to the local `tx_pool` if it has not been received. 149 | * Broadcast the new transaction to known peers. 150 | 151 | * msg.type=`BLOCK`, 152 | * Check the validity of the block. If invalid, drop the block and record the sender's offence.(**A block is valid if the `block ID` is correct**.) 153 | * Check whether the block has been received. If yes, drop the block to prevent replay attacks. 154 | * Record the count of redundant blocks if they have been received. 155 | * Add the new block to the list of orphaned blocks if its previous block does not exist in the blockchain due to network delay. 156 | * Add the new block to the local blockchain if its previous block exists in the blockchain. 157 | * Check whether the new block is the previous block of the orphaned blocks. 158 | * Broadcast the new block to known peers. 159 | 160 | * msg.type=`INV`, 161 | * Check whether the block ID in the INV message have been received. 162 | * Request missing blocks from the message sender. 163 | 164 | * msg.type=`GETBLOCK`, 165 | * Check whether the blocks requested are in the local blockchain. If not, request the blocks from known peers. 166 | * If the sender is a full peer, reply with the requested blocks. 167 | * If the sender is a lightweight peer, reply with the header of the requested blocks. 168 | 169 | * msg.type=`GET_BLOCK_HEADERS` 170 | * Reply with the header of blocks in the local blockchain. 171 | 172 | * msg.type=`BLOCK_HEADERS` 173 | * Check the validity of the list of block headers by checking whether the previous block of each block exists in the blockchain. 174 | * Request the missing block from known peers if the peer is a full peer. 175 | 176 | ------- 177 | 178 | ### Part 6: Start Dashboard 179 | 180 | Start a dashboard server to display the following message: 181 | 182 | * `Localhost: port/peers`: display the set of known peers. 183 | * `Localhost: port/transactions`: display the transactions in the local pool. 184 | * `Localhost: port/blocks`: display the blocks in the local blockchain. 185 | * `Localhost: port/orphan`: display the orphaned blocks. 186 | * `Localhost: port/latency`: display the transmission latency between peers. 187 | * `Localhost: port/capacity`: display the sending capacity of the peers. 188 | * `Localhost: port/redundancy`: display the number of redundant messages received. 189 | * `Localhost: port/queue`: display the message in the outbox queue. 190 | * `Localhost: port/blacklist`: display the blacklist. 191 | 192 | ------- 193 | 194 | ## 3. Functions to Complete 195 | 196 | The operation logic of the project is given in the `Main` function of `node.py`. Your task is to implement the following modules: 197 | 198 | ------- 199 | 200 | ### Part 1: Peer Initialization (`socket_server.py`) 201 | 202 | 1. `start_socket_server` 203 | 204 | * Create a TCP socket and bind it to the peer’s IP address and port. 205 | * Start listening on the socket for receiving incoming messages. 206 | * When receiving messages, pass the messages to the function `dispatch_message` in `message_handler.py`. 207 | 208 | ------- 209 | 210 | ### Part 2: Peer Discovery 211 | 212 | #### `peer_discovery.py`: This part is responsible for saying 'hello' to known peers when the peer joins the system. 213 | 214 | 1. `start_peer_discovery` 215 | 216 | * Define the JSON format of a `hello` message, which should include: `{message type, sender’s ID, IP address, port, flags, and message ID}`. A `sender’s ID` can be `peer_port`. The `flags` should indicate whether the peer is `NATed or non-NATed`, and `full or lightweight`. The `message ID` can be a random number. 217 | * Send a `hello` message to all reachable peers and put the messages into the outbox queue. Print out the event of saying `hello` to other peers. 218 | 219 | 2. `handle_hello_message` 220 | 221 | * Read information in the received `hello` message. Print out the event of receving `hello` messages from other peers. 222 | * If the sender is unknown, add it to the list of known peers (`known_peer`) and record their flags (`peer_flags`). 223 | * Update the set of reachable peers (`reachable_by`). 224 | 225 | **Tips:** Each peer can only receive `hello` messages from reachable peers and never forward `hello` messages. A NATed peer can only say `hello` to peers in the same local network. If a peer receives `hello` messages from a NATed peer, it can act as the relaying peers of the NATed peer. If a peer and a NATed peer are not in the same local network, they cannot say `hello` to each other. 226 | #### `peer_manager.py`: This part is responsible for checking the status and recording the performance of known peers. 227 | 228 | 1. `start_ping_loop` 229 | 230 | * Define the JSON format of a `ping` message, which should include `{message type, sender's ID, timestamp}`. 231 | 232 | * Send a `ping` message to each known peer periodically. 233 | 234 | 2. `create_pong` 235 | 236 | * Create the JSON format of a `pong` message, which should include `{message type, sender's ID, timestamp in the received ping message}` 237 | 238 | 3. `handle_pong` 239 | 240 | * Read the information in the received `pong` message. 241 | 242 | * Update the transmission latenty between the peer and the sender (`rtt_tracker`). 243 | 244 | 4. `start_peer_monitor` 245 | 246 | * Check the latest time to receive `ping` or `pong` message from each peer in `last_ping_time`. 247 | 248 | * If the latest time is earlier than the limit, mark the peer's status in `peer_status` as `UNREACHABLE` or otherwise `ALIVE`. 249 | 250 | 5. `update_peer_heartbeat` 251 | 252 | * Update the `last_ping_time` of a peer when receiving its `ping` or `pong` message. 253 | 254 | 6. `record_offense` 255 | 256 | * Record the offence times of a peer when malicious behaviors are detected. 257 | 258 | * Add a peer to `blacklist` if its offence times exceed 3. 259 | 260 | -------- 261 | 262 | ### Part 3: Block and Transaction Generation and Verification 263 | 264 | #### `transaction.py`: This part processes all transaction-related functions. 265 | 266 | 1. `transaction_generation` 267 | 268 | * Randomly choose a peer from `known_peers` and generate a transaction to transfer an arbitrary amount of money to the peer. 269 | 270 | * Add the transaction to local `tx_pool` using the function `add_transaction`. 271 | 272 | * Broadcast the transaction to `known_peers` using the function `gossip_message` in `outbox.py`. 273 | 274 | 2. `add_transaction` 275 | 276 | * Add a transaction to the local `tx_pool` if it is in the pool. 277 | 278 | * Add the transaction ID to `tx_ids`. 279 | 280 | 3. `get_recent_transaction` 281 | 282 | * Return all transactions in the local `tx_pool`. 283 | 284 | 4. `clear_pool` 285 | 286 | * Remove all transactions in `tx_pool` and transaction IDs in `tx_ids`. 287 | 288 | #### `block_handler.py`: This part processes all block-related functions. 289 | 290 | 1. `request_block_sync` 291 | 292 | * Define the JSON format of a `GET_BLOCK_HEADERS`, which should include `{message type, sender's ID}`. 293 | 294 | * Send a `GET_BLOCK_HEADERS` message to each known peer (to obtain the list of block headers in the latest blockchain) and put the messages in the outbox queue. 295 | 296 | 2. `block_generation` 297 | 298 | * Create a new block periodically using the function `create_dummy_block`. 299 | 300 | * Create an `INV` message for the new block using the function `create_inv` in `inv_message.py`. 301 | 302 | * Broadcast the `INV` message to known peers using the function `gossip` in `outbox.py`. 303 | 304 | 3. `create_dummy_block` 305 | 306 | * Define the JSON format of a `block`, which should include `{message type, peer's ID, timestamp, block ID, previous block's ID, and transactions}`. The `block ID` is the hash value of the block structure, except for the item `block ID`. `previous block` is the last block in the blockchain, to which the new block will be linked. If the block generator is malicious, it can generate a random block ID. 307 | 308 | * Read the transactions in the local `tx_pool` using the function `get_recent_transactions` in `transaction.py` before clearing the local transaction pool. 309 | 310 | * Create a new block with the transactions and generate the block ID using the function `compute_block_hash`. 311 | 312 | * Add the new block into the local blockchain (`receive_block`). 313 | 314 | 4. `compute_block_hash` 315 | 316 | * Compute the hash of a block except for the term `block ID`. 317 | 318 | 5. `handle_block` 319 | 320 | * Check the correctness of `block ID` in the received block. If incorrect, drop the block and record the sender's offence. 321 | 322 | * Check if the block exists in the local blockchain. If yes, drop the block. 323 | 324 | * 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 (for full peer) or the block header (for lightweight peer) to the local blockchain. 325 | 326 | * Check if the block is the previous block of blocks in `orphan_blocks`. If yes, add the orphaned blocks to the local blockchain. 327 | 328 | 6. `create_getblock` 329 | 330 | * Define the JSON format of a `GETBLOCK` message, which should include `{message type, sender's ID, requesting block IDs}`. 331 | 332 | 7. `get_block_by_id` 333 | 334 | * Return the block in the local blockchain based on the block ID. 335 | 336 | #### inv_message.py: This part processes the `INV` message while exchanging blocks. 337 | 338 | 1. `create_inv` 339 | 340 | * Define the JSON format of an `INV` message, which should include `{message type, sender's ID, sending blocks' IDs, message ID}`. Note that `INV` messages are sent before sending blocks. `sending blocks' IDs` is the ID of blocks that the sender wants to send. `message ID` can be a random number generated by `generate_message_id` in `util.py`. 341 | 342 | 2. `get_inventory` 343 | 344 | * Return the block ID of all blocks in the local blockchain. 345 | 346 | 3. `broadcast_inventory` 347 | 348 | * Create an `INV` message with all block IDs in the local blockchain. 349 | 350 | * Broadcast the `INV` message to known peers using the function `gossip_message` in `outbox.py` to synchronize the blockchain with known peers. 351 | 352 | --------- 353 | 354 | ### Part 4: Sending Message Processing (outbox.py) 355 | 356 | 1. `enqueue_message`: This function puts all sending messages into an outbox queue. 357 | 358 | * Check if the peer sends a message to the receiver too frequently using the function `is_rate_limited`. If yes, drop the message. 359 | 360 | * Check if the receiver exists in the `blacklist`. If yes, drop the message. 361 | 362 | * Classify the priority of the sending messages based on the message type using the function `classify_priority`. 363 | 364 | * Add the message to the queue (`queues`) if the length of the queue is within the limit `QUEUE_LIMIT`, or otherwise, drop the message. 365 | 366 | 2. `is_rate_limited` 367 | 368 | * Check how many messages were sent from the peer to a target peer during the `TIME_WINDOW` that ends now. 369 | 370 | * If the sending frequency exceeds the sending rate limit `RATE_LIMIT`, return `TRUE`; otherwise, record the current sending time into `peer_send_timestamps`. 371 | 372 | 3. `classify_priority` 373 | 374 | * Classify the priority of a message based on the message type. 375 | 376 | 4. `send_from_queue` (`outbox.py`) 377 | 378 | * 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. 379 | 380 | * Send the message using the function `relay_or_direct_send`, which will decide whether to send the message to the target peer directly or through a relaying peer. 381 | 382 | * Retry a message if it is sent unsuccessfully, and drop the message if the retry times exceed the limit `MAX_RETRIES`. 383 | 384 | 5. `relay_or_direct_send` 385 | 386 | * Check if the target peer is NATed. 387 | 388 | * If the target peer is NATed and in a different local network, use the function `get_relay_peer` to find the best relaying peer. Define the JSON format of a `RELAY` message, which should include `{message type, sender's ID, target peer's ID, `payload`}`. `payload` is the sending message. Send the `RELAY` message to the best relaying peer using the function `send_message`. 389 | 390 | * If the target peer is in the same local network, or both the peer and the target peer are Non-NATed, send the message to the target peer using the function `send_message`. 391 | 392 | 6. `get_relay_peer` 393 | 394 | * Find the set of relay candidates reachable from the target peer in `reachable_by` of `peer_discovery.py`. 395 | 396 | * Read the transmission latency between the sender and other peers in `rtt_tracker` in `peer_manager.py`. 397 | 398 | * Select and return the best relaying peer with the smallest transmission latency. 399 | 400 | 7. `send_message` 401 | 402 | * Send the message to the target peer. Wrap the function `send_message` with the dynamic network condition in the function `apply_network_condition`. 403 | 404 | 8. `apply_network_conditions`: This function simulates the peer's sending capacity control, message drop, and message transmission latency. 405 | 406 | * Use the function `rate_limiter.allow` to check if the peer's sending rate is out of limit. If yes, drop the message and update the drop states (`drop_stats`). 407 | 408 | * Generate a random number. If it is smaller than `DROP_PROB`, drop the message to simulate the random message drop in the channel. Update the drop states (`drop_stats`). 409 | 410 | * Add a random latency before sending the message to simulate message transmission delay. 411 | 412 | * Send the message using the function `send_func`. 413 | 414 | 9. `start_dynamic_capacity_adjustment` 415 | 416 | * Periodically change the peer's sending capacity in `rate_limiter` within the range [2, 10]. 417 | 418 | 10. `gossip_message` 419 | 420 | * Read the configuration `fanout` of the peer in `peer_config` of `peer_discovery.py`. 421 | 422 | * Randomly select the number of target peers from `known_peers`, which is equal to `fanout`. If the gossip message is a transaction, skip the lightweight peers in the `know_peers`. 423 | 424 | * Print out the event of gossiping messages. 425 | 426 | * Send the message to the selected target peer and put them in the outbox queue. 427 | 428 | 11. `get_outbox_status` 429 | 430 | * Return the message in the outbox queue. 431 | 432 | 12. `get_drop_stats` 433 | 434 | * Return the drop states (`drop_stats`). 435 | 436 | -------- 437 | 438 | ### PART 5: Receiving Message Processing (message_handler.py) 439 | 440 | 1. `dispatch_message` 441 | 442 | * Read the message. 443 | 444 | * 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`. 445 | 446 | * Check if the sender sends messages too frequently using the function `in_bound_limited`. If yes, drop the message. 447 | 448 | * Check if the sender exists in the `blacklist` of `peer_manager.py`. If yes, drop the message. 449 | 450 | * Process the message according to the message type: 451 | 452 | * msg_type == "RELAY": 453 | * Check if the peer is the target peer. 454 | * If yes, extract the payload and recall the function `dispatch_message` to process the payload. 455 | * If not, forward the message to the target peer using the function `enqueue_message` in `outbox.py`. 456 | 457 | * msg_type == "HELLO" 458 | * Call the function `handle_hello_message` in `peer_discovery.py` to process the message. 459 | 460 | * msg_type == "BLOCK" 461 | * Check the correctness of the block ID. If incorrect, record the sender's offence using the function `record_offence` in `peer_manager.py`. 462 | * Call the function `handle_block` in `block_handler.py` to process the block. 463 | * Call the function `create_inv` to create an `INV` message for the block. 464 | * Broadcast the `INV` message to known peers using the function `gossip_message` in `outbox.py`. 465 | 466 | * msg_type == "TX" 467 | * Check the correctness of the transaction ID. If incorrect, record the sender's offence using the function `record_offence` in `peer_manager.py`. 468 | * Add the transaction to `tx_pool` using the function `add_transaction` in `transaction.py`. 469 | * Broadcast the transaction to known peers using the function `gossip_message` in `outbox.py`. 470 | 471 | * msg_type == "PING" 472 | * Update the last ping time using the function `update_peer_heartbeat` in `peer_manager.py`. 473 | * Create a `pong` message using the function `create_pong` in `peer_manager.py`. 474 | * Send the `pong` message to the sender using the function `enqueue_message` in `outbox.py`. 475 | 476 | * msg_type == "PONG" 477 | * Update the last ping time using the function `update_peer_heartbeat` in `peer_manager.py`. 478 | * Call the function `handle_pong` in `peer_manager.py` to handle the message. 479 | 480 | * msg_type == "INV" 481 | * Read all block IDs in the local blockchain using the function `get_inventory` in `block_handler.py`. 482 | * Compare the local block IDs with those in the message. 483 | * If there are missing blocks, create a `GETBLOCK` message to request the missing blocks from the sender. 484 | * Send the `GETBLOCK` message to the sender using the function `enqueue_message` in `outbox.py`. 485 | 486 | * msg_type == "GETBLOCK" 487 | * Extract the block IDs from the message. 488 | * Get the blocks from the local blockchain according to the block IDs using the function `get_block_by_id` in `block_handler.py`. 489 | * If the blocks are not in the local blockchain, create a `GETBLOCK` message to request the missing blocks from known peers. 490 | * Send the `GETBLOCK` message to known peers using the function `enqueue_message` in `outbox.py`. 491 | * Retry getting the blocks from the local blockchain. If the retry times exceed 3, drop the message. 492 | * 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`. 493 | 494 | * msg_type == "GET_BLOCK_HEADERS" 495 | * Read all block headers in the local blockchain and store them in `headers`. 496 | * Create a `BLOCK_HEADERS` message, which should include `{message type, sender's ID, headers}`. 497 | * Send the `BLOCK_HEADERS` message to the requester using the function `enqueue_message` in `outbox.py`. 498 | 499 | * msg_type == "BLOCK_HEADERS" 500 | * Check if the previous block of each block exists in the local blockchain or the received block headers. 501 | * If yes and the peer is lightweight, add the block headers to the local blockchain. 502 | * 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. 503 | * If not, drop the message since there are orphaned blocks in the received message and, thus, the message is invalid. 504 | 505 | 2. `is_inbound_limited` 506 | 507 | * Record the timestamp when receiving a message from a sender. 508 | * 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`. 509 | 510 | 3. `get_redundancy_stats` 511 | 512 | * Return the times of receiving duplicated messages (`message_redundancy`). 513 | 514 | --------- 515 | 516 | ### PART 6: Dashboard (dashboard.py) 517 | 518 | * `peers`: display the information of known peers, including `{peer's ID, IP address, port, status, NATed or non-NATed, lightweight or full}`. 519 | 520 | * `transactions`: display the transactions in the local pool `tx_pool`. 521 | 522 | * `blocks`: display the blocks in the local blockchain. 523 | 524 | * `orphan`: display the orphaned blocks. 525 | 526 | * `latency`: display the transmission latency between peers. 527 | 528 | * `capacity`: display the sending capacity of the peer. 529 | 530 | * `redundancy`: display the number of redundant messages received. 531 | 532 | * `queue`: display the message in the outbox queue. 533 | 534 | * `blacklist`: display the blacklist. 535 | 536 | --------- 537 | 538 | ## 4. Test Method 539 | 540 | This project will deploy the blockchain P2P network based on Docker technology, where each peer runs in an independent container. The procedure to run a peer in a container can be summarized as follows: 541 | 542 | 1) Write a `Dockerfile` to build a container image for a peer. The image can be used to generate multiple containers, i.e., peers. 543 | 2) Define the peers in `docker-compose.yml`, including the number of containers, how the containers run, and connect with each other. 544 | 3) Use `docker compose build` to build the image for all services in `docker-compose.yml`. 545 | 4) Use `docker compose up` to generate and start containers specified in `docker-compose.yml`. 546 | 547 | We have provided the `Dockerfile` and `docker-compose.yml` in the starter code with ten peers. With these two files, we will use the following commands to check your project: 548 | 1) `docker compose up --build` to check if each peer runs correctly. 549 | 2) `localhost:port/{parameter}` to check whether the peers generate and transmit transactions and blocks correctly. 550 | 551 | **Bonus:** 552 | 1) **Dynamic Blockchain Network**: In the above test method, the number of peers in the blockchain P2P network is fixed. You may add additional functions to your project and modify Docker files to allow peers to dynamically join or leave the system without affecting the operation of other peers. Use different `config.json` for different new peers, so that newly-joined peers may not know all the peers existing in the network. 553 | 554 | 2) **Causes of Redundant Messages**: Explore the parameters that can affect the number of redundant messages received by a peer, for example, the larger the `fanout` is, the more messages are transmitted to the network and received by a peer. Change the values of the parameters to observe the number of redundant messages received by a peer. Draw figures to show their relationship. 555 | 556 | ------------ 557 | 558 | 559 | ## 5. Grading 560 | 561 | **Total Points: 100 pts** 562 | 563 | 1) Peer Initialization (5 pts) 564 | 2) Peer Discovery (10 pts) 565 | 3) Block and Transaction Generation and Verification (20 pts) 566 | 4) Sending Message Processing (25 pts) 567 | 5) Receiving Message Processing (25 pts) 568 | 6) Dashboard (5 pts) 569 | 7) **A comprehensive report about the implementation details, insights, and improvements is expected. (10 pts)** 570 | 571 | 572 | **Bonus Points: up to 20 pts** 573 | We will grade based on your implementation. 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | --------------------------------------------------------------------------------