GitHub Action
		
	commited on
		
		
					Commit 
							
							·
						
						4d94786
	
1
								Parent(s):
							
							1e1ee41
								
Sync from GitHub with Git LFS
Browse files- agents/_not_used/peer_sync.py +731 -0
- agents/_not_used/peer_sync.py.old +259 -0
- agents/peer_sync.py +247 -400
- agents/tools/storage.py +72 -51
- assets/logo-hand-small.png +3 -0
    	
        agents/_not_used/peer_sync.py
    ADDED
    
    | @@ -0,0 +1,731 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            # agent/peer_sync.py
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            import socket
         | 
| 4 | 
            +
            import json
         | 
| 5 | 
            +
            import time
         | 
| 6 | 
            +
            import threading
         | 
| 7 | 
            +
            import select
         | 
| 8 | 
            +
            import netifaces
         | 
| 9 | 
            +
            import re
         | 
| 10 | 
            +
            import ipaddress
         | 
| 11 | 
            +
            import asyncio
         | 
| 12 | 
            +
            import dateutil.parser
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            from datetime import datetime, timezone as UTC
         | 
| 15 | 
            +
            from tools.storage import Storage
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            storage = Storage()
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            # ---------------------------
         | 
| 20 | 
            +
            # Конфигурация (будем пересчитывать после bootstrap)
         | 
| 21 | 
            +
            # ---------------------------
         | 
| 22 | 
            +
            my_id = storage.get_config_value("agent_id")
         | 
| 23 | 
            +
            agent_name = storage.get_config_value("agent_name", "unknown")
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            # placeholders — реальные значения пересчитаются в start_sync()
         | 
| 26 | 
            +
            local_addresses = []
         | 
| 27 | 
            +
            global_addresses = []
         | 
| 28 | 
            +
            all_addresses = []
         | 
| 29 | 
            +
            local_ports = []
         | 
| 30 | 
            +
            print(f"[PeerSync] (init) my_id={my_id} name={agent_name}")
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            # ---------------------------
         | 
| 33 | 
            +
            # Загрузка bootstrap
         | 
| 34 | 
            +
            # ---------------------------
         | 
| 35 | 
            +
            def load_bootstrap_peers(filename="bootstrap.txt"):
         | 
| 36 | 
            +
                try:
         | 
| 37 | 
            +
                    with open(filename, "r", encoding="utf-8") as f:
         | 
| 38 | 
            +
                        lines = f.readlines()
         | 
| 39 | 
            +
                except FileNotFoundError:
         | 
| 40 | 
            +
                    print(f"[Bootstrap] File {filename} not found")
         | 
| 41 | 
            +
                    return
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                for line in lines:
         | 
| 44 | 
            +
                    line = line.strip()
         | 
| 45 | 
            +
                    if not line or line.startswith("#"):
         | 
| 46 | 
            +
                        continue
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    # Разделяем строку на ключ:значение по ";"
         | 
| 49 | 
            +
                    parts = [p.strip() for p in line.split(";") if p.strip()]
         | 
| 50 | 
            +
                    data = {}
         | 
| 51 | 
            +
                    for part in parts:
         | 
| 52 | 
            +
                        if ":" not in part:
         | 
| 53 | 
            +
                            continue
         | 
| 54 | 
            +
                        key, val = part.split(":", 1)
         | 
| 55 | 
            +
                        key = key.strip().upper()
         | 
| 56 | 
            +
                        val = val.strip()
         | 
| 57 | 
            +
                        if val.startswith('"') and val.endswith('"'):
         | 
| 58 | 
            +
                            val = val[1:-1].replace("\\n", "\n")
         | 
| 59 | 
            +
                        data[key] = val
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    # Проверка обязательных полей
         | 
| 62 | 
            +
                    did = data.get("DID")
         | 
| 63 | 
            +
                    pubkey = data.get("KEY")
         | 
| 64 | 
            +
                    addresses_json = data.get("ADDRESS")
         | 
| 65 | 
            +
                    name = data.get("NAME")
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    if not did or not pubkey or not addresses_json:
         | 
| 68 | 
            +
                        print(f"[Bootstrap] Missing required fields in line: {line}")
         | 
| 69 | 
            +
                        continue
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    # Парсим адреса
         | 
| 72 | 
            +
                    try:
         | 
| 73 | 
            +
                        addresses = json.loads(addresses_json)
         | 
| 74 | 
            +
                    except Exception as e:
         | 
| 75 | 
            +
                        print(f"[Bootstrap] Failed to parse JSON addresses: {line} ({e})")
         | 
| 76 | 
            +
                        continue
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    # Расширяем any:// в tcp/udp и приводим к формату адресов
         | 
| 79 | 
            +
                    expanded_addresses = []
         | 
| 80 | 
            +
                    for addr in addresses:
         | 
| 81 | 
            +
                        if isinstance(addr, dict):
         | 
| 82 | 
            +
                            # старый формат с address/pow → конвертим
         | 
| 83 | 
            +
                            if "address" in addr:
         | 
| 84 | 
            +
                                addr_str = addr["address"]
         | 
| 85 | 
            +
                                if addr_str.startswith("any://"):
         | 
| 86 | 
            +
                                    hostport = addr_str[len("any://"):]
         | 
| 87 | 
            +
                                    variants = [f"tcp://{hostport}", f"udp://{hostport}"]
         | 
| 88 | 
            +
                                else:
         | 
| 89 | 
            +
                                    variants = [addr_str]
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                                for v in variants:
         | 
| 92 | 
            +
                                    expanded_addresses.append({
         | 
| 93 | 
            +
                                        "addr": v,
         | 
| 94 | 
            +
                                        "nonce": addr.get("pow", {}).get("nonce"),
         | 
| 95 | 
            +
                                        "pow_hash": addr.get("pow", {}).get("hash"),
         | 
| 96 | 
            +
                                        "difficulty": addr.get("pow", {}).get("difficulty"),
         | 
| 97 | 
            +
                                        "datetime": addr.get("datetime", "")
         | 
| 98 | 
            +
                                    })
         | 
| 99 | 
            +
                            # уже новый формат → оставляем как есть
         | 
| 100 | 
            +
                            elif "addr" in addr:
         | 
| 101 | 
            +
                                expanded_addresses.append(addr)
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                        elif isinstance(addr, str):
         | 
| 104 | 
            +
                            if addr.startswith("any://"):
         | 
| 105 | 
            +
                                hostport = addr[len("any://"):]
         | 
| 106 | 
            +
                                expanded_addresses.extend([
         | 
| 107 | 
            +
                                    {"addr": f"tcp://{hostport}", "nonce": None, "pow_hash": None, "difficulty": None, "datetime": ""},
         | 
| 108 | 
            +
                                    {"addr": f"udp://{hostport}", "nonce": None, "pow_hash": None, "difficulty": None, "datetime": ""}
         | 
| 109 | 
            +
                                ])
         | 
| 110 | 
            +
                            else:
         | 
| 111 | 
            +
                                expanded_addresses.append({
         | 
| 112 | 
            +
                                    "addr": addr,
         | 
| 113 | 
            +
                                    "nonce": None,
         | 
| 114 | 
            +
                                    "pow_hash": None,
         | 
| 115 | 
            +
                                    "difficulty": None,
         | 
| 116 | 
            +
                                    "datetime": ""
         | 
| 117 | 
            +
                                })
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    # Сохраняем в storage
         | 
| 120 | 
            +
                    print(f"[DEBUG] Saving peer {did} with addresses:")
         | 
| 121 | 
            +
                    for a in expanded_addresses:
         | 
| 122 | 
            +
                        print(a)
         | 
| 123 | 
            +
                    storage.add_or_update_peer(
         | 
| 124 | 
            +
                        peer_id=did,
         | 
| 125 | 
            +
                        name=name,
         | 
| 126 | 
            +
                        addresses=expanded_addresses,
         | 
| 127 | 
            +
                        source="bootstrap",
         | 
| 128 | 
            +
                        status="offline",
         | 
| 129 | 
            +
                        pubkey=pubkey,
         | 
| 130 | 
            +
                        capabilities=None,
         | 
| 131 | 
            +
                        heard_from=None
         | 
| 132 | 
            +
                    )
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                    print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
         | 
| 135 | 
            +
             | 
| 136 | 
            +
            # ---------------------------
         | 
| 137 | 
            +
            # UDP Discovery
         | 
| 138 | 
            +
            # ---------------------------
         | 
| 139 | 
            +
            def udp_discovery():
         | 
| 140 | 
            +
                import dateutil.parser  # для парсинга ISO datetime
         | 
| 141 | 
            +
                DISCOVERY_INTERVAL = 30
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                try:
         | 
| 144 | 
            +
                    # --- Создаём слушающие сокеты один раз (на основе текущих локальных адресов в storage) ---
         | 
| 145 | 
            +
                    listen_sockets = []
         | 
| 146 | 
            +
                    cfg_local_addresses = storage.get_config_value("local_addresses", [])
         | 
| 147 | 
            +
                    print(f"[UDP Discovery] Local addresses (init): {cfg_local_addresses}")
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                    for entry in cfg_local_addresses:
         | 
| 150 | 
            +
                        addr_str = entry.get("addr") if isinstance(entry, dict) else entry
         | 
| 151 | 
            +
                        if not addr_str:
         | 
| 152 | 
            +
                            continue
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                        proto, hostport = addr_str.split("://", 1)
         | 
| 155 | 
            +
                        host, port = storage.parse_hostport(hostport)
         | 
| 156 | 
            +
                        if not port or proto.lower() != "udp":
         | 
| 157 | 
            +
                            continue
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                        # IPv4
         | 
| 160 | 
            +
                        if not host.startswith("["):
         | 
| 161 | 
            +
                            try:
         | 
| 162 | 
            +
                                sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         | 
| 163 | 
            +
                                sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 164 | 
            +
                                sock4.bind(("", port))
         | 
| 165 | 
            +
                                listen_sockets.append(sock4)
         | 
| 166 | 
            +
                                print(f"[UDP Discovery] Listening IPv4 on *:{port}")
         | 
| 167 | 
            +
                            except Exception as e:
         | 
| 168 | 
            +
                                print(f"[UDP Discovery] IPv4 bind failed on port {port}: {e}")
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                        # IPv6
         | 
| 171 | 
            +
                        else:
         | 
| 172 | 
            +
                            try:
         | 
| 173 | 
            +
                                sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
         | 
| 174 | 
            +
                                sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 175 | 
            +
                                sock6.bind(("::", port))
         | 
| 176 | 
            +
                                listen_sockets.append(sock6)
         | 
| 177 | 
            +
                                print(f"[UDP Discovery] Listening IPv6 on [::]:{port}")
         | 
| 178 | 
            +
                            except Exception as e:
         | 
| 179 | 
            +
                                print(f"[UDP Discovery] IPv6 bind failed on port {port}: {e}")
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                except Exception as init_e:
         | 
| 182 | 
            +
                    print(f"[UDP Discovery] init error: {init_e}")
         | 
| 183 | 
            +
                    return
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                # --- Основной цикл ---
         | 
| 186 | 
            +
                while True:
         | 
| 187 | 
            +
                    try:
         | 
| 188 | 
            +
                        agent_pubkey = storage.get_config_value("agent_pubkey")
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                        # Приём входящих пакетов
         | 
| 191 | 
            +
                        if listen_sockets:
         | 
| 192 | 
            +
                            rlist, _, _ = select.select(listen_sockets, [], [], 0.5)
         | 
| 193 | 
            +
                            for sock in rlist:
         | 
| 194 | 
            +
                                try:
         | 
| 195 | 
            +
                                    data, addr = sock.recvfrom(2048)
         | 
| 196 | 
            +
                                    print(f"[UDP Discovery] RAW from {addr}: {data!r}")
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                                    try:
         | 
| 199 | 
            +
                                        msg = json.loads(data.decode("utf-8"))
         | 
| 200 | 
            +
                                    except Exception as e:
         | 
| 201 | 
            +
                                        print(f"[UDP Discovery] JSON decode error from {addr}: {e}")
         | 
| 202 | 
            +
                                        continue
         | 
| 203 | 
            +
             | 
| 204 | 
            +
                                    peer_id = msg.get("id")
         | 
| 205 | 
            +
                                    if peer_id == my_id:
         | 
| 206 | 
            +
                                        continue
         | 
| 207 | 
            +
                                    name = msg.get("name", "unknown")
         | 
| 208 | 
            +
                                    addresses = msg.get("addresses", []) or []
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                                    valid_addresses = []
         | 
| 211 | 
            +
                                    for a in addresses:
         | 
| 212 | 
            +
                                        addr_str = a.get("addr")
         | 
| 213 | 
            +
                                        nonce = a.get("nonce")
         | 
| 214 | 
            +
                                        pow_hash = a.get("pow_hash")
         | 
| 215 | 
            +
                                        difficulty = a.get("difficulty")
         | 
| 216 | 
            +
                                        dt = a.get("datetime")
         | 
| 217 | 
            +
                                        pubkey = a.get("pubkey")
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                                        if not addr_str:
         | 
| 220 | 
            +
                                            continue
         | 
| 221 | 
            +
             | 
| 222 | 
            +
                                        # нормализуем адрес (везде используем единый формат)
         | 
| 223 | 
            +
                                        addr_norm = storage.normalize_address(addr_str)
         | 
| 224 | 
            +
                                        if not addr_norm:
         | 
| 225 | 
            +
                                            print(f"[UDP Discovery] Can't normalize addr {addr_str}, skip")
         | 
| 226 | 
            +
                                            continue
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                                        # Проверка PoW — только если есть все поля
         | 
| 229 | 
            +
                                        if nonce is not None and pow_hash and difficulty is not None:
         | 
| 230 | 
            +
                                            if not pubkey:
         | 
| 231 | 
            +
                                                print(f"[UDP Discovery] Peer {peer_id} addr {addr_norm} missing pubkey, skip PoW check")
         | 
| 232 | 
            +
                                                continue
         | 
| 233 | 
            +
                                            ok = storage.verify_pow(peer_id, pubkey, addr_norm, nonce, pow_hash, dt, difficulty)
         | 
| 234 | 
            +
                                            print(f"[UDP Discovery] Verify PoW for {addr_norm} = {ok}")
         | 
| 235 | 
            +
                                            if not ok:
         | 
| 236 | 
            +
                                                continue
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                                        # Проверка datetime
         | 
| 239 | 
            +
                                        existing = storage.get_peer_address(peer_id, addr_norm)
         | 
| 240 | 
            +
                                        try:
         | 
| 241 | 
            +
                                            existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
         | 
| 242 | 
            +
                                            dt_obj = dateutil.parser.isoparse(dt) if dt else None
         | 
| 243 | 
            +
                                        except Exception as e:
         | 
| 244 | 
            +
                                            print(f"[UDP Discovery] datetime parse error: {e}")
         | 
| 245 | 
            +
                                            continue
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                                        if existing_dt and dt_obj and existing_dt >= dt_obj:
         | 
| 248 | 
            +
                                            print(f"[UDP Discovery] Skip {addr_norm}: old datetime {dt}")
         | 
| 249 | 
            +
                                            continue
         | 
| 250 | 
            +
             | 
| 251 | 
            +
                                        # if all checks OK, ensure we keep canonical form in the stored dict
         | 
| 252 | 
            +
                                        a_copy = dict(a)
         | 
| 253 | 
            +
                                        a_copy["addr"] = addr_norm
         | 
| 254 | 
            +
                                        valid_addresses.append(a_copy)
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                                    if valid_addresses:
         | 
| 257 | 
            +
                                        storage.add_or_update_peer(
         | 
| 258 | 
            +
                                            peer_id=peer_id,
         | 
| 259 | 
            +
                                            name=name,
         | 
| 260 | 
            +
                                            addresses=valid_addresses,
         | 
| 261 | 
            +
                                            source="discovery",
         | 
| 262 | 
            +
                                            status="online"
         | 
| 263 | 
            +
                                        )
         | 
| 264 | 
            +
                                        print(f"[UDP Discovery] Accepted peer {peer_id} ({addr}), {len(valid_addresses)} addresses")
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                                except Exception as e:
         | 
| 267 | 
            +
                                    print(f"[UDP Discovery] receive error: {e}")
         | 
| 268 | 
            +
             | 
| 269 | 
            +
                        # --- Отправка broadcast/multicast ---
         | 
| 270 | 
            +
                        # берем текущие локальные адреса из storage (актуально)
         | 
| 271 | 
            +
                        cfg_local_addresses = storage.get_config_value("local_addresses", [])
         | 
| 272 | 
            +
                        valid_local_addresses = []
         | 
| 273 | 
            +
             | 
| 274 | 
            +
                        for a in cfg_local_addresses:
         | 
| 275 | 
            +
                            addr_str = a.get("addr") if isinstance(a, dict) else a
         | 
| 276 | 
            +
                            nonce = a.get("nonce") if isinstance(a, dict) else None
         | 
| 277 | 
            +
                            pow_hash = a.get("pow_hash") if isinstance(a, dict) else None
         | 
| 278 | 
            +
                            difficulty = a.get("difficulty") if isinstance(a, dict) else None
         | 
| 279 | 
            +
                            dt = a.get("datetime") if isinstance(a, dict) else None
         | 
| 280 | 
            +
                            # prefer explicit pubkey per-address, otherwise agent_pubkey
         | 
| 281 | 
            +
                            addr_pubkey = a.get("pubkey") if isinstance(a, dict) else None
         | 
| 282 | 
            +
                            pubkey_used = addr_pubkey or agent_pubkey
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                            if not addr_str:
         | 
| 285 | 
            +
                                continue
         | 
| 286 | 
            +
             | 
| 287 | 
            +
                            addr_norm = storage.normalize_address(addr_str)
         | 
| 288 | 
            +
                            if not addr_norm:
         | 
| 289 | 
            +
                                print(f"[UDP Discovery] Can't normalize local addr {addr_str}, skip")
         | 
| 290 | 
            +
                                continue
         | 
| 291 | 
            +
             | 
| 292 | 
            +
                            # Проверка PoW только если есть необходимые поля
         | 
| 293 | 
            +
                            if nonce is not None and pow_hash and difficulty is not None:
         | 
| 294 | 
            +
                                if not pubkey_used:
         | 
| 295 | 
            +
                                    # если у агента нет pubkey в конфигах, не делаем жёсткую проверку PoW,
         | 
| 296 | 
            +
                                    # потому что невозможно подтвердить — логируем и пропускаем проверку
         | 
| 297 | 
            +
                                    print(f"[UDP Discovery] No pubkey for self addr {addr_norm}, skipping PoW self-check (will broadcast anyway)")
         | 
| 298 | 
            +
                                    ok = True
         | 
| 299 | 
            +
                                else:
         | 
| 300 | 
            +
                                    ok = storage.verify_pow(my_id, pubkey_used, addr_norm, nonce, pow_hash, dt, difficulty)
         | 
| 301 | 
            +
                                    print(f"[UDP Discovery] Self-check PoW for {addr_norm} = {ok}")
         | 
| 302 | 
            +
                                if not ok:
         | 
| 303 | 
            +
                                    print(f"[UDP Discovery] Self addr {addr_norm} failed PoW, skip")
         | 
| 304 | 
            +
                                    continue
         | 
| 305 | 
            +
             | 
| 306 | 
            +
                            # attach pubkey for broadcast so receivers can verify
         | 
| 307 | 
            +
                            a_copy = dict(a) if isinstance(a, dict) else {"addr": addr_str}
         | 
| 308 | 
            +
                            a_copy["addr"] = addr_norm
         | 
| 309 | 
            +
                            if "pubkey" not in a_copy and agent_pubkey:
         | 
| 310 | 
            +
                                a_copy["pubkey"] = agent_pubkey
         | 
| 311 | 
            +
                            valid_local_addresses.append(a_copy)
         | 
| 312 | 
            +
             | 
| 313 | 
            +
                        msg_data = json.dumps({
         | 
| 314 | 
            +
                            "id": my_id,
         | 
| 315 | 
            +
                            "name": agent_name,
         | 
| 316 | 
            +
                            "addresses": valid_local_addresses
         | 
| 317 | 
            +
                        }).encode("utf-8")
         | 
| 318 | 
            +
             | 
| 319 | 
            +
                        print(f"[UDP Discovery] Broadcasting: {msg_data}")
         | 
| 320 | 
            +
             | 
| 321 | 
            +
                        for entry in valid_local_addresses:
         | 
| 322 | 
            +
                            addr_str = entry.get("addr")
         | 
| 323 | 
            +
                            proto, hostport = addr_str.split("://", 1)
         | 
| 324 | 
            +
                            host, port = storage.parse_hostport(hostport)
         | 
| 325 | 
            +
                            if not port or proto.lower() != "udp":
         | 
| 326 | 
            +
                                continue
         | 
| 327 | 
            +
             | 
| 328 | 
            +
                            # IPv4 broadcast
         | 
| 329 | 
            +
                            if not host.startswith("["):
         | 
| 330 | 
            +
                                for iface in netifaces.interfaces():
         | 
| 331 | 
            +
                                    addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
         | 
| 332 | 
            +
                                    for a in addrs:
         | 
| 333 | 
            +
                                        if "broadcast" in a:
         | 
| 334 | 
            +
                                            try:
         | 
| 335 | 
            +
                                                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         | 
| 336 | 
            +
                                                sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
         | 
| 337 | 
            +
                                                print(f"[UDP Discovery] Sending broadcast -> {a['broadcast']}:{port}")
         | 
| 338 | 
            +
                                                sock.sendto(msg_data, (a["broadcast"], port))
         | 
| 339 | 
            +
                                                sock.close()
         | 
| 340 | 
            +
                                            except Exception as e:
         | 
| 341 | 
            +
                                                print(f"[UDP Discovery] Broadcast error {a['broadcast']}:{port}: {e}")
         | 
| 342 | 
            +
             | 
| 343 | 
            +
                            # IPv6 multicast ff02::1
         | 
| 344 | 
            +
                            else:
         | 
| 345 | 
            +
                                for iface in netifaces.interfaces():
         | 
| 346 | 
            +
                                    ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
         | 
| 347 | 
            +
                                    for a in ifaddrs:
         | 
| 348 | 
            +
                                        addr_ipv6 = a.get("addr")
         | 
| 349 | 
            +
                                        if not addr_ipv6:
         | 
| 350 | 
            +
                                            continue
         | 
| 351 | 
            +
                                        multicast_addr = f"ff02::1%{iface}" if addr_ipv6.startswith("fe80:") else "ff02::1"
         | 
| 352 | 
            +
                                        try:
         | 
| 353 | 
            +
                                            sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
         | 
| 354 | 
            +
                                            sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
         | 
| 355 | 
            +
                                            sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
         | 
| 356 | 
            +
                                            print(f"[UDP Discovery] Sending multicast -> {multicast_addr}:{port}")
         | 
| 357 | 
            +
                                            sock6.sendto(msg_data, (multicast_addr, port))
         | 
| 358 | 
            +
                                            sock6.close()
         | 
| 359 | 
            +
                                        except Exception as e:
         | 
| 360 | 
            +
                                            print(f"[UDP Discovery] Multicast error {multicast_addr}:{port}: {e}")
         | 
| 361 | 
            +
             | 
| 362 | 
            +
                        time.sleep(DISCOVERY_INTERVAL)
         | 
| 363 | 
            +
             | 
| 364 | 
            +
                    except Exception as main_e:
         | 
| 365 | 
            +
                        print(f"[UDP Discovery] main loop error: {main_e}")
         | 
| 366 | 
            +
                        time.sleep(DISCOVERY_INTERVAL)
         | 
| 367 | 
            +
             | 
| 368 | 
            +
            # ---------------------------
         | 
| 369 | 
            +
            # TCP Peer Exchange (исходящие)
         | 
| 370 | 
            +
            # ---------------------------
         | 
| 371 | 
            +
            def tcp_peer_exchange():
         | 
| 372 | 
            +
                import dateutil.parser  # для корректного парсинга ISO datetime
         | 
| 373 | 
            +
                PEER_EXCHANGE_INTERVAL = 20  # секунды для отладки
         | 
| 374 | 
            +
             | 
| 375 | 
            +
                while True:
         | 
| 376 | 
            +
                    # получаем свежий список пиров из БД
         | 
| 377 | 
            +
                    peers = storage.get_known_peers(my_id, limit=50)
         | 
| 378 | 
            +
                    print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
         | 
| 379 | 
            +
             | 
| 380 | 
            +
                    for peer in peers:
         | 
| 381 | 
            +
                        peer_id = peer["id"] if isinstance(peer, dict) else peer[0]
         | 
| 382 | 
            +
                        addresses_json = peer["addresses"] if isinstance(peer, dict) else peer[1]
         | 
| 383 | 
            +
             | 
| 384 | 
            +
                        if peer_id == my_id:
         | 
| 385 | 
            +
                            continue
         | 
| 386 | 
            +
             | 
| 387 | 
            +
                        try:
         | 
| 388 | 
            +
                            addr_list = json.loads(addresses_json)
         | 
| 389 | 
            +
                        except Exception as e:
         | 
| 390 | 
            +
                            print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
         | 
| 391 | 
            +
                            addr_list = []
         | 
| 392 | 
            +
             | 
| 393 | 
            +
                        for addr_entry in addr_list:
         | 
| 394 | 
            +
                            addr_str = addr_entry.get("addr")
         | 
| 395 | 
            +
                            nonce = addr_entry.get("nonce")
         | 
| 396 | 
            +
                            pow_hash = addr_entry.get("pow_hash")
         | 
| 397 | 
            +
                            difficulty = addr_entry.get("difficulty")
         | 
| 398 | 
            +
                            dt = addr_entry.get("datetime")
         | 
| 399 | 
            +
                            pubkey = addr_entry.get("pubkey")
         | 
| 400 | 
            +
             | 
| 401 | 
            +
                            # нормализация адреса (включая скобки IPv6 и т.п.)
         | 
| 402 | 
            +
                            norm = storage.normalize_address(addr_str)
         | 
| 403 | 
            +
                            if not norm:
         | 
| 404 | 
            +
                                continue
         | 
| 405 | 
            +
             | 
| 406 | 
            +
                            # Проверка PoW
         | 
| 407 | 
            +
                            if nonce is not None and pow_hash and difficulty is not None:
         | 
| 408 | 
            +
                                if not pubkey:
         | 
| 409 | 
            +
                                    print(f"[PeerExchange] Peer {peer_id} addr {norm} missing pubkey, skip PoW check")
         | 
| 410 | 
            +
                                    continue
         | 
| 411 | 
            +
                                ok = storage.verify_pow(peer_id, pubkey, norm, nonce, pow_hash, dt, difficulty)
         | 
| 412 | 
            +
                                print(f"[PeerExchange] Verify PoW for {peer_id}@{norm} = {ok}")
         | 
| 413 | 
            +
                                if not ok:
         | 
| 414 | 
            +
                                    continue
         | 
| 415 | 
            +
             | 
| 416 | 
            +
                            # Проверка datetime с использованием dateutil
         | 
| 417 | 
            +
                            existing = storage.get_peer_address(peer_id, norm)
         | 
| 418 | 
            +
                            try:
         | 
| 419 | 
            +
                                existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
         | 
| 420 | 
            +
                                dt_obj = dateutil.parser.isoparse(dt) if dt else None
         | 
| 421 | 
            +
                            except Exception as e:
         | 
| 422 | 
            +
                                print(f"[PeerExchange] datetime parse error for {norm}: {e}")
         | 
| 423 | 
            +
                                continue
         | 
| 424 | 
            +
             | 
| 425 | 
            +
                            if existing_dt and dt_obj and existing_dt >= dt_obj:
         | 
| 426 | 
            +
                                print(f"[PeerExchange] Skip {norm}: old datetime {dt}")
         | 
| 427 | 
            +
                                continue
         | 
| 428 | 
            +
             | 
| 429 | 
            +
                            # Парсим host и port
         | 
| 430 | 
            +
                            proto, hostport = norm.split("://", 1)
         | 
| 431 | 
            +
                            if proto not in ["tcp", "any"]:
         | 
| 432 | 
            +
                                continue
         | 
| 433 | 
            +
                            host, port = storage.parse_hostport(hostport)
         | 
| 434 | 
            +
                            if not host or not port:
         | 
| 435 | 
            +
                                continue
         | 
| 436 | 
            +
             | 
| 437 | 
            +
                            print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
         | 
| 438 | 
            +
             | 
| 439 | 
            +
                            try:
         | 
| 440 | 
            +
                                # IPv6 link-local
         | 
| 441 | 
            +
                                if storage.is_ipv6(host) and host.startswith("fe80:"):
         | 
| 442 | 
            +
                                    scope_id = storage.get_ipv6_scope(host)
         | 
| 443 | 
            +
                                    if scope_id is None:
         | 
| 444 | 
            +
                                        print(f"[PeerExchange] Skipping {host}, no scope_id")
         | 
| 445 | 
            +
                                        continue
         | 
| 446 | 
            +
                                    sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
         | 
| 447 | 
            +
                                    sock.settimeout(3)
         | 
| 448 | 
            +
                                    sock.connect((host, port, 0, scope_id))
         | 
| 449 | 
            +
                                else:
         | 
| 450 | 
            +
                                    sock = socket.socket(socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET,
         | 
| 451 | 
            +
                                                         socket.SOCK_STREAM)
         | 
| 452 | 
            +
                                    sock.settimeout(3)
         | 
| 453 | 
            +
                                    sock.connect((host, port))
         | 
| 454 | 
            +
             | 
| 455 | 
            +
                                # Получаем актуальные адреса на момент отправки (не использовать stale all_addresses)
         | 
| 456 | 
            +
                                cfg_local_addresses = storage.get_config_value("local_addresses", [])
         | 
| 457 | 
            +
                                cfg_global_addresses = storage.get_config_value("global_addresses", [])
         | 
| 458 | 
            +
                                cur_all_addresses = cfg_local_addresses + cfg_global_addresses
         | 
| 459 | 
            +
             | 
| 460 | 
            +
                                # Отправка handshake — фильтр адресов для public/private
         | 
| 461 | 
            +
                                if storage.is_private(host):
         | 
| 462 | 
            +
                                    send_addresses = cur_all_addresses
         | 
| 463 | 
            +
                                else:
         | 
| 464 | 
            +
                                    # фильтруем только публичные
         | 
| 465 | 
            +
                                    send_addresses = []
         | 
| 466 | 
            +
                                    for a in cur_all_addresses:
         | 
| 467 | 
            +
                                        a_addr = a.get("addr") if isinstance(a, dict) else a
         | 
| 468 | 
            +
                                        try:
         | 
| 469 | 
            +
                                            host_only = storage.parse_hostport(a_addr.split("://", 1)[1])[0]
         | 
| 470 | 
            +
                                            if is_public(host_only):
         | 
| 471 | 
            +
                                                send_addresses.append(a)
         | 
| 472 | 
            +
                                        except Exception:
         | 
| 473 | 
            +
                                            continue
         | 
| 474 | 
            +
             | 
| 475 | 
            +
                                handshake = {
         | 
| 476 | 
            +
                                    "type": "PEER_EXCHANGE_REQUEST",
         | 
| 477 | 
            +
                                    "id": my_id,
         | 
| 478 | 
            +
                                    "name": agent_name,
         | 
| 479 | 
            +
                                    "addresses": send_addresses,
         | 
| 480 | 
            +
                                }
         | 
| 481 | 
            +
                                raw_handshake = json.dumps(handshake).encode("utf-8")
         | 
| 482 | 
            +
                                print(f"[PeerExchange] Sending handshake -> {host}:{port}: {raw_handshake}")
         | 
| 483 | 
            +
                                sock.sendall(raw_handshake)
         | 
| 484 | 
            +
             | 
| 485 | 
            +
                                # Читаем ответ
         | 
| 486 | 
            +
                                data = sock.recv(64 * 1024)
         | 
| 487 | 
            +
                                sock.close()
         | 
| 488 | 
            +
             | 
| 489 | 
            +
                                if not data:
         | 
| 490 | 
            +
                                    print(f"[PeerExchange] No data from {host}:{port}")
         | 
| 491 | 
            +
                                    continue
         | 
| 492 | 
            +
             | 
| 493 | 
            +
                                print(f"[PeerExchange] RAW recv from {host}:{port}: {data!r}")
         | 
| 494 | 
            +
             | 
| 495 | 
            +
                                try:
         | 
| 496 | 
            +
                                    peers_recv = json.loads(data.decode("utf-8"))
         | 
| 497 | 
            +
                                    print(f"[PeerExchange] Parsed recv from {host}:{port}: {peers_recv}")
         | 
| 498 | 
            +
                                    for p in peers_recv:
         | 
| 499 | 
            +
                                        new_addrs = []
         | 
| 500 | 
            +
                                        for a in p.get("addresses", []):
         | 
| 501 | 
            +
                                            try:
         | 
| 502 | 
            +
                                                existing_addr = storage.get_peer_address(p["id"], a.get("addr"))
         | 
| 503 | 
            +
                                                existing_dt = dateutil.parser.isoparse(existing_addr.get("datetime")) if existing_addr else None
         | 
| 504 | 
            +
                                                dt_obj = dateutil.parser.isoparse(a.get("datetime")) if a.get("datetime") else None
         | 
| 505 | 
            +
                                                if existing_addr is None or (existing_dt and dt_obj and existing_dt < dt_obj) or existing_dt is None:
         | 
| 506 | 
            +
                                                    new_addrs.append(a)
         | 
| 507 | 
            +
                                                else:
         | 
| 508 | 
            +
                                                    print(f"[PeerExchange] Ignored old {a.get('addr')} from {p['id']}")
         | 
| 509 | 
            +
                                            except Exception as e:
         | 
| 510 | 
            +
                                                print(f"[PeerExchange] Error parsing datetime for {a.get('addr')}: {e}")
         | 
| 511 | 
            +
                                                continue
         | 
| 512 | 
            +
             | 
| 513 | 
            +
                                        if new_addrs:
         | 
| 514 | 
            +
                                            storage.add_or_update_peer(
         | 
| 515 | 
            +
                                                p["id"],
         | 
| 516 | 
            +
                                                p.get("name", "unknown"),
         | 
| 517 | 
            +
                                                new_addrs,
         | 
| 518 | 
            +
                                                source="peer_exchange",
         | 
| 519 | 
            +
                                                status="online"
         | 
| 520 | 
            +
                                            )
         | 
| 521 | 
            +
                                            print(f"[PeerExchange] Stored {len(new_addrs)} new addrs for peer {p['id']}")
         | 
| 522 | 
            +
                                    print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}")
         | 
| 523 | 
            +
                                except Exception as e:
         | 
| 524 | 
            +
                                    print(f"[PeerExchange] Decode error from {host}:{port}: {e}")
         | 
| 525 | 
            +
                                    continue
         | 
| 526 | 
            +
             | 
| 527 | 
            +
                                break
         | 
| 528 | 
            +
                            except Exception as e:
         | 
| 529 | 
            +
                                print(f"[PeerExchange] Connection to {host}:{port} failed: {e}")
         | 
| 530 | 
            +
                                continue
         | 
| 531 | 
            +
             | 
| 532 | 
            +
                    time.sleep(PEER_EXCHANGE_INTERVAL)
         | 
| 533 | 
            +
             | 
| 534 | 
            +
            # ---------------------------
         | 
| 535 | 
            +
            # TCP Listener (входящие)
         | 
| 536 | 
            +
            # ---------------------------
         | 
| 537 | 
            +
            def tcp_listener():
         | 
| 538 | 
            +
                # получаем локальные порты в момент старта listener'а
         | 
| 539 | 
            +
                listen_sockets = []
         | 
| 540 | 
            +
                cfg_local_ports = storage.get_local_ports()
         | 
| 541 | 
            +
                print(f"[TCP Listener] binding to local ports: {cfg_local_ports}")
         | 
| 542 | 
            +
                for port in cfg_local_ports:
         | 
| 543 | 
            +
                    for family, addr_str in [(socket.AF_INET, ""), (socket.AF_INET6, "::")]:
         | 
| 544 | 
            +
                        try:
         | 
| 545 | 
            +
                            sock = socket.socket(family, socket.SOCK_STREAM)
         | 
| 546 | 
            +
                            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 547 | 
            +
                            sock.bind((addr_str, port))
         | 
| 548 | 
            +
                            sock.listen(5)
         | 
| 549 | 
            +
                            listen_sockets.append(sock)
         | 
| 550 | 
            +
                            proto_str = "IPv6" if family == socket.AF_INET6 else "IPv4"
         | 
| 551 | 
            +
                            print(f"[TCP Listener] Listening {proto_str} on {addr_str}:{port}")
         | 
| 552 | 
            +
                        except Exception as e:
         | 
| 553 | 
            +
                            print(f"[TCP Listener] {proto_str} bind failed on port {port}: {e}")
         | 
| 554 | 
            +
             | 
| 555 | 
            +
                while True:
         | 
| 556 | 
            +
                    if not listen_sockets:
         | 
| 557 | 
            +
                        time.sleep(1)
         | 
| 558 | 
            +
                        continue
         | 
| 559 | 
            +
             | 
| 560 | 
            +
                    rlist, _, _ = select.select(listen_sockets, [], [], 1)
         | 
| 561 | 
            +
                    for s in rlist:
         | 
| 562 | 
            +
                        try:
         | 
| 563 | 
            +
                            conn, addr = s.accept()
         | 
| 564 | 
            +
                            data = conn.recv(64 * 1024)
         | 
| 565 | 
            +
                            if not data:
         | 
| 566 | 
            +
                                print(f"[TCP Listener] Empty data from {addr}, closing")
         | 
| 567 | 
            +
                                conn.close()
         | 
| 568 | 
            +
                                continue
         | 
| 569 | 
            +
             | 
| 570 | 
            +
                            print(f"[TCP Listener] RAW recv from {addr}: {data!r}")
         | 
| 571 | 
            +
             | 
| 572 | 
            +
                            try:
         | 
| 573 | 
            +
                                msg = json.loads(data.decode("utf-8"))
         | 
| 574 | 
            +
                                print(f"[TCP Listener] Decoded JSON from {addr}: {msg}")
         | 
| 575 | 
            +
                            except Exception as e:
         | 
| 576 | 
            +
                                print(f"[TCP Listener] JSON decode error from {addr}: {e}")
         | 
| 577 | 
            +
                                conn.close()
         | 
| 578 | 
            +
                                continue
         | 
| 579 | 
            +
             | 
| 580 | 
            +
                            if msg.get("type") == "PEER_EXCHANGE_REQUEST":
         | 
| 581 | 
            +
                                peer_id = msg.get("id") or f"did:hmp:{addr[0]}:{addr[1]}"
         | 
| 582 | 
            +
                                peer_name = msg.get("name", "unknown")
         | 
| 583 | 
            +
                                peer_addrs = msg.get("addresses", []) or []
         | 
| 584 | 
            +
             | 
| 585 | 
            +
                                valid_addrs = []
         | 
| 586 | 
            +
                                for a in peer_addrs:
         | 
| 587 | 
            +
                                    addr_value = a.get("addr")
         | 
| 588 | 
            +
                                    nonce = a.get("nonce")
         | 
| 589 | 
            +
                                    pow_hash = a.get("pow_hash")
         | 
| 590 | 
            +
                                    difficulty = a.get("difficulty")
         | 
| 591 | 
            +
                                    dt = a.get("datetime")
         | 
| 592 | 
            +
                                    pubkey = a.get("pubkey")
         | 
| 593 | 
            +
             | 
| 594 | 
            +
                                    if not addr_value:
         | 
| 595 | 
            +
                                        print(f"[TCP Listener] Skip addr (no addr field): {a}")
         | 
| 596 | 
            +
                                        continue
         | 
| 597 | 
            +
             | 
| 598 | 
            +
                                    addr_norm = storage.normalize_address(addr_value)
         | 
| 599 | 
            +
                                    if not addr_norm:
         | 
| 600 | 
            +
                                        print(f"[TCP Listener] Can't normalize {addr_value}, skip")
         | 
| 601 | 
            +
                                        continue
         | 
| 602 | 
            +
             | 
| 603 | 
            +
                                    if nonce is None or not pow_hash or not pubkey:
         | 
| 604 | 
            +
                                        print(f"[TCP Listener] Skip addr (incomplete): {a}")
         | 
| 605 | 
            +
                                        continue
         | 
| 606 | 
            +
             | 
| 607 | 
            +
                                    ok = storage.verify_pow(peer_id, pubkey, addr_norm, nonce, pow_hash, dt, difficulty)
         | 
| 608 | 
            +
                                    print(f"[TCP Listener] Verify PoW for {addr_norm} = {ok}")
         | 
| 609 | 
            +
                                    if not ok:
         | 
| 610 | 
            +
                                        continue
         | 
| 611 | 
            +
             | 
| 612 | 
            +
                                    existing = storage.get_peer_address(peer_id, addr_norm)
         | 
| 613 | 
            +
                                    try:
         | 
| 614 | 
            +
                                        existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
         | 
| 615 | 
            +
                                        dt_obj = dateutil.parser.isoparse(dt) if dt else None
         | 
| 616 | 
            +
                                    except Exception as e:
         | 
| 617 | 
            +
                                        print(f"[TCP Listener] datetime parse error for {addr_norm}: {e}")
         | 
| 618 | 
            +
                                        continue
         | 
| 619 | 
            +
             | 
| 620 | 
            +
                                    if existing_dt and dt_obj and existing_dt >= dt_obj:
         | 
| 621 | 
            +
                                        print(f"[TCP Listener] Skip old addr {addr_norm} (dt={dt})")
         | 
| 622 | 
            +
                                        continue
         | 
| 623 | 
            +
             | 
| 624 | 
            +
                                    a_copy = dict(a)
         | 
| 625 | 
            +
                                    a_copy["addr"] = addr_norm
         | 
| 626 | 
            +
                                    valid_addrs.append(a_copy)
         | 
| 627 | 
            +
             | 
| 628 | 
            +
                                if valid_addrs:
         | 
| 629 | 
            +
                                    storage.add_or_update_peer(
         | 
| 630 | 
            +
                                        peer_id=peer_id,
         | 
| 631 | 
            +
                                        name=peer_name,
         | 
| 632 | 
            +
                                        addresses=valid_addrs,
         | 
| 633 | 
            +
                                        source="incoming",
         | 
| 634 | 
            +
                                        status="online"
         | 
| 635 | 
            +
                                    )
         | 
| 636 | 
            +
                                    print(f"[TCP Listener] Stored {len(valid_addrs)} addrs for peer {peer_id}")
         | 
| 637 | 
            +
                                else:
         | 
| 638 | 
            +
                                    print(f"[TCP Listener] No valid addrs from {peer_id}")
         | 
| 639 | 
            +
             | 
| 640 | 
            +
                                print(f"[TCP Listener] Handshake from {peer_id} ({addr}) -> name={peer_name}")
         | 
| 641 | 
            +
             | 
| 642 | 
            +
                                # Готовим список пиров для ответа
         | 
| 643 | 
            +
                                is_lan = storage.is_private(addr[0])
         | 
| 644 | 
            +
                                peers_list = []
         | 
| 645 | 
            +
             | 
| 646 | 
            +
                                for peer in storage.get_known_peers(my_id, limit=50):
         | 
| 647 | 
            +
                                    peer_id_local = peer["id"]
         | 
| 648 | 
            +
                                    try:
         | 
| 649 | 
            +
                                        addresses = json.loads(peer["addresses"])
         | 
| 650 | 
            +
                                    except:
         | 
| 651 | 
            +
                                        addresses = []
         | 
| 652 | 
            +
             | 
| 653 | 
            +
                                    updated_addresses = []
         | 
| 654 | 
            +
                                    for a in addresses:
         | 
| 655 | 
            +
                                        try:
         | 
| 656 | 
            +
                                            proto, hostport = a["addr"].split("://", 1)
         | 
| 657 | 
            +
                                            host, port = storage.parse_hostport(hostport)
         | 
| 658 | 
            +
                                            if not host or not port:
         | 
| 659 | 
            +
                                                continue
         | 
| 660 | 
            +
             | 
| 661 | 
            +
                                            if not is_lan and not is_public(host):
         | 
| 662 | 
            +
                                                continue
         | 
| 663 | 
            +
             | 
| 664 | 
            +
                                            if storage.is_ipv6(host) and host.startswith("fe80:"):
         | 
| 665 | 
            +
                                                scope_id = storage.get_ipv6_scope(host)
         | 
| 666 | 
            +
                                                if scope_id:
         | 
| 667 | 
            +
                                                    host = f"{host}%{scope_id}"
         | 
| 668 | 
            +
             | 
| 669 | 
            +
                                            updated_addresses.append({
         | 
| 670 | 
            +
                                                "addr": f"{proto}://{host}:{port}"
         | 
| 671 | 
            +
                                            })
         | 
| 672 | 
            +
                                        except Exception:
         | 
| 673 | 
            +
                                            continue
         | 
| 674 | 
            +
             | 
| 675 | 
            +
                                    peers_list.append({
         | 
| 676 | 
            +
                                        "id": peer_id_local,
         | 
| 677 | 
            +
                                        "addresses": updated_addresses
         | 
| 678 | 
            +
                                    })
         | 
| 679 | 
            +
             | 
| 680 | 
            +
                                print(f"[TCP Listener] Sending {len(peers_list)} peers back to {peer_id}")
         | 
| 681 | 
            +
                                conn.sendall(json.dumps(peers_list).encode("utf-8"))
         | 
| 682 | 
            +
             | 
| 683 | 
            +
                            conn.close()
         | 
| 684 | 
            +
                        except Exception as e:
         | 
| 685 | 
            +
                            print(f"[TCP Listener] Connection handling error: {e}")
         | 
| 686 | 
            +
             | 
| 687 | 
            +
            # ---------------------------
         | 
| 688 | 
            +
            # Запуск потоков
         | 
| 689 | 
            +
            # ---------------------------
         | 
| 690 | 
            +
            def start_sync(bootstrap_file="bootstrap.txt"):
         | 
| 691 | 
            +
                # 1) загрузка bootstrap
         | 
| 692 | 
            +
                load_bootstrap_peers(bootstrap_file)
         | 
| 693 | 
            +
             | 
| 694 | 
            +
                # 2) пересчитать локальные config-derived переменные (теперь bootstrap и config загружены)
         | 
| 695 | 
            +
                global local_addresses, global_addresses, all_addresses, local_ports
         | 
| 696 | 
            +
                local_addresses = storage.get_config_value("local_addresses", [])
         | 
| 697 | 
            +
                global_addresses = storage.get_config_value("global_addresses", [])
         | 
| 698 | 
            +
                all_addresses = local_addresses + global_addresses
         | 
| 699 | 
            +
                local_ports = storage.get_local_ports()
         | 
| 700 | 
            +
                print(f"[PeerSync] Local ports (after bootstrap): {local_ports}")
         | 
| 701 | 
            +
                print(f"[PeerSync] Local addresses (after bootstrap): {local_addresses}")
         | 
| 702 | 
            +
             | 
| 703 | 
            +
                # 3) добавить себя в таблицу peers (чтобы pubkey, адреса были в БД и др. части кода могли их читать)
         | 
| 704 | 
            +
                agent_pubkey = storage.get_config_value("agent_pubkey")
         | 
| 705 | 
            +
                # подготавливаем адреса в том же формате, который add_or_update_peer ожидает
         | 
| 706 | 
            +
                self_addrs = []
         | 
| 707 | 
            +
                for a in all_addresses:
         | 
| 708 | 
            +
                    if isinstance(a, dict):
         | 
| 709 | 
            +
                        self_addrs.append(a)
         | 
| 710 | 
            +
                    else:
         | 
| 711 | 
            +
                        self_addrs.append({"addr": a, "nonce": None, "pow_hash": None, "difficulty": None, "datetime": ""})
         | 
| 712 | 
            +
             | 
| 713 | 
            +
                try:
         | 
| 714 | 
            +
                    storage.add_or_update_peer(
         | 
| 715 | 
            +
                        peer_id=my_id,
         | 
| 716 | 
            +
                        name=agent_name,
         | 
| 717 | 
            +
                        addresses=self_addrs,
         | 
| 718 | 
            +
                        source="self",
         | 
| 719 | 
            +
                        status="online",
         | 
| 720 | 
            +
                        pubkey=agent_pubkey,
         | 
| 721 | 
            +
                        capabilities=None,
         | 
| 722 | 
            +
                        heard_from=None
         | 
| 723 | 
            +
                    )
         | 
| 724 | 
            +
                    print(f"[PeerSync] Registered self {my_id} in agent_peers (pubkey present: {bool(agent_pubkey)})")
         | 
| 725 | 
            +
                except Exception as e:
         | 
| 726 | 
            +
                    print(f"[PeerSync] Failed to register self in agent_peers: {e}")
         | 
| 727 | 
            +
             | 
| 728 | 
            +
                # 4) старт потоков
         | 
| 729 | 
            +
                threading.Thread(target=udp_discovery, daemon=True).start()
         | 
| 730 | 
            +
                threading.Thread(target=tcp_peer_exchange, daemon=True).start()
         | 
| 731 | 
            +
                threading.Thread(target=tcp_listener, daemon=True).start()
         | 
    	
        agents/_not_used/peer_sync.py.old
    ADDED
    
    | @@ -0,0 +1,259 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            Давай! Вот старые версии:
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            ```
         | 
| 4 | 
            +
            def udp_discovery():
         | 
| 5 | 
            +
                DISCOVERY_INTERVAL = 30
         | 
| 6 | 
            +
                local_addresses = storage.get_config_value("local_addresses", [])
         | 
| 7 | 
            +
                msg_data = json.dumps({
         | 
| 8 | 
            +
                    "id": my_id,
         | 
| 9 | 
            +
                    "name": agent_name,
         | 
| 10 | 
            +
                    "addresses": local_addresses
         | 
| 11 | 
            +
                }).encode("utf-8")
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                # Создаём UDP сокеты для прослушки
         | 
| 14 | 
            +
                listen_sockets = []
         | 
| 15 | 
            +
                for port in local_ports:
         | 
| 16 | 
            +
                    # IPv4
         | 
| 17 | 
            +
                    try:
         | 
| 18 | 
            +
                        sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         | 
| 19 | 
            +
                        sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 20 | 
            +
                        sock4.bind(("", port))
         | 
| 21 | 
            +
                        listen_sockets.append(sock4)
         | 
| 22 | 
            +
                        print(f"[UDP Discovery] Listening IPv4 on *:{port}")
         | 
| 23 | 
            +
                    except Exception as e:
         | 
| 24 | 
            +
                        print(f"[UDP Discovery] IPv4 bind failed on port {port}: {e}")
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    # IPv6
         | 
| 27 | 
            +
                    try:
         | 
| 28 | 
            +
                        sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
         | 
| 29 | 
            +
                        sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 30 | 
            +
                        sock6.bind(("::", port))
         | 
| 31 | 
            +
                        listen_sockets.append(sock6)
         | 
| 32 | 
            +
                        print(f"[UDP Discovery] Listening IPv6 on [::]:{port}")
         | 
| 33 | 
            +
                    except Exception as e:
         | 
| 34 | 
            +
                        print(f"[UDP Discovery] IPv6 bind failed on port {port}: {e}")
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                while True:
         | 
| 37 | 
            +
                    # Приём сообщений
         | 
| 38 | 
            +
                    rlist, _, _ = select.select(listen_sockets, [], [], 0.5)
         | 
| 39 | 
            +
                    for sock in rlist:
         | 
| 40 | 
            +
                        try:
         | 
| 41 | 
            +
                            data, addr = sock.recvfrom(2048)
         | 
| 42 | 
            +
                            msg = json.loads(data.decode("utf-8"))
         | 
| 43 | 
            +
                            peer_id = msg.get("id")
         | 
| 44 | 
            +
                            if peer_id == my_id:
         | 
| 45 | 
            +
                                continue
         | 
| 46 | 
            +
                            name = msg.get("name", "unknown")
         | 
| 47 | 
            +
                            addresses = msg.get("addresses", [])
         | 
| 48 | 
            +
                            storage.add_or_update_peer(peer_id, name, addresses, "discovery", "online")
         | 
| 49 | 
            +
                            print(f"[UDP Discovery] peer={peer_id} from {addr}")
         | 
| 50 | 
            +
                        except Exception as e:
         | 
| 51 | 
            +
                            print(f"[UDP Discovery] receive error: {e}")
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    # Отправка broadcast/multicast
         | 
| 54 | 
            +
                    for port in local_ports:
         | 
| 55 | 
            +
                        # IPv4 broadcast
         | 
| 56 | 
            +
                        for iface in netifaces.interfaces():
         | 
| 57 | 
            +
                            addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
         | 
| 58 | 
            +
                            for a in addrs:
         | 
| 59 | 
            +
                                if "broadcast" in a:
         | 
| 60 | 
            +
                                    try:
         | 
| 61 | 
            +
                                        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         | 
| 62 | 
            +
                                        sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
         | 
| 63 | 
            +
                                        sock.sendto(msg_data, (a["broadcast"], port))
         | 
| 64 | 
            +
                                        sock.close()
         | 
| 65 | 
            +
                                    except Exception:
         | 
| 66 | 
            +
                                        continue
         | 
| 67 | 
            +
                        # IPv6 multicast ff02::1
         | 
| 68 | 
            +
                        for iface in netifaces.interfaces():
         | 
| 69 | 
            +
                            ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
         | 
| 70 | 
            +
                            for a in ifaddrs:
         | 
| 71 | 
            +
                                addr = a.get("addr")
         | 
| 72 | 
            +
                                if not addr:
         | 
| 73 | 
            +
                                    continue
         | 
| 74 | 
            +
                                multicast_addr = f"ff02::1%{iface}" if addr.startswith("fe80:") else "ff02::1"
         | 
| 75 | 
            +
                                try:
         | 
| 76 | 
            +
                                    sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
         | 
| 77 | 
            +
                                    sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
         | 
| 78 | 
            +
                                    sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
         | 
| 79 | 
            +
                                    sock6.sendto(msg_data, (multicast_addr, port))
         | 
| 80 | 
            +
                                    sock6.close()
         | 
| 81 | 
            +
                                except Exception:
         | 
| 82 | 
            +
                                    continue
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    time.sleep(DISCOVERY_INTERVAL)
         | 
| 85 | 
            +
             | 
| 86 | 
            +
            ---
         | 
| 87 | 
            +
             | 
| 88 | 
            +
            def tcp_peer_exchange():
         | 
| 89 | 
            +
                PEER_EXCHANGE_INTERVAL = 20  # для отладки
         | 
| 90 | 
            +
                while True:
         | 
| 91 | 
            +
                    peers = storage.get_known_peers(my_id, limit=50)
         | 
| 92 | 
            +
                    print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                    for peer in peers:
         | 
| 95 | 
            +
                        peer_id = peer["id"] if isinstance(peer, dict) else peer[0]
         | 
| 96 | 
            +
                        addresses_json = peer["addresses"] if isinstance(peer, dict) else peer[1]
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                        if peer_id == my_id:
         | 
| 99 | 
            +
                            continue
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                        try:
         | 
| 102 | 
            +
                            addr_list = json.loads(addresses_json)
         | 
| 103 | 
            +
                        except Exception as e:
         | 
| 104 | 
            +
                            print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
         | 
| 105 | 
            +
                            addr_list = []
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                        for addr in addr_list:
         | 
| 108 | 
            +
                            norm = storage.normalize_address(addr)
         | 
| 109 | 
            +
                            if not norm:
         | 
| 110 | 
            +
                                continue
         | 
| 111 | 
            +
                            proto, hostport = norm.split("://", 1)
         | 
| 112 | 
            +
                            if proto not in ["tcp", "any"]:
         | 
| 113 | 
            +
                                continue
         | 
| 114 | 
            +
                            host, port = storage.parse_hostport(hostport)
         | 
| 115 | 
            +
                            if not host or not port:
         | 
| 116 | 
            +
                                continue
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                            print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
         | 
| 119 | 
            +
                            try:
         | 
| 120 | 
            +
                                # IPv6 link-local
         | 
| 121 | 
            +
                                if storage.is_ipv6(host) and host.startswith("fe80:"):
         | 
| 122 | 
            +
                                    scope_id = storage.get_ipv6_scope(host)
         | 
| 123 | 
            +
                                    if scope_id is None:
         | 
| 124 | 
            +
                                        print(f"[PeerExchange] Skipping {host}, no scope_id")
         | 
| 125 | 
            +
                                        continue
         | 
| 126 | 
            +
                                    sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
         | 
| 127 | 
            +
                                    sock.settimeout(3)
         | 
| 128 | 
            +
                                    sock.connect((host, port, 0, scope_id))
         | 
| 129 | 
            +
                                else:
         | 
| 130 | 
            +
                                    sock = socket.socket(socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET,
         | 
| 131 | 
            +
                                                         socket.SOCK_STREAM)
         | 
| 132 | 
            +
                                    sock.settimeout(3)
         | 
| 133 | 
            +
                                    sock.connect((host, port))
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                                # LAN или Интернет
         | 
| 136 | 
            +
                                if storage.is_private(host):
         | 
| 137 | 
            +
                                    send_addresses = all_addresses
         | 
| 138 | 
            +
                                else:
         | 
| 139 | 
            +
                                    send_addresses = [a for a in all_addresses
         | 
| 140 | 
            +
                                                      if is_public(stprage.parse_hostport(a.split("://", 1)[1])[0])]
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                                handshake = {
         | 
| 143 | 
            +
                                    "type": "PEER_EXCHANGE_REQUEST",
         | 
| 144 | 
            +
                                    "id": my_id,
         | 
| 145 | 
            +
                                    "name": agent_name,
         | 
| 146 | 
            +
                                    "addresses": send_addresses,
         | 
| 147 | 
            +
                                }
         | 
| 148 | 
            +
                                sock.sendall(json.dumps(handshake).encode("utf-8"))
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                                data = sock.recv(64 * 1024)
         | 
| 151 | 
            +
                                sock.close()
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                                if not data:
         | 
| 154 | 
            +
                                    print(f"[PeerExchange] No data from {host}:{port}")
         | 
| 155 | 
            +
                                    continue
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                                try:
         | 
| 158 | 
            +
                                    peers_recv = json.loads(data.decode("utf-8"))
         | 
| 159 | 
            +
                                    for p in peers_recv:
         | 
| 160 | 
            +
                                        if p.get("id") and p["id"] != my_id:
         | 
| 161 | 
            +
                                            storage.add_or_update_peer(
         | 
| 162 | 
            +
                                                p["id"], p.get("name", "unknown"), p.get("addresses", []),
         | 
| 163 | 
            +
                                                "peer_exchange", "online"
         | 
| 164 | 
            +
                                            )
         | 
| 165 | 
            +
                                    print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}")
         | 
| 166 | 
            +
                                except Exception as e:
         | 
| 167 | 
            +
                                    print(f"[PeerExchange] Decode error from {host}:{port} -> {e}")
         | 
| 168 | 
            +
                                    continue
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                                break
         | 
| 171 | 
            +
                            except Exception as e:
         | 
| 172 | 
            +
                                print(f"[PeerExchange] Connection to {host}:{port} failed: {e}")
         | 
| 173 | 
            +
                                continue
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                    time.sleep(PEER_EXCHANGE_INTERVAL)
         | 
| 176 | 
            +
             | 
| 177 | 
            +
            ---
         | 
| 178 | 
            +
             | 
| 179 | 
            +
            def tcp_listener():
         | 
| 180 | 
            +
                listen_sockets = []
         | 
| 181 | 
            +
                for port in local_ports:
         | 
| 182 | 
            +
                    for family, addr_str in [(socket.AF_INET, ""), (socket.AF_INET6, "::")]:
         | 
| 183 | 
            +
                        try:
         | 
| 184 | 
            +
                            sock = socket.socket(family, socket.SOCK_STREAM)
         | 
| 185 | 
            +
                            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 186 | 
            +
                            sock.bind((addr_str, port))
         | 
| 187 | 
            +
                            sock.listen(5)
         | 
| 188 | 
            +
                            listen_sockets.append(sock)
         | 
| 189 | 
            +
                            proto_str = "IPv6" if family == socket.AF_INET6 else "IPv4"
         | 
| 190 | 
            +
                            print(f"[TCP Listener] Listening {proto_str} on {addr_str}:{port}")
         | 
| 191 | 
            +
                        except Exception as e:
         | 
| 192 | 
            +
                            print(f"[TCP Listener] {proto_str} bind failed on port {port}: {e}")
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                while True:
         | 
| 195 | 
            +
                    if not listen_sockets:
         | 
| 196 | 
            +
                        time.sleep(1)
         | 
| 197 | 
            +
                        continue
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                    rlist, _, _ = select.select(listen_sockets, [], [], 1)
         | 
| 200 | 
            +
                    for s in rlist:
         | 
| 201 | 
            +
                        try:
         | 
| 202 | 
            +
                            conn, addr = s.accept()
         | 
| 203 | 
            +
                            data = conn.recv(64 * 1024)
         | 
| 204 | 
            +
                            if not data:
         | 
| 205 | 
            +
                                conn.close()
         | 
| 206 | 
            +
                                continue
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                            try:
         | 
| 209 | 
            +
                                msg = json.loads(data.decode("utf-8"))
         | 
| 210 | 
            +
                            except Exception as e:
         | 
| 211 | 
            +
                                print(f"[TCP Listener] JSON decode error from {addr}: {e}")
         | 
| 212 | 
            +
                                conn.close()
         | 
| 213 | 
            +
                                continue
         | 
| 214 | 
            +
             | 
| 215 | 
            +
                            if msg.get("type") == "PEER_EXCHANGE_REQUEST":
         | 
| 216 | 
            +
                                peer_id = msg.get("id") or f"did:hmp:{addr[0]}:{addr[1]}"
         | 
| 217 | 
            +
                                peer_name = msg.get("name", "unknown")
         | 
| 218 | 
            +
                                peer_addrs = msg.get("addresses", [])
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                                storage.add_or_update_peer(peer_id, peer_name, peer_addrs,
         | 
| 221 | 
            +
                                                           source="incoming", status="online")
         | 
| 222 | 
            +
                                print(f"[TCP Listener] Handshake from {peer_id} ({addr})")
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                                # LAN или Интернет
         | 
| 225 | 
            +
                                is_lan = storage.is_private(addr[0])
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                                peers_list = []
         | 
| 228 | 
            +
                                for peer in storage.get_known_peers(my_id, limit=50):
         | 
| 229 | 
            +
                                    peer_id = peer["id"]
         | 
| 230 | 
            +
                                    try:
         | 
| 231 | 
            +
                                        addresses = json.loads(peer["addresses"])
         | 
| 232 | 
            +
                                    except:
         | 
| 233 | 
            +
                                        addresses = []
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                                    updated_addresses = []
         | 
| 236 | 
            +
                                    for a in addresses:
         | 
| 237 | 
            +
                                        proto, hostport = a.split("://")
         | 
| 238 | 
            +
                                        host, port = storage.parse_hostport(hostport)
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                                        # Фильтруем по LAN/Internet
         | 
| 241 | 
            +
                                        if not is_lan and not is_public(host):
         | 
| 242 | 
            +
                                            continue
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                                        # IPv6 link-local
         | 
| 245 | 
            +
                                        if storage.is_ipv6(host) and host.startswith("fe80:"):
         | 
| 246 | 
            +
                                            scope_id = storage.get_ipv6_scope(host)
         | 
| 247 | 
            +
                                            if scope_id:
         | 
| 248 | 
            +
                                                host = f"{host}%{scope_id}"
         | 
| 249 | 
            +
             | 
| 250 | 
            +
                                        updated_addresses.append(f"{proto}://{host}:{port}")
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                                    peers_list.append({"id": peer_id, "addresses": updated_addresses})
         | 
| 253 | 
            +
             | 
| 254 | 
            +
                                conn.sendall(json.dumps(peers_list).encode("utf-8"))
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                            conn.close()
         | 
| 257 | 
            +
                        except Exception as e:
         | 
| 258 | 
            +
                            print(f"[TCP Listener] Connection handling error: {e}")
         | 
| 259 | 
            +
            ```
         | 
    	
        agents/peer_sync.py
    CHANGED
    
    | @@ -6,28 +6,30 @@ import time | |
| 6 | 
             
            import threading
         | 
| 7 | 
             
            import select
         | 
| 8 | 
             
            import netifaces
         | 
| 9 | 
            -
            import re
         | 
| 10 | 
             
            import ipaddress
         | 
| 11 | 
            -
            import asyncio
         | 
| 12 | 
            -
            import dateutil.parser
         | 
| 13 |  | 
| 14 | 
            -
             | 
| 15 | 
            -
            from datetime import datetime, timezone as UTC
         | 
| 16 | 
             
            from tools.storage import Storage
         | 
| 17 |  | 
|  | |
|  | |
| 18 | 
             
            storage = Storage()
         | 
| 19 |  | 
| 20 | 
             
            # ---------------------------
         | 
| 21 | 
             
            # Конфигурация
         | 
| 22 | 
             
            # ---------------------------
         | 
| 23 | 
             
            my_id = storage.get_config_value("agent_id")
         | 
|  | |
| 24 | 
             
            agent_name = storage.get_config_value("agent_name", "unknown")
         | 
| 25 | 
            -
            local_addresses = storage.get_config_value("local_addresses", [])
         | 
| 26 | 
            -
            global_addresses = storage.get_config_value("global_addresses", [])
         | 
| 27 | 
            -
            all_addresses = local_addresses + global_addresses  # один раз
         | 
| 28 |  | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 31 |  | 
| 32 | 
             
            # ---------------------------
         | 
| 33 | 
             
            # Загрузка bootstrap
         | 
| @@ -75,7 +77,7 @@ def load_bootstrap_peers(filename="bootstrap.txt"): | |
| 75 | 
             
                        print(f"[Bootstrap] Failed to parse JSON addresses: {line} ({e})")
         | 
| 76 | 
             
                        continue
         | 
| 77 |  | 
| 78 | 
            -
                    # Расширяем any:// в tcp/udp и приводим к  | 
| 79 | 
             
                    expanded_addresses = []
         | 
| 80 | 
             
                    for addr in addresses:
         | 
| 81 | 
             
                        if isinstance(addr, dict):
         | 
| @@ -128,268 +130,175 @@ def load_bootstrap_peers(filename="bootstrap.txt"): | |
| 128 | 
             
                        status="offline",
         | 
| 129 | 
             
                        pubkey=pubkey,
         | 
| 130 | 
             
                        capabilities=None,
         | 
| 131 | 
            -
                        heard_from=None
         | 
|  | |
| 132 | 
             
                    )
         | 
| 133 |  | 
| 134 | 
             
                    print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
         | 
| 135 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 136 | 
             
            # ---------------------------
         | 
| 137 | 
             
            # UDP Discovery
         | 
| 138 | 
             
            # ---------------------------
         | 
| 139 | 
            -
            def udp_discovery():
         | 
| 140 | 
            -
                 | 
| 141 | 
             
                DISCOVERY_INTERVAL = 30
         | 
| 142 |  | 
| 143 | 
            -
                try:
         | 
| 144 | 
            -
                    # --- Создаём слушающие сокеты один раз ---
         | 
| 145 | 
            -
                    listen_sockets = []
         | 
| 146 | 
            -
                    local_addresses = storage.get_config_value("local_addresses", [])
         | 
| 147 | 
            -
                    print(f"[UDP Discovery] Local addresses (init): {local_addresses}")
         | 
| 148 | 
            -
             | 
| 149 | 
            -
                    for entry in local_addresses:
         | 
| 150 | 
            -
                        addr_str = entry.get("addr") if isinstance(entry, dict) else entry
         | 
| 151 | 
            -
                        if not addr_str:
         | 
| 152 | 
            -
                            continue
         | 
| 153 | 
            -
             | 
| 154 | 
            -
                        proto, hostport = addr_str.split("://", 1)
         | 
| 155 | 
            -
                        host, port = storage.parse_hostport(hostport)
         | 
| 156 | 
            -
                        if not port or proto.lower() != "udp":
         | 
| 157 | 
            -
                            continue
         | 
| 158 | 
            -
             | 
| 159 | 
            -
                        # IPv4
         | 
| 160 | 
            -
                        if not host.startswith("["):
         | 
| 161 | 
            -
                            try:
         | 
| 162 | 
            -
                                sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         | 
| 163 | 
            -
                                sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 164 | 
            -
                                sock4.bind(("", port))
         | 
| 165 | 
            -
                                listen_sockets.append(sock4)
         | 
| 166 | 
            -
                                print(f"[UDP Discovery] Listening IPv4 on *:{port}")
         | 
| 167 | 
            -
                            except Exception as e:
         | 
| 168 | 
            -
                                print(f"[UDP Discovery] IPv4 bind failed on port {port}: {e}")
         | 
| 169 | 
            -
             | 
| 170 | 
            -
                        # IPv6
         | 
| 171 | 
            -
                        else:
         | 
| 172 | 
            -
                            try:
         | 
| 173 | 
            -
                                sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
         | 
| 174 | 
            -
                                sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 175 | 
            -
                                sock6.bind(("::", port))
         | 
| 176 | 
            -
                                listen_sockets.append(sock6)
         | 
| 177 | 
            -
                                print(f"[UDP Discovery] Listening IPv6 on [::]:{port}")
         | 
| 178 | 
            -
                            except Exception as e:
         | 
| 179 | 
            -
                                print(f"[UDP Discovery] IPv6 bind failed on port {port}: {e}")
         | 
| 180 | 
            -
             | 
| 181 | 
            -
                except Exception as init_e:
         | 
| 182 | 
            -
                    print(f"[UDP Discovery] init error: {init_e}")
         | 
| 183 | 
            -
                    return
         | 
| 184 | 
            -
             | 
| 185 | 
            -
                # --- Основной цикл ---
         | 
| 186 | 
            -
                agent_pubkey = storage.get_config_value("agent_pubkey")
         | 
| 187 | 
            -
             | 
| 188 | 
             
                while True:
         | 
|  | |
| 189 | 
             
                    try:
         | 
| 190 | 
            -
                         | 
| 191 | 
            -
                         | 
| 192 | 
            -
                             | 
| 193 | 
            -
             | 
| 194 | 
            -
                                 | 
| 195 | 
            -
             | 
| 196 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 197 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 198 | 
             
                                    try:
         | 
| 199 | 
            -
                                         | 
|  | |
|  | |
|  | |
| 200 | 
             
                                    except Exception as e:
         | 
| 201 | 
            -
                                        print(f"[UDP Discovery]  | 
| 202 | 
            -
             | 
| 203 | 
            -
             | 
| 204 | 
            -
             | 
| 205 | 
            -
             | 
| 206 | 
            -
             | 
| 207 | 
            -
             | 
| 208 | 
            -
             | 
| 209 | 
            -
             | 
| 210 | 
            -
                                    valid_addresses = []
         | 
| 211 | 
            -
                                    for a in addresses:
         | 
| 212 | 
            -
                                        addr_str = a.get("addr")
         | 
| 213 | 
            -
                                        nonce = a.get("nonce")
         | 
| 214 | 
            -
                                        pow_hash = a.get("pow_hash")
         | 
| 215 | 
            -
                                        difficulty = a.get("difficulty")
         | 
| 216 | 
            -
                                        dt = a.get("datetime")
         | 
| 217 | 
            -
                                        pubkey = a.get("pubkey")
         | 
| 218 | 
            -
             | 
| 219 | 
            -
                                        if not addr_str or not pubkey:
         | 
| 220 | 
            -
                                            continue
         | 
| 221 | 
            -
             | 
| 222 | 
            -
                                        # Проверка PoW
         | 
| 223 | 
            -
                                        if nonce is not None and pow_hash and difficulty is not None:
         | 
| 224 | 
            -
                                            ok = storage.verify_pow(peer_id, pubkey, addr_str, nonce, pow_hash, dt, difficulty)
         | 
| 225 | 
            -
                                            print(f"[UDP Discovery] Verify PoW for {addr_str} = {ok}")
         | 
| 226 | 
            -
                                            if not ok:
         | 
| 227 | 
            -
                                                continue
         | 
| 228 | 
            -
             | 
| 229 | 
            -
                                        # Проверка datetime
         | 
| 230 | 
            -
                                        existing = storage.get_peer_address(peer_id, addr_str)
         | 
| 231 | 
            -
                                        try:
         | 
| 232 | 
            -
                                            existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
         | 
| 233 | 
            -
                                            dt_obj = dateutil.parser.isoparse(dt) if dt else None
         | 
| 234 | 
            -
                                        except Exception as e:
         | 
| 235 | 
            -
                                            print(f"[UDP Discovery] datetime parse error: {e}")
         | 
| 236 | 
            -
                                            continue
         | 
| 237 | 
            -
             | 
| 238 | 
            -
                                        if existing_dt and dt_obj and existing_dt >= dt_obj:
         | 
| 239 | 
            -
                                            print(f"[UDP Discovery] Skip {addr_str}: old datetime {dt}")
         | 
| 240 | 
            -
                                            continue
         | 
| 241 | 
            -
             | 
| 242 | 
            -
                                        valid_addresses.append(a)
         | 
| 243 | 
            -
             | 
| 244 | 
            -
                                    if valid_addresses:
         | 
| 245 | 
            -
                                        storage.add_or_update_peer(
         | 
| 246 | 
            -
                                            peer_id=peer_id,
         | 
| 247 | 
            -
                                            name=name,
         | 
| 248 | 
            -
                                            addresses=valid_addresses,
         | 
| 249 | 
            -
                                            source="discovery",
         | 
| 250 | 
            -
                                            status="online"
         | 
| 251 | 
            -
                                        )
         | 
| 252 | 
            -
                                        print(f"[UDP Discovery] Accepted peer {peer_id} ({addr}), {len(valid_addresses)} addresses")
         | 
| 253 | 
            -
             | 
| 254 | 
            -
                                except Exception as e:
         | 
| 255 | 
            -
                                    print(f"[UDP Discovery] receive error: {e}")
         | 
| 256 | 
            -
             | 
| 257 | 
            -
                        # --- Отправка broadcast/multicast ---
         | 
| 258 | 
            -
                        local_addresses = storage.get_config_value("local_addresses", [])
         | 
| 259 | 
            -
                        valid_local_addresses = []
         | 
| 260 | 
            -
             | 
| 261 | 
            -
                        for a in local_addresses:
         | 
| 262 | 
            -
                            addr_str = a.get("addr") if isinstance(a, dict) else a
         | 
| 263 | 
            -
                            nonce = a.get("nonce")
         | 
| 264 | 
            -
                            pow_hash = a.get("pow_hash")
         | 
| 265 | 
            -
                            difficulty = a.get("difficulty")
         | 
| 266 | 
            -
                            dt = a.get("datetime")
         | 
| 267 | 
            -
                            pubkey = a.get("pubkey") if isinstance(a, dict) else agent_pubkey  # self-check
         | 
| 268 | 
            -
             | 
| 269 | 
            -
                            if not addr_str:
         | 
| 270 | 
            -
                                continue
         | 
| 271 | 
            -
             | 
| 272 | 
            -
                            # Проверка PoW только если есть необходимые поля
         | 
| 273 | 
            -
                            if nonce is not None and pow_hash and difficulty is not None:
         | 
| 274 | 
            -
                                ok = storage.verify_pow(my_id, pubkey, addr_str, nonce, pow_hash, dt, difficulty)
         | 
| 275 | 
            -
                                print(f"[UDP Discovery] Self-check PoW for {addr_str} = {ok}")
         | 
| 276 | 
            -
                                if not ok:
         | 
| 277 | 
             
                                    continue
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 278 |  | 
| 279 | 
            -
             | 
| 280 | 
            -
             | 
| 281 | 
            -
                        msg_data = json.dumps({
         | 
| 282 | 
            -
                            "id": my_id,
         | 
| 283 | 
            -
                            "name": agent_name,
         | 
| 284 | 
            -
                            "addresses": valid_local_addresses
         | 
| 285 | 
            -
                        }).encode("utf-8")
         | 
| 286 | 
            -
             | 
| 287 | 
            -
                        print(f"[UDP Discovery] Broadcasting: {msg_data}")
         | 
| 288 | 
            -
             | 
| 289 | 
            -
                        for entry in valid_local_addresses:
         | 
| 290 | 
            -
                            addr_str = entry.get("addr")
         | 
| 291 | 
            -
                            proto, hostport = addr_str.split("://", 1)
         | 
| 292 | 
            -
                            host, port = storage.parse_hostport(hostport)
         | 
| 293 | 
            -
                            if not port or proto.lower() != "udp":
         | 
| 294 | 
            -
                                continue
         | 
| 295 | 
            -
             | 
| 296 | 
            -
                            # IPv4 broadcast
         | 
| 297 | 
            -
                            if not host.startswith("["):
         | 
| 298 | 
            -
                                for iface in netifaces.interfaces():
         | 
| 299 | 
            -
                                    addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
         | 
| 300 | 
            -
                                    for a in addrs:
         | 
| 301 | 
            -
                                        if "broadcast" in a:
         | 
| 302 | 
            -
                                            try:
         | 
| 303 | 
            -
                                                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         | 
| 304 | 
            -
                                                sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
         | 
| 305 | 
            -
                                                print(f"[UDP Discovery] Sending broadcast -> {a['broadcast']}:{port}")
         | 
| 306 | 
            -
                                                sock.sendto(msg_data, (a["broadcast"], port))
         | 
| 307 | 
            -
                                                sock.close()
         | 
| 308 | 
            -
                                            except Exception as e:
         | 
| 309 | 
            -
                                                print(f"[UDP Discovery] Broadcast error {a['broadcast']}:{port}: {e}")
         | 
| 310 | 
            -
             | 
| 311 | 
            -
                            # IPv6 multicast ff02::1
         | 
| 312 | 
            -
                            else:
         | 
| 313 | 
            -
                                for iface in netifaces.interfaces():
         | 
| 314 | 
            -
                                    ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
         | 
| 315 | 
            -
                                    for a in ifaddrs:
         | 
| 316 | 
            -
                                        addr_ipv6 = a.get("addr")
         | 
| 317 | 
            -
                                        if not addr_ipv6:
         | 
| 318 | 
            -
                                            continue
         | 
| 319 | 
            -
                                        multicast_addr = f"ff02::1%{iface}" if addr_ipv6.startswith("fe80:") else "ff02::1"
         | 
| 320 | 
            -
                                        try:
         | 
| 321 | 
            -
                                            sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
         | 
| 322 | 
            -
                                            sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
         | 
| 323 | 
            -
                                            sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
         | 
| 324 | 
            -
                                            print(f"[UDP Discovery] Sending multicast -> {multicast_addr}:{port}")
         | 
| 325 | 
            -
                                            sock6.sendto(msg_data, (multicast_addr, port))
         | 
| 326 | 
            -
                                            sock6.close()
         | 
| 327 | 
            -
                                        except Exception as e:
         | 
| 328 | 
            -
                                            print(f"[UDP Discovery] Multicast error {multicast_addr}:{port}: {e}")
         | 
| 329 | 
            -
             | 
| 330 | 
            -
                        time.sleep(DISCOVERY_INTERVAL)
         | 
| 331 | 
            -
             | 
| 332 | 
            -
                    except Exception as main_e:
         | 
| 333 | 
            -
                        print(f"[UDP Discovery] main loop error: {main_e}")
         | 
| 334 | 
            -
                        time.sleep(DISCOVERY_INTERVAL)
         | 
| 335 |  | 
| 336 | 
             
            # ---------------------------
         | 
| 337 | 
             
            # TCP Peer Exchange (исходящие)
         | 
| 338 | 
             
            # ---------------------------
         | 
| 339 | 
             
            def tcp_peer_exchange():
         | 
| 340 | 
            -
                 | 
| 341 | 
            -
                PEER_EXCHANGE_INTERVAL = 20  # секунды для отладки
         | 
| 342 | 
            -
             | 
| 343 | 
             
                while True:
         | 
| 344 | 
             
                    peers = storage.get_known_peers(my_id, limit=50)
         | 
| 345 | 
             
                    print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
         | 
| 346 |  | 
| 347 | 
             
                    for peer in peers:
         | 
| 348 | 
            -
                         | 
| 349 | 
            -
                         | 
|  | |
| 350 |  | 
|  | |
| 351 | 
             
                        if peer_id == my_id:
         | 
| 352 | 
             
                            continue
         | 
| 353 |  | 
| 354 | 
             
                        try:
         | 
| 355 | 
            -
                            addr_list = json.loads( | 
| 356 | 
             
                        except Exception as e:
         | 
| 357 | 
             
                            print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
         | 
| 358 | 
             
                            addr_list = []
         | 
| 359 |  | 
| 360 | 
            -
                        for  | 
| 361 | 
            -
                             | 
| 362 | 
            -
                            nonce = addr_entry.get("nonce")
         | 
| 363 | 
            -
                            pow_hash = addr_entry.get("pow_hash")
         | 
| 364 | 
            -
                            difficulty = addr_entry.get("difficulty")
         | 
| 365 | 
            -
                            dt = addr_entry.get("datetime")
         | 
| 366 | 
            -
                            pubkey = addr_entry.get("pubkey")
         | 
| 367 | 
            -
             | 
| 368 | 
            -
                            norm = storage.normalize_address(addr_str)
         | 
| 369 | 
             
                            if not norm:
         | 
| 370 | 
             
                                continue
         | 
| 371 | 
            -
             | 
| 372 | 
            -
                            # Проверка PoW
         | 
| 373 | 
            -
                            if nonce is not None and pow_hash and difficulty is not None and pubkey:
         | 
| 374 | 
            -
                                ok = storage.verify_pow(peer_id, pubkey, addr_str, nonce, pow_hash, dt, difficulty)
         | 
| 375 | 
            -
                                print(f"[PeerExchange] Verify PoW for {peer_id}@{addr_str} = {ok}")
         | 
| 376 | 
            -
                                if not ok:
         | 
| 377 | 
            -
                                    continue
         | 
| 378 | 
            -
             | 
| 379 | 
            -
                            # Проверка datetime с использованием dateutil
         | 
| 380 | 
            -
                            existing = storage.get_peer_address(peer_id, addr_str)
         | 
| 381 | 
            -
                            try:
         | 
| 382 | 
            -
                                existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
         | 
| 383 | 
            -
                                dt_obj = dateutil.parser.isoparse(dt) if dt else None
         | 
| 384 | 
            -
                            except Exception as e:
         | 
| 385 | 
            -
                                print(f"[PeerExchange] datetime parse error for {addr_str}: {e}")
         | 
| 386 | 
            -
                                continue
         | 
| 387 | 
            -
             | 
| 388 | 
            -
                            if existing_dt and dt_obj and existing_dt >= dt_obj:
         | 
| 389 | 
            -
                                print(f"[PeerExchange] Skip {addr_str}: old datetime {dt}")
         | 
| 390 | 
            -
                                continue
         | 
| 391 | 
            -
             | 
| 392 | 
            -
                            # Парсим host и port
         | 
| 393 | 
             
                            proto, hostport = norm.split("://", 1)
         | 
| 394 | 
             
                            if proto not in ["tcp", "any"]:
         | 
| 395 | 
             
                                continue
         | 
| @@ -398,30 +307,21 @@ def tcp_peer_exchange(): | |
| 398 | 
             
                                continue
         | 
| 399 |  | 
| 400 | 
             
                            print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
         | 
| 401 | 
            -
             | 
| 402 | 
             
                            try:
         | 
| 403 | 
            -
                                 | 
| 404 | 
            -
             | 
| 405 | 
            -
                                     | 
| 406 | 
            -
             | 
| 407 | 
            -
             | 
| 408 | 
            -
             | 
| 409 | 
            -
             | 
| 410 | 
            -
             | 
| 411 | 
            -
                                    sock.connect((host, port, 0, scope_id))
         | 
| 412 | 
            -
                                else:
         | 
| 413 | 
            -
                                    sock = socket.socket(socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET,
         | 
| 414 | 
            -
                                                         socket.SOCK_STREAM)
         | 
| 415 | 
            -
                                    sock.settimeout(3)
         | 
| 416 | 
            -
                                    sock.connect((host, port))
         | 
| 417 | 
            -
             | 
| 418 | 
            -
                                # Отправляем handshake
         | 
| 419 | 
             
                                if storage.is_private(host):
         | 
| 420 | 
             
                                    send_addresses = all_addresses
         | 
| 421 | 
             
                                else:
         | 
| 422 | 
             
                                    send_addresses = [
         | 
| 423 | 
             
                                        a for a in all_addresses
         | 
| 424 | 
            -
                                        if  | 
| 425 | 
             
                                    ]
         | 
| 426 |  | 
| 427 | 
             
                                handshake = {
         | 
| @@ -430,11 +330,8 @@ def tcp_peer_exchange(): | |
| 430 | 
             
                                    "name": agent_name,
         | 
| 431 | 
             
                                    "addresses": send_addresses,
         | 
| 432 | 
             
                                }
         | 
| 433 | 
            -
                                 | 
| 434 | 
            -
                                print(f"[PeerExchange] Sending handshake -> {host}:{port}: {raw_handshake}")
         | 
| 435 | 
            -
                                sock.sendall(raw_handshake)
         | 
| 436 |  | 
| 437 | 
            -
                                # Читаем ответ
         | 
| 438 | 
             
                                data = sock.recv(64 * 1024)
         | 
| 439 | 
             
                                sock.close()
         | 
| 440 |  | 
| @@ -442,38 +339,21 @@ def tcp_peer_exchange(): | |
| 442 | 
             
                                    print(f"[PeerExchange] No data from {host}:{port}")
         | 
| 443 | 
             
                                    continue
         | 
| 444 |  | 
| 445 | 
            -
                                print(f"[PeerExchange] RAW recv from {host}:{port}: {data!r}")
         | 
| 446 | 
            -
             | 
| 447 | 
             
                                try:
         | 
| 448 | 
             
                                    peers_recv = json.loads(data.decode("utf-8"))
         | 
| 449 | 
            -
                                    print(f"[PeerExchange] Parsed recv from {host}:{port}: {peers_recv}")
         | 
| 450 | 
             
                                    for p in peers_recv:
         | 
| 451 | 
            -
                                         | 
| 452 | 
            -
                                        for a in p.get("addresses", []):
         | 
| 453 | 
            -
                                            try:
         | 
| 454 | 
            -
                                                existing_addr = storage.get_peer_address(p["id"], a.get("addr"))
         | 
| 455 | 
            -
                                                existing_dt = dateutil.parser.isoparse(existing_addr.get("datetime")) if existing_addr else None
         | 
| 456 | 
            -
                                                dt_obj = dateutil.parser.isoparse(a.get("datetime")) if a.get("datetime") else None
         | 
| 457 | 
            -
                                                if existing_addr is None or (existing_dt and dt_obj and existing_dt < dt_obj) or existing_dt is None:
         | 
| 458 | 
            -
                                                    new_addrs.append(a)
         | 
| 459 | 
            -
                                                else:
         | 
| 460 | 
            -
                                                    print(f"[PeerExchange] Ignored old {a.get('addr')} from {p['id']}")
         | 
| 461 | 
            -
                                            except Exception as e:
         | 
| 462 | 
            -
                                                print(f"[PeerExchange] Error parsing datetime for {a.get('addr')}: {e}")
         | 
| 463 | 
            -
                                                continue
         | 
| 464 | 
            -
             | 
| 465 | 
            -
                                        if new_addrs:
         | 
| 466 | 
             
                                            storage.add_or_update_peer(
         | 
| 467 | 
             
                                                p["id"],
         | 
| 468 | 
             
                                                p.get("name", "unknown"),
         | 
| 469 | 
            -
                                                 | 
| 470 | 
            -
                                                 | 
| 471 | 
            -
                                                 | 
|  | |
| 472 | 
             
                                            )
         | 
| 473 | 
            -
                                            print(f"[PeerExchange] Stored {len(new_addrs)} new addrs for peer {p['id']}")
         | 
| 474 | 
             
                                    print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}")
         | 
| 475 | 
             
                                except Exception as e:
         | 
| 476 | 
            -
                                    print(f"[PeerExchange] Decode error from {host}:{port} | 
| 477 | 
             
                                    continue
         | 
| 478 |  | 
| 479 | 
             
                                break
         | 
| @@ -486,149 +366,116 @@ def tcp_peer_exchange(): | |
| 486 | 
             
            # ---------------------------
         | 
| 487 | 
             
            # TCP Listener (входящие)
         | 
| 488 | 
             
            # ---------------------------
         | 
| 489 | 
            -
            def tcp_listener():
         | 
| 490 | 
            -
                 | 
| 491 | 
            -
                for port in local_ports:
         | 
| 492 | 
            -
                    for family, addr_str in [(socket.AF_INET, ""), (socket.AF_INET6, "::")]:
         | 
| 493 | 
            -
                        try:
         | 
| 494 | 
            -
                            sock = socket.socket(family, socket.SOCK_STREAM)
         | 
| 495 | 
            -
                            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 496 | 
            -
                            sock.bind((addr_str, port))
         | 
| 497 | 
            -
                            sock.listen(5)
         | 
| 498 | 
            -
                            listen_sockets.append(sock)
         | 
| 499 | 
            -
                            proto_str = "IPv6" if family == socket.AF_INET6 else "IPv4"
         | 
| 500 | 
            -
                            print(f"[TCP Listener] Listening {proto_str} on {addr_str}:{port}")
         | 
| 501 | 
            -
                        except Exception as e:
         | 
| 502 | 
            -
                            print(f"[TCP Listener] {proto_str} bind failed on port {port}: {e}")
         | 
| 503 | 
            -
             | 
| 504 | 
             
                while True:
         | 
| 505 | 
            -
                     | 
| 506 | 
            -
                         | 
| 507 | 
            -
                         | 
| 508 | 
            -
             | 
| 509 | 
            -
                    rlist, _, _ = select.select(listen_sockets, [], [], 1)
         | 
| 510 | 
            -
                    for s in rlist:
         | 
| 511 | 
            -
                        try:
         | 
| 512 | 
            -
                            conn, addr = s.accept()
         | 
| 513 | 
            -
                            data = conn.recv(64 * 1024)
         | 
| 514 | 
            -
                            if not data:
         | 
| 515 | 
            -
                                print(f"[TCP Listener] Empty data from {addr}, closing")
         | 
| 516 | 
            -
                                conn.close()
         | 
| 517 | 
            -
                                continue
         | 
| 518 | 
            -
             | 
| 519 | 
            -
                            print(f"[TCP Listener] RAW recv from {addr}: {data!r}")
         | 
| 520 | 
            -
             | 
| 521 | 
             
                            try:
         | 
| 522 | 
            -
                                 | 
| 523 | 
            -
                                 | 
| 524 | 
            -
             | 
| 525 | 
            -
             | 
| 526 | 
            -
             | 
| 527 | 
            -
                                continue
         | 
| 528 | 
            -
             | 
| 529 | 
            -
                            if msg.get("type") == "PEER_EXCHANGE_REQUEST":
         | 
| 530 | 
            -
                                peer_id = msg.get("id") or f"did:hmp:{addr[0]}:{addr[1]}"
         | 
| 531 | 
            -
                                peer_name = msg.get("name", "unknown")
         | 
| 532 | 
            -
                                peer_addrs = msg.get("addresses", [])
         | 
| 533 | 
            -
             | 
| 534 | 
            -
                                valid_addrs = []
         | 
| 535 | 
            -
                                for a in peer_addrs:
         | 
| 536 | 
            -
                                    addr_value = a.get("addr")
         | 
| 537 | 
            -
                                    nonce = a.get("nonce")
         | 
| 538 | 
            -
                                    pow_hash = a.get("pow_hash")
         | 
| 539 | 
            -
                                    difficulty = a.get("difficulty")
         | 
| 540 | 
            -
                                    dt = a.get("datetime")
         | 
| 541 | 
            -
                                    pubkey = a.get("pubkey")
         | 
| 542 | 
            -
             | 
| 543 | 
            -
                                    if not addr_value or nonce is None or not pow_hash or not pubkey:
         | 
| 544 | 
            -
                                        print(f"[TCP Listener] Skip addr (incomplete): {a}")
         | 
| 545 | 
            -
                                        continue
         | 
| 546 | 
            -
             | 
| 547 | 
            -
                                    ok = storage.verify_pow(peer_id, pubkey, addr_value, nonce, pow_hash, dt, difficulty)
         | 
| 548 | 
            -
                                    print(f"[TCP Listener] Verify PoW for {addr_value} = {ok}")
         | 
| 549 | 
            -
                                    if not ok:
         | 
| 550 | 
            -
                                        continue
         | 
| 551 | 
            -
             | 
| 552 | 
            -
                                    existing = storage.get_peer_address(peer_id, addr_value)
         | 
| 553 | 
            -
                                    try:
         | 
| 554 | 
            -
                                        existing_dt = dateutil.parser.isoparse(existing.get("datetime")) if existing else None
         | 
| 555 | 
            -
                                        dt_obj = dateutil.parser.isoparse(dt) if dt else None
         | 
| 556 | 
            -
                                    except Exception as e:
         | 
| 557 | 
            -
                                        print(f"[TCP Listener] datetime parse error for {addr_value}: {e}")
         | 
| 558 | 
            -
                                        continue
         | 
| 559 |  | 
| 560 | 
            -
             | 
| 561 | 
            -
             | 
| 562 | 
            -
             | 
|  | |
|  | |
|  | |
| 563 |  | 
| 564 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 565 |  | 
| 566 | 
            -
                                if valid_addrs:
         | 
| 567 | 
             
                                    storage.add_or_update_peer(
         | 
| 568 | 
            -
                                        peer_id | 
| 569 | 
            -
                                         | 
| 570 | 
            -
                                         | 
| 571 | 
            -
                                        source="incoming",
         | 
| 572 | 
            -
                                        status="online"
         | 
| 573 | 
             
                                    )
         | 
| 574 | 
            -
                                    print(f"[TCP Listener]  | 
| 575 | 
            -
                                else:
         | 
| 576 | 
            -
                                    print(f"[TCP Listener] No valid addrs from {peer_id}")
         | 
| 577 | 
            -
             | 
| 578 | 
            -
                                print(f"[TCP Listener] Handshake from {peer_id} ({addr}) -> name={peer_name}")
         | 
| 579 |  | 
| 580 | 
            -
             | 
| 581 | 
            -
             | 
| 582 | 
            -
                                peers_list = []
         | 
| 583 |  | 
| 584 | 
            -
             | 
| 585 | 
            -
                                     | 
| 586 | 
            -
                                     | 
| 587 | 
            -
                                         | 
| 588 | 
            -
                                    except:
         | 
| 589 | 
            -
                                        addresses = []
         | 
| 590 | 
            -
             | 
| 591 | 
            -
                                    updated_addresses = []
         | 
| 592 | 
            -
                                    for a in addresses:
         | 
| 593 | 
             
                                        try:
         | 
| 594 | 
            -
                                             | 
| 595 | 
            -
             | 
| 596 | 
            -
                                             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 597 | 
             
                                                continue
         | 
|  | |
|  | |
| 598 |  | 
| 599 | 
            -
                                             | 
|  | |
| 600 | 
             
                                                continue
         | 
| 601 |  | 
| 602 | 
            -
                                            if storage.is_ipv6(host) and host.startswith("fe80:"):
         | 
| 603 | 
            -
                                                scope_id = storage.get_ipv6_scope(host)
         | 
| 604 | 
            -
                                                if scope_id:
         | 
| 605 | 
            -
                                                    host = f"{host}%{scope_id}"
         | 
| 606 | 
            -
             | 
| 607 | 
             
                                            updated_addresses.append({
         | 
| 608 | 
            -
                                                "addr": f"{proto}://{host}:{port}"
         | 
|  | |
|  | |
|  | |
| 609 | 
             
                                            })
         | 
| 610 | 
            -
                                        except Exception:
         | 
| 611 | 
            -
                                            continue
         | 
| 612 |  | 
| 613 | 
            -
             | 
| 614 | 
            -
             | 
| 615 | 
            -
             | 
| 616 | 
            -
             | 
|  | |
| 617 |  | 
| 618 | 
            -
             | 
| 619 | 
            -
                                conn.sendall(json.dumps(peers_list).encode("utf-8"))
         | 
| 620 |  | 
| 621 | 
            -
             | 
| 622 | 
            -
             | 
| 623 | 
            -
             | 
|  | |
|  | |
| 624 |  | 
| 625 | 
             
            # ---------------------------
         | 
| 626 | 
             
            # Запуск потоков
         | 
| 627 | 
             
            # ---------------------------
         | 
| 628 | 
             
            def start_sync(bootstrap_file="bootstrap.txt"):
         | 
| 629 | 
             
                load_bootstrap_peers(bootstrap_file)
         | 
|  | |
|  | |
| 630 | 
             
                print(f"[PeerSync] Local ports: {local_ports}")
         | 
| 631 |  | 
| 632 | 
            -
                 | 
| 633 | 
            -
             | 
| 634 | 
            -
             | 
|  | |
|  | |
|  | |
|  | 
|  | |
| 6 | 
             
            import threading
         | 
| 7 | 
             
            import select
         | 
| 8 | 
             
            import netifaces
         | 
|  | |
| 9 | 
             
            import ipaddress
         | 
|  | |
|  | |
| 10 |  | 
| 11 | 
            +
            from datetime import datetime, timezone
         | 
|  | |
| 12 | 
             
            from tools.storage import Storage
         | 
| 13 |  | 
| 14 | 
            +
            UTC = timezone.utc
         | 
| 15 | 
            +
             | 
| 16 | 
             
            storage = Storage()
         | 
| 17 |  | 
| 18 | 
             
            # ---------------------------
         | 
| 19 | 
             
            # Конфигурация
         | 
| 20 | 
             
            # ---------------------------
         | 
| 21 | 
             
            my_id = storage.get_config_value("agent_id")
         | 
| 22 | 
            +
            my_pubkey = storage.get_config_value("pubkay")
         | 
| 23 | 
             
            agent_name = storage.get_config_value("agent_name", "unknown")
         | 
|  | |
|  | |
|  | |
| 24 |  | 
| 25 | 
            +
            local_addresses = storage.get_addresses("local")
         | 
| 26 | 
            +
            global_addresses = storage.get_addresses("global")
         | 
| 27 | 
            +
            all_addresses = local_addresses + global_addresses # один раз
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            #local_ports = list(set(storage.get_local_ports()))
         | 
| 30 | 
            +
            #print(f"[PeerSync] Local ports: {local_ports}")
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            #print(f"[INFO] ID: {my_id}, NAME: {agent_name}: ADDRESS: {local_addresses} + {global_addresses} = {all_addresses}; Local ports: {local_ports}")
         | 
| 33 |  | 
| 34 | 
             
            # ---------------------------
         | 
| 35 | 
             
            # Загрузка bootstrap
         | 
|  | |
| 77 | 
             
                        print(f"[Bootstrap] Failed to parse JSON addresses: {line} ({e})")
         | 
| 78 | 
             
                        continue
         | 
| 79 |  | 
| 80 | 
            +
                    # Расширяем any:// в tcp/udp и приводим к формату адресов
         | 
| 81 | 
             
                    expanded_addresses = []
         | 
| 82 | 
             
                    for addr in addresses:
         | 
| 83 | 
             
                        if isinstance(addr, dict):
         | 
|  | |
| 130 | 
             
                        status="offline",
         | 
| 131 | 
             
                        pubkey=pubkey,
         | 
| 132 | 
             
                        capabilities=None,
         | 
| 133 | 
            +
                        heard_from=None,
         | 
| 134 | 
            +
                        strict=False
         | 
| 135 | 
             
                    )
         | 
| 136 |  | 
| 137 | 
             
                    print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
         | 
| 138 |  | 
| 139 | 
            +
            # ---------------------------
         | 
| 140 | 
            +
            # start_peer_services
         | 
| 141 | 
            +
            # ---------------------------
         | 
| 142 | 
            +
            def start_peer_services(port):
         | 
| 143 | 
            +
                """Запускаем UDP и TCP слушатели на всех интерфейсах сразу"""
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                # UDP (один сокет для IPv4 и IPv6)
         | 
| 146 | 
            +
                udp_sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
         | 
| 147 | 
            +
                udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 148 | 
            +
                udp_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)  # слушаем и IPv4, и IPv6
         | 
| 149 | 
            +
                udp_sock.bind(("::", port))
         | 
| 150 | 
            +
                print(f"[UDP Discovery] Listening on [::]:{port} (IPv4+IPv6)")
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                # TCP (один сокет для IPv4 и IPv6)
         | 
| 153 | 
            +
                tcp_sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
         | 
| 154 | 
            +
                tcp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
         | 
| 155 | 
            +
                tcp_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)  # слушаем и IPv4, и IPv6
         | 
| 156 | 
            +
                tcp_sock.bind(("::", port))
         | 
| 157 | 
            +
                tcp_sock.listen()
         | 
| 158 | 
            +
                print(f"[TCP Listener] Listening on [::]:{port} (IPv4+IPv6)")
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                return udp_sock, tcp_sock
         | 
| 161 | 
            +
             | 
| 162 | 
             
            # ---------------------------
         | 
| 163 | 
             
            # UDP Discovery
         | 
| 164 | 
             
            # ---------------------------
         | 
| 165 | 
            +
            def udp_discovery(sock, local_ports):
         | 
| 166 | 
            +
                """Приём и рассылка discovery через один сокет (IPv4+IPv6)."""
         | 
| 167 | 
             
                DISCOVERY_INTERVAL = 30
         | 
| 168 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 169 | 
             
                while True:
         | 
| 170 | 
            +
                    # --- Приём сообщений ---
         | 
| 171 | 
             
                    try:
         | 
| 172 | 
            +
                        rlist, _, _ = select.select([sock], [], [], 0.5)
         | 
| 173 | 
            +
                        for s in rlist:
         | 
| 174 | 
            +
                            try:
         | 
| 175 | 
            +
                                data, addr = s.recvfrom(2048)
         | 
| 176 | 
            +
                                msg = json.loads(data.decode("utf-8"))
         | 
| 177 | 
            +
                                peer_id = msg.get("id")
         | 
| 178 | 
            +
                                if peer_id == my_id:
         | 
| 179 | 
            +
                                    continue
         | 
| 180 | 
            +
                                name = msg.get("name", "unknown")
         | 
| 181 | 
            +
                                raw_addresses = msg.get("addresses", [])
         | 
| 182 | 
            +
                                pubkey = msg.get("pubkey")
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                                addresses = []
         | 
| 185 | 
            +
                                for a in raw_addresses:
         | 
| 186 | 
            +
                                    if isinstance(a, dict) and "addr" in a:
         | 
| 187 | 
            +
                                        addresses.append({
         | 
| 188 | 
            +
                                            "addr": storage.normalize_address(a["addr"]),
         | 
| 189 | 
            +
                                            "nonce": a.get("nonce"),
         | 
| 190 | 
            +
                                            "pow_hash": a.get("pow_hash"),
         | 
| 191 | 
            +
                                            "datetime": a.get("datetime")
         | 
| 192 | 
            +
                                        })
         | 
| 193 | 
            +
                                    elif isinstance(a, str):
         | 
| 194 | 
            +
                                        addresses.append({
         | 
| 195 | 
            +
                                            "addr": storage.normalize_address(a),
         | 
| 196 | 
            +
                                            "nonce": None,
         | 
| 197 | 
            +
                                            "pow_hash": None,
         | 
| 198 | 
            +
                                            "datetime": datetime.now(UTC).replace(microsecond=0).isoformat()
         | 
| 199 | 
            +
                                        })
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                                storage.add_or_update_peer(
         | 
| 202 | 
            +
                                    peer_id, name, addresses,
         | 
| 203 | 
            +
                                    source="discovery", status="online",
         | 
| 204 | 
            +
                                    pubkey=pubkey, strict=False
         | 
| 205 | 
            +
                                )
         | 
| 206 | 
            +
                                print(f"[UDP Discovery] peer={peer_id} from {addr}")
         | 
| 207 | 
            +
                            except Exception as e:
         | 
| 208 | 
            +
                                print(f"[UDP Discovery] receive error: {e}")
         | 
| 209 | 
            +
                    except Exception as e:
         | 
| 210 | 
            +
                        print(f"[UDP Discovery] select() error: {e}")
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                    # --- Формируем локальные адреса для рассылки ---
         | 
| 213 | 
            +
                    local_addresses = []
         | 
| 214 | 
            +
                    for iface in netifaces.interfaces():
         | 
| 215 | 
            +
                        for a in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []):
         | 
| 216 | 
            +
                            ip = a.get("addr")
         | 
| 217 | 
            +
                            if ip:
         | 
| 218 | 
            +
                                local_addresses.append({
         | 
| 219 | 
            +
                                    "addr": storage.normalize_address(f"any://{ip}:{local_ports[0]}"),
         | 
| 220 | 
            +
                                    "nonce": 0,
         | 
| 221 | 
            +
                                    "pow_hash": "0"*64,
         | 
| 222 | 
            +
                                    "datetime": datetime.now(UTC).replace(microsecond=0).isoformat()
         | 
| 223 | 
            +
                                })
         | 
| 224 | 
            +
                        for a in netifaces.ifaddresses(iface).get(netifaces.AF_INET6, []):
         | 
| 225 | 
            +
                            ip = a.get("addr")
         | 
| 226 | 
            +
                            if ip:
         | 
| 227 | 
            +
                                local_addresses.append({
         | 
| 228 | 
            +
                                    "addr": storage.normalize_address(f"any://[{ip}]:{local_ports[0]}"),
         | 
| 229 | 
            +
                                    "nonce": 0,
         | 
| 230 | 
            +
                                    "pow_hash": "0"*64,
         | 
| 231 | 
            +
                                    "datetime": datetime.now(UTC).replace(microsecond=0).isoformat()
         | 
| 232 | 
            +
                                })
         | 
| 233 |  | 
| 234 | 
            +
                    msg_data = json.dumps({
         | 
| 235 | 
            +
                        "id": my_id,
         | 
| 236 | 
            +
                        "name": agent_name,
         | 
| 237 | 
            +
                        "addresses": local_addresses,
         | 
| 238 | 
            +
                        "pubkey": my_pubkey
         | 
| 239 | 
            +
                    }).encode("utf-8")
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                    for port in local_ports:
         | 
| 242 | 
            +
                        # IPv4 broadcast
         | 
| 243 | 
            +
                        for iface in netifaces.interfaces():
         | 
| 244 | 
            +
                            addrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
         | 
| 245 | 
            +
                            for a in addrs:
         | 
| 246 | 
            +
                                if "broadcast" in a:
         | 
| 247 | 
             
                                    try:
         | 
| 248 | 
            +
                                        b_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         | 
| 249 | 
            +
                                        b_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
         | 
| 250 | 
            +
                                        b_sock.sendto(msg_data, (a["broadcast"], port))
         | 
| 251 | 
            +
                                        b_sock.close()
         | 
| 252 | 
             
                                    except Exception as e:
         | 
| 253 | 
            +
                                        print(f"[UDP Discovery] IPv4 send error on {iface}:{port} -> {e}")
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                        # IPv6 multicast ff02::1
         | 
| 256 | 
            +
                        for iface in netifaces.interfaces():
         | 
| 257 | 
            +
                            ifaddrs = netifaces.ifaddresses(iface).get(netifaces.AF_INET6, [])
         | 
| 258 | 
            +
                            for a in ifaddrs:
         | 
| 259 | 
            +
                                ip = a.get("addr")
         | 
| 260 | 
            +
                                if not ip:
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 261 | 
             
                                    continue
         | 
| 262 | 
            +
                                multicast_addr = f"ff02::1%{iface}" if ip.startswith("fe80:") else "ff02::1"
         | 
| 263 | 
            +
                                try:
         | 
| 264 | 
            +
                                    m_sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
         | 
| 265 | 
            +
                                    m_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, socket.if_nametoindex(iface))
         | 
| 266 | 
            +
                                    m_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 1)
         | 
| 267 | 
            +
                                    m_sock.sendto(msg_data, (multicast_addr, port))
         | 
| 268 | 
            +
                                    m_sock.close()
         | 
| 269 | 
            +
                                except Exception as e:
         | 
| 270 | 
            +
                                    print(f"[UDP Discovery] IPv6 send error on {iface}:{port} -> {e}")
         | 
| 271 |  | 
| 272 | 
            +
                    time.sleep(DISCOVERY_INTERVAL)
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 273 |  | 
| 274 | 
             
            # ---------------------------
         | 
| 275 | 
             
            # TCP Peer Exchange (исходящие)
         | 
| 276 | 
             
            # ---------------------------
         | 
| 277 | 
             
            def tcp_peer_exchange():
         | 
| 278 | 
            +
                PEER_EXCHANGE_INTERVAL = 20
         | 
|  | |
|  | |
| 279 | 
             
                while True:
         | 
| 280 | 
             
                    peers = storage.get_known_peers(my_id, limit=50)
         | 
| 281 | 
             
                    print(f"[PeerExchange] Checking {len(peers)} peers (raw DB)...")
         | 
| 282 |  | 
| 283 | 
             
                    for peer in peers:
         | 
| 284 | 
            +
                        # sqlite3.Row → dict
         | 
| 285 | 
            +
                        if not isinstance(peer, dict):
         | 
| 286 | 
            +
                            peer = dict(peer)
         | 
| 287 |  | 
| 288 | 
            +
                        peer_id = peer.get("id")
         | 
| 289 | 
             
                        if peer_id == my_id:
         | 
| 290 | 
             
                            continue
         | 
| 291 |  | 
| 292 | 
             
                        try:
         | 
| 293 | 
            +
                            addr_list = json.loads(peer.get("addresses", "[]"))
         | 
| 294 | 
             
                        except Exception as e:
         | 
| 295 | 
             
                            print(f"[PeerExchange] JSON decode error for peer {peer_id}: {e}")
         | 
| 296 | 
             
                            addr_list = []
         | 
| 297 |  | 
| 298 | 
            +
                        for addr in addr_list:
         | 
| 299 | 
            +
                            norm = storage.normalize_address(addr)
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 300 | 
             
                            if not norm:
         | 
| 301 | 
             
                                continue
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 302 | 
             
                            proto, hostport = norm.split("://", 1)
         | 
| 303 | 
             
                            if proto not in ["tcp", "any"]:
         | 
| 304 | 
             
                                continue
         | 
|  | |
| 307 | 
             
                                continue
         | 
| 308 |  | 
| 309 | 
             
                            print(f"[PeerExchange] Trying {peer_id} at {host}:{port} (proto={proto})")
         | 
|  | |
| 310 | 
             
                            try:
         | 
| 311 | 
            +
                                sock = socket.socket(
         | 
| 312 | 
            +
                                    socket.AF_INET6 if storage.is_ipv6(host) else socket.AF_INET,
         | 
| 313 | 
            +
                                    socket.SOCK_STREAM
         | 
| 314 | 
            +
                                )
         | 
| 315 | 
            +
                                sock.settimeout(3)
         | 
| 316 | 
            +
                                sock.connect((host, port))
         | 
| 317 | 
            +
             | 
| 318 | 
            +
                                # LAN или Интернет
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 319 | 
             
                                if storage.is_private(host):
         | 
| 320 | 
             
                                    send_addresses = all_addresses
         | 
| 321 | 
             
                                else:
         | 
| 322 | 
             
                                    send_addresses = [
         | 
| 323 | 
             
                                        a for a in all_addresses
         | 
| 324 | 
            +
                                        if not storage.is_private(storage.parse_hostport(a.split("://", 1)[1])[0])
         | 
| 325 | 
             
                                    ]
         | 
| 326 |  | 
| 327 | 
             
                                handshake = {
         | 
|  | |
| 330 | 
             
                                    "name": agent_name,
         | 
| 331 | 
             
                                    "addresses": send_addresses,
         | 
| 332 | 
             
                                }
         | 
| 333 | 
            +
                                sock.sendall(json.dumps(handshake).encode("utf-8"))
         | 
|  | |
|  | |
| 334 |  | 
|  | |
| 335 | 
             
                                data = sock.recv(64 * 1024)
         | 
| 336 | 
             
                                sock.close()
         | 
| 337 |  | 
|  | |
| 339 | 
             
                                    print(f"[PeerExchange] No data from {host}:{port}")
         | 
| 340 | 
             
                                    continue
         | 
| 341 |  | 
|  | |
|  | |
| 342 | 
             
                                try:
         | 
| 343 | 
             
                                    peers_recv = json.loads(data.decode("utf-8"))
         | 
|  | |
| 344 | 
             
                                    for p in peers_recv:
         | 
| 345 | 
            +
                                        if p.get("id") and p["id"] != my_id:
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 346 | 
             
                                            storage.add_or_update_peer(
         | 
| 347 | 
             
                                                p["id"],
         | 
| 348 | 
             
                                                p.get("name", "unknown"),
         | 
| 349 | 
            +
                                                p.get("addresses", []),
         | 
| 350 | 
            +
                                                "peer_exchange",
         | 
| 351 | 
            +
                                                "online",
         | 
| 352 | 
            +
                                                strict=False
         | 
| 353 | 
             
                                            )
         | 
|  | |
| 354 | 
             
                                    print(f"[PeerExchange] Received {len(peers_recv)} peers from {host}:{port}")
         | 
| 355 | 
             
                                except Exception as e:
         | 
| 356 | 
            +
                                    print(f"[PeerExchange] Decode error from {host}:{port} -> {e}")
         | 
| 357 | 
             
                                    continue
         | 
| 358 |  | 
| 359 | 
             
                                break
         | 
|  | |
| 366 | 
             
            # ---------------------------
         | 
| 367 | 
             
            # TCP Listener (входящие)
         | 
| 368 | 
             
            # ---------------------------
         | 
| 369 | 
            +
            def tcp_listener(sock):
         | 
| 370 | 
            +
                """Слушатель TCP (один сокет на IPv6, работает и для IPv4)."""
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 371 | 
             
                while True:
         | 
| 372 | 
            +
                    try:
         | 
| 373 | 
            +
                        rlist, _, _ = select.select([sock], [], [], 1)
         | 
| 374 | 
            +
                        for s in rlist:
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 375 | 
             
                            try:
         | 
| 376 | 
            +
                                conn, addr = s.accept()
         | 
| 377 | 
            +
                                data = conn.recv(64 * 1024)
         | 
| 378 | 
            +
                                if not data:
         | 
| 379 | 
            +
                                    conn.close()
         | 
| 380 | 
            +
                                    continue
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 381 |  | 
| 382 | 
            +
                                try:
         | 
| 383 | 
            +
                                    msg = json.loads(data.decode("utf-8"))
         | 
| 384 | 
            +
                                except Exception as e:
         | 
| 385 | 
            +
                                    print(f"[TCP Listener] JSON decode error from {addr}: {e}")
         | 
| 386 | 
            +
                                    conn.close()
         | 
| 387 | 
            +
                                    continue
         | 
| 388 |  | 
| 389 | 
            +
                                if msg.get("type") == "PEER_EXCHANGE_REQUEST":
         | 
| 390 | 
            +
                                    peer_id = msg.get("id") or f"did:hmp:{addr[0]}:{addr[1]}"
         | 
| 391 | 
            +
                                    peer_name = msg.get("name", "unknown")
         | 
| 392 | 
            +
                                    raw_addrs = msg.get("addresses", [])
         | 
| 393 | 
            +
                                    pubkey = msg.get("pubkey")
         | 
| 394 | 
            +
             | 
| 395 | 
            +
                                    # Нормализация и подготовка адресов
         | 
| 396 | 
            +
                                    addresses = []
         | 
| 397 | 
            +
                                    for a in raw_addrs:
         | 
| 398 | 
            +
                                        if isinstance(a, dict) and "addr" in a:
         | 
| 399 | 
            +
                                            addresses.append({
         | 
| 400 | 
            +
                                                "addr": storage.normalize_address(a["addr"]),
         | 
| 401 | 
            +
                                                "nonce": a.get("nonce"),
         | 
| 402 | 
            +
                                                "pow_hash": a.get("pow_hash"),
         | 
| 403 | 
            +
                                                "datetime": a.get("datetime")
         | 
| 404 | 
            +
                                            })
         | 
| 405 | 
            +
                                        elif isinstance(a, str):
         | 
| 406 | 
            +
                                            addresses.append({
         | 
| 407 | 
            +
                                                "addr": storage.normalize_address(a),
         | 
| 408 | 
            +
                                                "nonce": None,
         | 
| 409 | 
            +
                                                "pow_hash": None,
         | 
| 410 | 
            +
                                                "datetime": datetime.now(UTC).replace(microsecond=0).isoformat()
         | 
| 411 | 
            +
                                            })
         | 
| 412 |  | 
|  | |
| 413 | 
             
                                    storage.add_or_update_peer(
         | 
| 414 | 
            +
                                        peer_id, peer_name, addresses,
         | 
| 415 | 
            +
                                        source="incoming", status="online",
         | 
| 416 | 
            +
                                        pubkey=pubkey, strict=False
         | 
|  | |
|  | |
| 417 | 
             
                                    )
         | 
| 418 | 
            +
                                    print(f"[TCP Listener] Handshake from {peer_id} ({addr})")
         | 
|  | |
|  | |
|  | |
|  | |
| 419 |  | 
| 420 | 
            +
                                    # LAN или Интернет
         | 
| 421 | 
            +
                                    is_lan = storage.is_private(addr[0])
         | 
|  | |
| 422 |  | 
| 423 | 
            +
                                    # Формируем список пиров для отправки
         | 
| 424 | 
            +
                                    peers_list = []
         | 
| 425 | 
            +
                                    for peer in storage.get_known_peers(my_id, limit=50):
         | 
| 426 | 
            +
                                        pid = peer["id"]
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 427 | 
             
                                        try:
         | 
| 428 | 
            +
                                            peer_addrs = json.loads(peer.get("addresses", "[]"))
         | 
| 429 | 
            +
                                        except:
         | 
| 430 | 
            +
                                            peer_addrs = []
         | 
| 431 | 
            +
             | 
| 432 | 
            +
                                        updated_addresses = []
         | 
| 433 | 
            +
                                        for a in peer_addrs:
         | 
| 434 | 
            +
                                            # Нормализация и проверка
         | 
| 435 | 
            +
                                            addr_norm = storage.normalize_address(a.get("addr") if isinstance(a, dict) else a)
         | 
| 436 | 
            +
                                            if not addr_norm:
         | 
| 437 | 
             
                                                continue
         | 
| 438 | 
            +
                                            proto, hostport = addr_norm.split("://", 1)
         | 
| 439 | 
            +
                                            host, port = storage.parse_hostport(hostport)
         | 
| 440 |  | 
| 441 | 
            +
                                            # Фильтруем приватные адреса при обмене с внешними пирами
         | 
| 442 | 
            +
                                            if not is_lan and storage.is_private(host):
         | 
| 443 | 
             
                                                continue
         | 
| 444 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 445 | 
             
                                            updated_addresses.append({
         | 
| 446 | 
            +
                                                "addr": f"{proto}://{host}:{port}",
         | 
| 447 | 
            +
                                                "nonce": a.get("nonce") if isinstance(a, dict) else None,
         | 
| 448 | 
            +
                                                "pow_hash": a.get("pow_hash") if isinstance(a, dict) else None,
         | 
| 449 | 
            +
                                                "datetime": a.get("datetime") if isinstance(a, dict) else None
         | 
| 450 | 
             
                                            })
         | 
|  | |
|  | |
| 451 |  | 
| 452 | 
            +
                                        peers_list.append({
         | 
| 453 | 
            +
                                            "id": pid,
         | 
| 454 | 
            +
                                            "addresses": updated_addresses,
         | 
| 455 | 
            +
                                            "pubkey": peer.get("pubkey")
         | 
| 456 | 
            +
                                        })
         | 
| 457 |  | 
| 458 | 
            +
                                    conn.sendall(json.dumps(peers_list).encode("utf-8"))
         | 
|  | |
| 459 |  | 
| 460 | 
            +
                                conn.close()
         | 
| 461 | 
            +
                            except Exception as e:
         | 
| 462 | 
            +
                                print(f"[TCP Listener] Connection handling error: {e}")
         | 
| 463 | 
            +
                    except Exception as e:
         | 
| 464 | 
            +
                        print(f"[TCP Listener] select() error: {e}")
         | 
| 465 |  | 
| 466 | 
             
            # ---------------------------
         | 
| 467 | 
             
            # Запуск потоков
         | 
| 468 | 
             
            # ---------------------------
         | 
| 469 | 
             
            def start_sync(bootstrap_file="bootstrap.txt"):
         | 
| 470 | 
             
                load_bootstrap_peers(bootstrap_file)
         | 
| 471 | 
            +
             | 
| 472 | 
            +
                local_ports = list(set(storage.get_local_ports()))
         | 
| 473 | 
             
                print(f"[PeerSync] Local ports: {local_ports}")
         | 
| 474 |  | 
| 475 | 
            +
                for port in local_ports:
         | 
| 476 | 
            +
                    udp_sock, tcp_sock = start_peer_services(port)
         | 
| 477 | 
            +
             | 
| 478 | 
            +
                    threading.Thread(target=udp_discovery, args=(udp_sock, local_ports), daemon=True).start()
         | 
| 479 | 
            +
                    threading.Thread(target=tcp_listener, args=(tcp_sock,), daemon=True).start()
         | 
| 480 | 
            +
             | 
| 481 | 
            +
                threading.Thread(target=tcp_peer_exchange, daemon=True).start()
         | 
    	
        agents/tools/storage.py
    CHANGED
    
    | @@ -977,24 +977,17 @@ class Storage: | |
| 977 |  | 
| 978 | 
             
                # Получаем уникальные локальные порты
         | 
| 979 | 
             
                def get_local_ports(self):
         | 
| 980 | 
            -
                    "" | 
| 981 | 
            -
                     | 
| 982 | 
            -
             | 
| 983 | 
            -
             | 
| 984 | 
            -
             | 
| 985 | 
            -
             | 
| 986 | 
            -
             | 
| 987 | 
            -
             | 
| 988 | 
            -
                    try:
         | 
| 989 | 
            -
                        local_addrs = json.loads(local_addrs_json)
         | 
| 990 | 
            -
                    except Exception:
         | 
| 991 | 
            -
                        print("[WARN] Не удалось разобрать local_addresses из БД")
         | 
| 992 | 
            -
                        return []
         | 
| 993 |  | 
| 994 | 
             
                    ports = []
         | 
| 995 | 
             
                    for entry in local_addrs:
         | 
| 996 | 
            -
                        addr_str = entry | 
| 997 | 
            -
             | 
| 998 | 
             
                        try:
         | 
| 999 | 
             
                            proto, hostport = addr_str.split("://", 1)
         | 
| 1000 | 
             
                            _, port = self.parse_hostport(hostport)
         | 
| @@ -1004,6 +997,25 @@ class Storage: | |
| 1004 |  | 
| 1005 | 
             
                    return ports
         | 
| 1006 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 1007 | 
             
                # Нормализация DID
         | 
| 1008 | 
             
                @staticmethod
         | 
| 1009 | 
             
                def normalize_did(did: str) -> str:
         | 
| @@ -1025,15 +1037,14 @@ class Storage: | |
| 1025 | 
             
                    self, peer_id, name, addresses,
         | 
| 1026 | 
             
                    source="discovery", status="unknown",
         | 
| 1027 | 
             
                    pubkey=None, capabilities=None,
         | 
| 1028 | 
            -
                    heard_from=None
         | 
| 1029 | 
             
                ):
         | 
| 1030 | 
             
                    c = self.conn.cursor()
         | 
| 1031 |  | 
| 1032 | 
            -
                    # нормализуем входные адреса
         | 
| 1033 | 
             
                    norm_addresses = []
         | 
| 1034 | 
             
                    for a in (addresses or []):
         | 
| 1035 | 
             
                        if isinstance(a, dict) and "addr" in a:
         | 
| 1036 | 
            -
                            # нормализация datetime: ISO 8601 без микросекунд
         | 
| 1037 | 
             
                            dt_raw = a.get("datetime")
         | 
| 1038 | 
             
                            if dt_raw:
         | 
| 1039 | 
             
                                try:
         | 
| @@ -1059,7 +1070,7 @@ class Storage: | |
| 1059 | 
             
                                "datetime": datetime.now(timezone.utc).replace(microsecond=0).isoformat()
         | 
| 1060 | 
             
                            })
         | 
| 1061 |  | 
| 1062 | 
            -
                    # получаем существующую запись
         | 
| 1063 | 
             
                    existing_addresses = []
         | 
| 1064 | 
             
                    existing_pubkey = None
         | 
| 1065 | 
             
                    existing_capabilities = {}
         | 
| @@ -1086,38 +1097,48 @@ class Storage: | |
| 1086 | 
             
                    final_capabilities = capabilities or existing_capabilities
         | 
| 1087 | 
             
                    combined_heard_from = list(set(existing_heard_from + (heard_from or [])))
         | 
| 1088 |  | 
| 1089 | 
            -
                    #  | 
| 1090 | 
            -
                    if  | 
| 1091 | 
            -
                         | 
| 1092 | 
            -
                         | 
| 1093 | 
            -
             | 
| 1094 | 
            -
             | 
| 1095 | 
            -
             | 
| 1096 | 
            -
             | 
| 1097 | 
            -
             | 
| 1098 | 
            -
                         | 
| 1099 | 
            -
                         | 
| 1100 | 
            -
             | 
| 1101 | 
            -
             | 
| 1102 | 
            -
             | 
| 1103 | 
            -
             | 
| 1104 | 
            -
             | 
| 1105 | 
            -
                             | 
| 1106 | 
            -
             | 
| 1107 | 
            -
                                 | 
| 1108 | 
            -
             | 
| 1109 | 
            -
             | 
| 1110 | 
            -
             | 
| 1111 | 
            -
                             | 
| 1112 | 
            -
                            if  | 
| 1113 | 
            -
                                 | 
| 1114 | 
            -
             | 
| 1115 | 
            -
             | 
| 1116 | 
            -
             | 
| 1117 | 
            -
             | 
| 1118 | 
            -
             | 
| 1119 | 
            -
             | 
| 1120 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 1121 | 
             
                    c.execute("""
         | 
| 1122 | 
             
                        INSERT INTO agent_peers (id, name, addresses, source, status, last_seen, pubkey, capabilities, heard_from)
         | 
| 1123 | 
             
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
         | 
|  | |
| 977 |  | 
| 978 | 
             
                # Получаем уникальные локальные порты
         | 
| 979 | 
             
                def get_local_ports(self):
         | 
| 980 | 
            +
                    local_addrs = self.get_config_value("local_addresses", [])
         | 
| 981 | 
            +
                    if not isinstance(local_addrs, list):
         | 
| 982 | 
            +
                        try:
         | 
| 983 | 
            +
                            local_addrs = json.loads(local_addrs)
         | 
| 984 | 
            +
                        except Exception:
         | 
| 985 | 
            +
                            print("[WARN] Не удалось разобрать local_addresses из БД")
         | 
| 986 | 
            +
                            return []
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 987 |  | 
| 988 | 
             
                    ports = []
         | 
| 989 | 
             
                    for entry in local_addrs:
         | 
| 990 | 
            +
                        addr_str = entry.get("addr") or entry.get("address") if isinstance(entry, dict) else entry
         | 
|  | |
| 991 | 
             
                        try:
         | 
| 992 | 
             
                            proto, hostport = addr_str.split("://", 1)
         | 
| 993 | 
             
                            _, port = self.parse_hostport(hostport)
         | 
|  | |
| 997 |  | 
| 998 | 
             
                    return ports
         | 
| 999 |  | 
| 1000 | 
            +
                # Получить локальные или глобальные адреса
         | 
| 1001 | 
            +
                def get_addresses(self, which="local"):
         | 
| 1002 | 
            +
                    key = f"{which}_addresses"
         | 
| 1003 | 
            +
                    addrs = self.get_config_value(key, [])
         | 
| 1004 | 
            +
                    if not isinstance(addrs, list):
         | 
| 1005 | 
            +
                        try:
         | 
| 1006 | 
            +
                            addrs = json.loads(addrs)
         | 
| 1007 | 
            +
                        except Exception:
         | 
| 1008 | 
            +
                            print(f"[WARN] Не удалось разобрать {key} из БД")
         | 
| 1009 | 
            +
                            return []
         | 
| 1010 | 
            +
             | 
| 1011 | 
            +
                    result = []
         | 
| 1012 | 
            +
                    for entry in addrs:
         | 
| 1013 | 
            +
                        if isinstance(entry, dict):
         | 
| 1014 | 
            +
                            result.append(entry.get("addr") or entry.get("address"))
         | 
| 1015 | 
            +
                        elif isinstance(entry, str):
         | 
| 1016 | 
            +
                            result.append(entry)
         | 
| 1017 | 
            +
                    return result
         | 
| 1018 | 
            +
             | 
| 1019 | 
             
                # Нормализация DID
         | 
| 1020 | 
             
                @staticmethod
         | 
| 1021 | 
             
                def normalize_did(did: str) -> str:
         | 
|  | |
| 1037 | 
             
                    self, peer_id, name, addresses,
         | 
| 1038 | 
             
                    source="discovery", status="unknown",
         | 
| 1039 | 
             
                    pubkey=None, capabilities=None,
         | 
| 1040 | 
            +
                    heard_from=None, strict: bool = True
         | 
| 1041 | 
             
                ):
         | 
| 1042 | 
             
                    c = self.conn.cursor()
         | 
| 1043 |  | 
| 1044 | 
            +
                    # --- нормализуем входные адреса ---
         | 
| 1045 | 
             
                    norm_addresses = []
         | 
| 1046 | 
             
                    for a in (addresses or []):
         | 
| 1047 | 
             
                        if isinstance(a, dict) and "addr" in a:
         | 
|  | |
| 1048 | 
             
                            dt_raw = a.get("datetime")
         | 
| 1049 | 
             
                            if dt_raw:
         | 
| 1050 | 
             
                                try:
         | 
|  | |
| 1070 | 
             
                                "datetime": datetime.now(timezone.utc).replace(microsecond=0).isoformat()
         | 
| 1071 | 
             
                            })
         | 
| 1072 |  | 
| 1073 | 
            +
                    # --- получаем существующую запись ---
         | 
| 1074 | 
             
                    existing_addresses = []
         | 
| 1075 | 
             
                    existing_pubkey = None
         | 
| 1076 | 
             
                    existing_capabilities = {}
         | 
|  | |
| 1097 | 
             
                    final_capabilities = capabilities or existing_capabilities
         | 
| 1098 | 
             
                    combined_heard_from = list(set(existing_heard_from + (heard_from or [])))
         | 
| 1099 |  | 
| 1100 | 
            +
                    # --- строгий режим ---
         | 
| 1101 | 
            +
                    if strict:
         | 
| 1102 | 
            +
                        # Проверка pubkey
         | 
| 1103 | 
            +
                        if existing_pubkey and pubkey and existing_pubkey != pubkey:
         | 
| 1104 | 
            +
                            print(f"[WARN] Peer {peer_id} pubkey mismatch! Possible impersonation attempt.")
         | 
| 1105 | 
            +
                            return
         | 
| 1106 | 
            +
                        final_pubkey = existing_pubkey or pubkey
         | 
| 1107 | 
            +
             | 
| 1108 | 
            +
                        # Объединяем адреса по addr, проверяем PoW и datetime
         | 
| 1109 | 
            +
                        addr_map = {a["addr"]: a for a in existing_addresses if isinstance(a, dict)}
         | 
| 1110 | 
            +
                        for a in norm_addresses:
         | 
| 1111 | 
            +
                            addr = a["addr"]
         | 
| 1112 | 
            +
                            nonce = a.get("nonce")
         | 
| 1113 | 
            +
                            pow_hash = a.get("pow_hash")
         | 
| 1114 | 
            +
                            dt = a.get("datetime")
         | 
| 1115 | 
            +
             | 
| 1116 | 
            +
                            # проверка PoW
         | 
| 1117 | 
            +
                            if nonce is not None and pow_hash is not None:
         | 
| 1118 | 
            +
                                if not self.verify_pow(peer_id, final_pubkey, addr, nonce, pow_hash, dt):
         | 
| 1119 | 
            +
                                    print(f"[WARN] Peer {peer_id} address {addr} failed PoW validation")
         | 
| 1120 | 
            +
                                    continue
         | 
| 1121 | 
            +
             | 
| 1122 | 
            +
                            # проверка актуальности datetime
         | 
| 1123 | 
            +
                            if addr in addr_map:
         | 
| 1124 | 
            +
                                old_dt = addr_map[addr].get("datetime")
         | 
| 1125 | 
            +
                                if old_dt and dt <= old_dt:
         | 
| 1126 | 
            +
                                    continue
         | 
| 1127 | 
            +
             | 
| 1128 | 
            +
                            addr_map[addr] = {"addr": addr, "nonce": nonce, "pow_hash": pow_hash, "datetime": dt}
         | 
| 1129 | 
            +
             | 
| 1130 | 
            +
                        combined_addresses = list(addr_map.values())
         | 
| 1131 | 
            +
             | 
| 1132 | 
            +
                    # --- упрощённый режим ---
         | 
| 1133 | 
            +
                    else:
         | 
| 1134 | 
            +
                        final_pubkey = existing_pubkey or pubkey
         | 
| 1135 | 
            +
                        addr_map = {a["addr"]: a for a in existing_addresses if isinstance(a, dict)}
         | 
| 1136 | 
            +
                        for a in norm_addresses:
         | 
| 1137 | 
            +
                            # просто перезаписываем адреса без PoW и datetime проверки
         | 
| 1138 | 
            +
                            addr_map[a["addr"]] = a
         | 
| 1139 | 
            +
                        combined_addresses = list(addr_map.values())
         | 
| 1140 | 
            +
             | 
| 1141 | 
            +
                    # --- запись в БД ---
         | 
| 1142 | 
             
                    c.execute("""
         | 
| 1143 | 
             
                        INSERT INTO agent_peers (id, name, addresses, source, status, last_seen, pubkey, capabilities, heard_from)
         | 
| 1144 | 
             
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
         | 
    	
        assets/logo-hand-small.png
    ADDED
    
    |   | 
| Git LFS Details
 | 
