GitHub Action
commited on
Commit
·
ab3306e
1
Parent(s):
bcc313a
Sync from GitHub with Git LFS
Browse files- agents/bootstrap.txt +0 -4
- agents/examples/bootstrap.txt +4 -0
- agents/init.py +59 -9
- agents/peer_sync.py +86 -21
- agents/tools/db_structure.sql +1 -3
- agents/tools/storage.py +57 -30
agents/bootstrap.txt
CHANGED
@@ -1,4 +0,0 @@
|
|
1 |
-
did:hmp:ac0e063e-8609-4ef9-75f8-d7dcafc65977 ["tcp://node1.mesh.local:8000","udp://node1.mesh.local:8030"]
|
2 |
-
did:hmp:ac0e063e-8709-4ef9-75f8-d7dcafc65977 ["tcp://node2.mesh.local:8010"]
|
3 |
-
did:hmp:ac0e063e-8609-4ff9-75f8-d7dc46c65977 ["tcp://node3.mesh.local:8020"]
|
4 |
-
did:hmp:ac0f053e-8609-4ef9-75f8-d7dcafc65977 ["any://node4.mesh.local:8000"]
|
|
|
|
|
|
|
|
|
|
agents/examples/bootstrap.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
did:hmp:ac0e063e-8609-4ef9-75f8-d7dcafc65977 ["tcp://node1.mesh.local:8000","udp://node1.mesh.local:8030"] "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA+J7pfdbWotIo31Omd0b49bIFAc6nCZpriQc9BdbZ2TQ=\n-----END PUBLIC KEY-----\n" 12166 "0000967e52fdbeb393ec9e4376cf32605209da69fabc590607cea0574273e470"
|
2 |
+
did:hmp:ac0e063e-8709-4ef9-75f8-d7dcafc65977 ["tcp://node2.mesh.local:8010"] "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA6OZO4dJ9IZethuM+8f+m8WVRXwMsCPOYaNrwcaA1a08=\n-----END PUBLIC KEY-----\n" 82022 "00004a512b13ca5084fa913e890578befe1068ac06d3dba541589822a90a36c6"
|
3 |
+
did:hmp:ac0e063e-8609-4ff9-75f8-d7dc46c65977 ["tcp://node3.mesh.local:8020"] "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAAGTXI+KznuhV1N9+sb9/FHQndgmw2Ro2dAvwpRBrG78=\n-----END PUBLIC KEY-----\n" 47803 "0000beb657e738dabc9731eb911f45f6706fba7a8bc342583d022b004ce97921"
|
4 |
+
did:hmp:ac0f053e-8609-4ef9-75f8-d7dcafc65977 ["any://node4.mesh.local:8000"] "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA3HR98+9G+MbrAuMKx5mhGKccOlWdre9A+OKE1W0gMa4=\n-----END PUBLIC KEY-----\n" 13549 "00008b71f46aa0f0c9e3f15a6b32a24ef58a1642936e324a548b5aac97e989d6"
|
agents/init.py
CHANGED
@@ -48,11 +48,7 @@ def init_identity(storage, config):
|
|
48 |
privkey, pubkey = generate_keypair(method="ed25519")
|
49 |
privkey, pubkey = privkey.decode(), pubkey.decode()
|
50 |
|
51 |
-
# 3.
|
52 |
-
addresses = config.get("local_addresses", [])
|
53 |
-
nonce, pow_hash = storage.generate_pow(identity_id, pubkey, addresses, difficulty=4)
|
54 |
-
|
55 |
-
# 4. Создать запись в identity
|
56 |
identity = {
|
57 |
"id": identity_id,
|
58 |
"name": config.get("agent_name", "Unnamed"),
|
@@ -64,16 +60,14 @@ def init_identity(storage, config):
|
|
64 |
}
|
65 |
storage.add_identity(identity)
|
66 |
|
67 |
-
#
|
68 |
config["agent_id"] = did
|
69 |
config["identity_agent"] = identity_id
|
70 |
config["pubkey"] = pubkey
|
71 |
config["privkey"] = privkey
|
72 |
-
config["pow_nonce"] = nonce
|
73 |
-
config["pow_hash"] = pow_hash
|
74 |
|
75 |
save_config(CONFIG_PATH, config)
|
76 |
-
print(f"[+] Создана личность: {identity_id}
|
77 |
else:
|
78 |
print("[=] agent_id уже задан, пропускаем генерацию DiD.")
|
79 |
|
@@ -132,10 +126,65 @@ def init_config_table(storage, config):
|
|
132 |
else:
|
133 |
flat_config[addr_key] = expand_addresses(value)
|
134 |
|
|
|
135 |
for key, value in flat_config.items():
|
136 |
storage.set_config(key, json.dumps(value))
|
137 |
print("[+] Конфигурация сохранена в БД.")
|
138 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
def init_prompts_and_ethics():
|
140 |
folder = os.path.dirname(__file__)
|
141 |
prompt_files = [
|
@@ -246,6 +295,7 @@ def ensure_db_initialized():
|
|
246 |
init_user(storage, config)
|
247 |
init_llm_backends(storage, config)
|
248 |
init_config_table(storage, config)
|
|
|
249 |
save_config(CONFIG_PATH, config)
|
250 |
init_prompts_and_ethics()
|
251 |
except Exception as e:
|
|
|
48 |
privkey, pubkey = generate_keypair(method="ed25519")
|
49 |
privkey, pubkey = privkey.decode(), pubkey.decode()
|
50 |
|
51 |
+
# 3. Создать запись в identity
|
|
|
|
|
|
|
|
|
52 |
identity = {
|
53 |
"id": identity_id,
|
54 |
"name": config.get("agent_name", "Unnamed"),
|
|
|
60 |
}
|
61 |
storage.add_identity(identity)
|
62 |
|
63 |
+
# 4. Записать в config
|
64 |
config["agent_id"] = did
|
65 |
config["identity_agent"] = identity_id
|
66 |
config["pubkey"] = pubkey
|
67 |
config["privkey"] = privkey
|
|
|
|
|
68 |
|
69 |
save_config(CONFIG_PATH, config)
|
70 |
+
print(f"[+] Создана личность: {identity_id}.")
|
71 |
else:
|
72 |
print("[=] agent_id уже задан, пропускаем генерацию DiD.")
|
73 |
|
|
|
126 |
else:
|
127 |
flat_config[addr_key] = expand_addresses(value)
|
128 |
|
129 |
+
# Сохраняем конфиг в БД
|
130 |
for key, value in flat_config.items():
|
131 |
storage.set_config(key, json.dumps(value))
|
132 |
print("[+] Конфигурация сохранена в БД.")
|
133 |
|
134 |
+
def update_pow_for_addresses(storage, difficulty=4):
|
135 |
+
raw_id = storage.get_config_value("agent_id")
|
136 |
+
if not raw_id:
|
137 |
+
print("[-] Нет agent_id в config — пропуск обновления PoW.")
|
138 |
+
return
|
139 |
+
agent_id = storage.normalize_did(raw_id)
|
140 |
+
pubkey = storage.get_config_value("pubkey")
|
141 |
+
|
142 |
+
if not agent_id or not pubkey:
|
143 |
+
print("[-] Нет agent_id/pubkey в config — пропуск обновления PoW.")
|
144 |
+
return
|
145 |
+
|
146 |
+
for addr_key in ("local_addresses", "global_addresses"):
|
147 |
+
raw = storage.get_config_value(addr_key)
|
148 |
+
if not raw:
|
149 |
+
continue
|
150 |
+
|
151 |
+
# raw может быть либо JSON-строкой, либо уже list
|
152 |
+
if isinstance(raw, str):
|
153 |
+
try:
|
154 |
+
addresses = json.loads(raw)
|
155 |
+
except Exception as e:
|
156 |
+
print(f"[!] Ошибка при чтении {addr_key}: {e}")
|
157 |
+
continue
|
158 |
+
else:
|
159 |
+
addresses = raw
|
160 |
+
|
161 |
+
enriched = []
|
162 |
+
for addr in addresses:
|
163 |
+
# если уже dict → PoW записан
|
164 |
+
if isinstance(addr, dict):
|
165 |
+
enriched.append(addr)
|
166 |
+
continue
|
167 |
+
|
168 |
+
nonce, hash_value = storage.generate_pow(
|
169 |
+
peer_id=agent_id,
|
170 |
+
pubkey=pubkey,
|
171 |
+
address=addr,
|
172 |
+
difficulty=difficulty
|
173 |
+
)
|
174 |
+
enriched.append({
|
175 |
+
"address": addr,
|
176 |
+
"expires": "",
|
177 |
+
"pow": {
|
178 |
+
"nonce": nonce,
|
179 |
+
"hash": hash_value,
|
180 |
+
"difficulty": difficulty
|
181 |
+
}
|
182 |
+
})
|
183 |
+
|
184 |
+
storage.set_config(addr_key, json.dumps(enriched))
|
185 |
+
|
186 |
+
print("[+] Адреса обновлены с PoW.")
|
187 |
+
|
188 |
def init_prompts_and_ethics():
|
189 |
folder = os.path.dirname(__file__)
|
190 |
prompt_files = [
|
|
|
295 |
init_user(storage, config)
|
296 |
init_llm_backends(storage, config)
|
297 |
init_config_table(storage, config)
|
298 |
+
update_pow_for_addresses(storage, difficulty=4)
|
299 |
save_config(CONFIG_PATH, config)
|
300 |
init_prompts_and_ethics()
|
301 |
except Exception as e:
|
agents/peer_sync.py
CHANGED
@@ -24,13 +24,35 @@ global_addresses = storage.get_config_value("global_addresses", [])
|
|
24 |
all_addresses = local_addresses + global_addresses # один раз
|
25 |
|
26 |
# Получаем уникальные локальные порты
|
27 |
-
def get_local_ports():
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
|
35 |
local_ports = get_local_ports()
|
36 |
print(f"[PeerSync] Local ports: {local_ports}")
|
@@ -38,12 +60,7 @@ print(f"[PeerSync] Local ports: {local_ports}")
|
|
38 |
# ---------------------------
|
39 |
# Загрузка bootstrap
|
40 |
# ---------------------------
|
41 |
-
|
42 |
def load_bootstrap_peers(filename="bootstrap.txt"):
|
43 |
-
"""
|
44 |
-
Читает bootstrap.txt и добавляет узлы в storage.
|
45 |
-
Формат строки: did [JSON-список адресов]
|
46 |
-
"""
|
47 |
try:
|
48 |
with open(filename, "r", encoding="utf-8") as f:
|
49 |
lines = f.readlines()
|
@@ -56,19 +73,57 @@ def load_bootstrap_peers(filename="bootstrap.txt"):
|
|
56 |
if not line or line.startswith("#"):
|
57 |
continue
|
58 |
|
59 |
-
|
60 |
-
|
61 |
-
|
|
|
62 |
continue
|
63 |
-
|
64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
try:
|
66 |
addresses = json.loads(addresses_json)
|
67 |
except Exception as e:
|
68 |
-
print(f"[Bootstrap]
|
69 |
continue
|
|
|
70 |
|
71 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
expanded_addresses = []
|
73 |
for addr in addresses:
|
74 |
if addr.startswith("any://"):
|
@@ -78,8 +133,18 @@ def load_bootstrap_peers(filename="bootstrap.txt"):
|
|
78 |
else:
|
79 |
expanded_addresses.append(addr)
|
80 |
|
81 |
-
storage.add_or_update_peer(
|
82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
83 |
print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
|
84 |
|
85 |
# ---------------------------
|
|
|
24 |
all_addresses = local_addresses + global_addresses # один раз
|
25 |
|
26 |
# Получаем уникальные локальные порты
|
27 |
+
def get_local_ports(storage):
|
28 |
+
"""
|
29 |
+
Возвращает список портов для всех локальных адресов.
|
30 |
+
Формат конфигурации: список dict {"addr": str, "nonce": int, "pow_hash": str, "expires": ...}
|
31 |
+
"""
|
32 |
+
local_addrs_json = storage.get_config("local_addresses")
|
33 |
+
if not local_addrs_json:
|
34 |
+
return []
|
35 |
+
|
36 |
+
try:
|
37 |
+
local_addrs = json.loads(local_addrs_json)
|
38 |
+
except:
|
39 |
+
print("[WARN] Не удалось разобрать local_addresses из БД")
|
40 |
+
return []
|
41 |
+
|
42 |
+
ports = []
|
43 |
+
for entry in local_addrs:
|
44 |
+
# Если entry — словарь, берём поле addr, иначе предполагаем строку
|
45 |
+
addr_str = entry["addr"] if isinstance(entry, dict) else entry
|
46 |
+
|
47 |
+
# Разбираем протокол и host:port
|
48 |
+
try:
|
49 |
+
proto, hostport = addr_str.split("://", 1)
|
50 |
+
_, port = storage.parse_hostport(hostport)
|
51 |
+
ports.append(port)
|
52 |
+
except Exception as e:
|
53 |
+
print(f"[WARN] Не удалось разобрать адрес {addr_str}: {e}")
|
54 |
+
|
55 |
+
return ports
|
56 |
|
57 |
local_ports = get_local_ports()
|
58 |
print(f"[PeerSync] Local ports: {local_ports}")
|
|
|
60 |
# ---------------------------
|
61 |
# Загрузка bootstrap
|
62 |
# ---------------------------
|
|
|
63 |
def load_bootstrap_peers(filename="bootstrap.txt"):
|
|
|
|
|
|
|
|
|
64 |
try:
|
65 |
with open(filename, "r", encoding="utf-8") as f:
|
66 |
lines = f.readlines()
|
|
|
73 |
if not line or line.startswith("#"):
|
74 |
continue
|
75 |
|
76 |
+
# 1. DID
|
77 |
+
did_end = line.find(" ")
|
78 |
+
if did_end == -1:
|
79 |
+
print(f"[Bootstrap] Invalid line (no DID): {line}")
|
80 |
continue
|
81 |
+
did = line[:did_end]
|
82 |
+
rest = line[did_end + 1:].strip()
|
83 |
+
|
84 |
+
# 2. JSON-адреса
|
85 |
+
addr_start = rest.find("[")
|
86 |
+
addr_end = rest.find("]") + 1
|
87 |
+
if addr_start == -1 or addr_end == 0:
|
88 |
+
print(f"[Bootstrap] Invalid JSON addresses: {line}")
|
89 |
+
continue
|
90 |
+
addresses_json = rest[addr_start:addr_end]
|
91 |
try:
|
92 |
addresses = json.loads(addresses_json)
|
93 |
except Exception as e:
|
94 |
+
print(f"[Bootstrap] Failed to parse JSON addresses: {line} ({e})")
|
95 |
continue
|
96 |
+
rest = rest[addr_end:].strip()
|
97 |
|
98 |
+
# 3. pubkey (в кавычках)
|
99 |
+
pub_start = rest.find('"')
|
100 |
+
pub_end = rest.find('"', pub_start + 1)
|
101 |
+
if pub_start == -1 or pub_end == -1:
|
102 |
+
print(f"[Bootstrap] Invalid pubkey: {line}")
|
103 |
+
continue
|
104 |
+
pubkey = rest[pub_start + 1:pub_end].replace("\\n", "\n")
|
105 |
+
rest = rest[pub_end + 1:].strip()
|
106 |
+
|
107 |
+
# 4. pow_nonce
|
108 |
+
nonce_end = rest.find(" ")
|
109 |
+
if nonce_end == -1:
|
110 |
+
print(f"[Bootstrap] Invalid pow_nonce: {line}")
|
111 |
+
continue
|
112 |
+
try:
|
113 |
+
pow_nonce = int(rest[:nonce_end])
|
114 |
+
except ValueError:
|
115 |
+
print(f"[Bootstrap] Invalid pow_nonce: {rest[:nonce_end]} in line: {line}")
|
116 |
+
continue
|
117 |
+
rest = rest[nonce_end:].strip()
|
118 |
+
|
119 |
+
# 5. pow_hash (в кавычках)
|
120 |
+
if rest.startswith('"') and rest.endswith('"'):
|
121 |
+
pow_hash = rest[1:-1]
|
122 |
+
else:
|
123 |
+
print(f"[Bootstrap] Invalid pow_hash: {line}")
|
124 |
+
continue
|
125 |
+
|
126 |
+
# Разворачиваем any://
|
127 |
expanded_addresses = []
|
128 |
for addr in addresses:
|
129 |
if addr.startswith("any://"):
|
|
|
133 |
else:
|
134 |
expanded_addresses.append(addr)
|
135 |
|
136 |
+
storage.add_or_update_peer(
|
137 |
+
peer_id=did,
|
138 |
+
name=None,
|
139 |
+
addresses=expanded_addresses,
|
140 |
+
source="bootstrap",
|
141 |
+
status="offline",
|
142 |
+
pubkey=pubkey,
|
143 |
+
capabilities=None,
|
144 |
+
pow_nonce=pow_nonce,
|
145 |
+
pow_hash=pow_hash
|
146 |
+
)
|
147 |
+
|
148 |
print(f"[Bootstrap] Loaded peer {did} -> {expanded_addresses}")
|
149 |
|
150 |
# ---------------------------
|
agents/tools/db_structure.sql
CHANGED
@@ -151,7 +151,7 @@ CREATE TABLE IF NOT EXISTS llm_recent_responses (
|
|
151 |
CREATE TABLE IF NOT EXISTS agent_peers (
|
152 |
id TEXT PRIMARY KEY, -- Уникальный идентификатор (UUID или псевдоним)
|
153 |
name TEXT, -- Имя агента
|
154 |
-
addresses TEXT, -- Адреса для связи (JSON)
|
155 |
tags TEXT, -- Теги (Postman, Friend и т.д.)
|
156 |
status TEXT DEFAULT 'unknown', -- online | offline | untrusted | blacklisted и др.
|
157 |
source TEXT, -- bootstrap | discovery | exchange
|
@@ -159,8 +159,6 @@ CREATE TABLE IF NOT EXISTS agent_peers (
|
|
159 |
description TEXT, -- Описание агента
|
160 |
capabilities TEXT, -- Возможности (JSON)
|
161 |
pubkey TEXT, -- Публичный ключ
|
162 |
-
pow_nonce INTEGER, -- Nonce для PoW
|
163 |
-
pow_hash TEXT, -- Контрольный хеш PoW (например, sha256(pubkey + addresses + nonce))
|
164 |
heard_from TEXT, -- JSON список DID, от кого агент о нем узнал
|
165 |
software_info TEXT, -- Информация о ПО агента (JSON)
|
166 |
registered_at DATETIME DEFAULT CURRENT_TIMESTAMP -- Время регистрации
|
|
|
151 |
CREATE TABLE IF NOT EXISTS agent_peers (
|
152 |
id TEXT PRIMARY KEY, -- Уникальный идентификатор (UUID или псевдоним)
|
153 |
name TEXT, -- Имя агента
|
154 |
+
addresses TEXT, -- Адреса для связи (JSON), каждый адрес содержит addr, nonce, pow_hash, expiries
|
155 |
tags TEXT, -- Теги (Postman, Friend и т.д.)
|
156 |
status TEXT DEFAULT 'unknown', -- online | offline | untrusted | blacklisted и др.
|
157 |
source TEXT, -- bootstrap | discovery | exchange
|
|
|
159 |
description TEXT, -- Описание агента
|
160 |
capabilities TEXT, -- Возможности (JSON)
|
161 |
pubkey TEXT, -- Публичный ключ
|
|
|
|
|
162 |
heard_from TEXT, -- JSON список DID, от кого агент о нем узнал
|
163 |
software_info TEXT, -- Информация о ПО агента (JSON)
|
164 |
registered_at DATETIME DEFAULT CURRENT_TIMESTAMP -- Время регистрации
|
agents/tools/storage.py
CHANGED
@@ -677,11 +677,15 @@ class Storage:
|
|
677 |
self.conn.commit()
|
678 |
return cursor.lastrowid
|
679 |
|
680 |
-
def generate_pow(self, peer_id, pubkey,
|
|
|
|
|
|
|
681 |
nonce = 0
|
682 |
prefix = "0" * difficulty
|
|
|
683 |
while True:
|
684 |
-
base = f"{peer_id}{pubkey}{
|
685 |
h = hashlib.sha256(base).hexdigest()
|
686 |
if h.startswith(prefix):
|
687 |
return nonce, h
|
@@ -963,9 +967,19 @@ class Storage:
|
|
963 |
|
964 |
return f"{proto}://{host}:{port}" if port else f"{proto}://{host}"
|
965 |
|
|
|
|
|
|
|
|
|
|
|
966 |
# Работа с пирам (agent_peers)
|
967 |
-
|
968 |
-
|
|
|
|
|
|
|
|
|
|
|
969 |
h = hashlib.sha256(base).hexdigest()
|
970 |
return h == pow_hash and h.startswith("0" * difficulty)
|
971 |
|
@@ -973,14 +987,24 @@ class Storage:
|
|
973 |
self, peer_id, name, addresses,
|
974 |
source="discovery", status="unknown",
|
975 |
pubkey=None, capabilities=None,
|
976 |
-
pow_nonce=None, pow_hash=None,
|
977 |
heard_from=None
|
978 |
):
|
979 |
c = self.conn.cursor()
|
980 |
|
981 |
-
#
|
982 |
-
|
983 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
984 |
existing_addresses = []
|
985 |
existing_pubkey = None
|
986 |
existing_capabilities = {}
|
@@ -1004,37 +1028,42 @@ class Storage:
|
|
1004 |
except:
|
1005 |
existing_heard_from = []
|
1006 |
|
1007 |
-
# объединяем адреса
|
1008 |
-
combined_addresses = list({self.normalize_address(a) for a in (*existing_addresses, *addresses)})
|
1009 |
-
final_pubkey = pubkey or existing_pubkey
|
1010 |
final_capabilities = capabilities or existing_capabilities
|
1011 |
-
|
1012 |
-
# обновляем heard_from
|
1013 |
combined_heard_from = list(set(existing_heard_from + (heard_from or [])))
|
1014 |
|
1015 |
-
#
|
1016 |
-
if
|
1017 |
-
|
1018 |
-
|
1019 |
-
|
1020 |
-
|
1021 |
-
|
1022 |
-
|
1023 |
-
|
1024 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1025 |
c.execute("""
|
1026 |
-
INSERT INTO agent_peers (id, name, addresses, source, status, last_seen, pubkey, capabilities,
|
1027 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
1028 |
ON CONFLICT(id) DO UPDATE SET
|
1029 |
name=excluded.name,
|
1030 |
addresses=excluded.addresses,
|
1031 |
source=excluded.source,
|
1032 |
status=excluded.status,
|
1033 |
last_seen=excluded.last_seen,
|
1034 |
-
pubkey=excluded.pubkey,
|
1035 |
capabilities=excluded.capabilities,
|
1036 |
-
pow_nonce=excluded.pow_nonce,
|
1037 |
-
pow_hash=excluded.pow_hash,
|
1038 |
heard_from=excluded.heard_from
|
1039 |
""", (
|
1040 |
peer_id,
|
@@ -1045,8 +1074,6 @@ class Storage:
|
|
1045 |
datetime.now(UTC).isoformat(),
|
1046 |
final_pubkey,
|
1047 |
json.dumps(final_capabilities),
|
1048 |
-
pow_nonce,
|
1049 |
-
pow_hash,
|
1050 |
json.dumps(combined_heard_from)
|
1051 |
))
|
1052 |
self.conn.commit()
|
|
|
677 |
self.conn.commit()
|
678 |
return cursor.lastrowid
|
679 |
|
680 |
+
def generate_pow(self, peer_id, pubkey, address, expires=None, difficulty=4):
|
681 |
+
"""
|
682 |
+
Генерирует PoW для одной пары (peer_id + pubkey + address + expires).
|
683 |
+
"""
|
684 |
nonce = 0
|
685 |
prefix = "0" * difficulty
|
686 |
+
expires_str = str(expires) if expires is not None else ""
|
687 |
while True:
|
688 |
+
base = f"{peer_id}{pubkey}{address}{expires_str}{nonce}".encode()
|
689 |
h = hashlib.sha256(base).hexdigest()
|
690 |
if h.startswith(prefix):
|
691 |
return nonce, h
|
|
|
967 |
|
968 |
return f"{proto}://{host}:{port}" if port else f"{proto}://{host}"
|
969 |
|
970 |
+
# Нормализация DID
|
971 |
+
@staticmethod
|
972 |
+
def normalize_did(did: str) -> str:
|
973 |
+
return did.strip().strip('"').strip("'")
|
974 |
+
|
975 |
# Работа с пирам (agent_peers)
|
976 |
+
@staticmethod
|
977 |
+
def verify_pow(peer_id, pubkey, address, nonce, pow_hash, expires=None, difficulty=4):
|
978 |
+
"""
|
979 |
+
Проверяет PoW для одной пары (peer_id + pubkey + address + expires).
|
980 |
+
"""
|
981 |
+
expires_str = str(expires) if expires is not None else ""
|
982 |
+
base = f"{peer_id}{pubkey}{address}{expires_str}{nonce}".encode()
|
983 |
h = hashlib.sha256(base).hexdigest()
|
984 |
return h == pow_hash and h.startswith("0" * difficulty)
|
985 |
|
|
|
987 |
self, peer_id, name, addresses,
|
988 |
source="discovery", status="unknown",
|
989 |
pubkey=None, capabilities=None,
|
|
|
990 |
heard_from=None
|
991 |
):
|
992 |
c = self.conn.cursor()
|
993 |
|
994 |
+
# нормализуем входные адреса
|
995 |
+
norm_addresses = []
|
996 |
+
for a in (addresses or []):
|
997 |
+
if isinstance(a, dict) and "addr" in a:
|
998 |
+
norm_addresses.append({
|
999 |
+
"addr": self.normalize_address(a["addr"]),
|
1000 |
+
"nonce": a.get("nonce"),
|
1001 |
+
"pow_hash": a.get("pow_hash"),
|
1002 |
+
"expires": a.get("expires")
|
1003 |
+
})
|
1004 |
+
elif isinstance(a, str):
|
1005 |
+
norm_addresses.append({"addr": self.normalize_address(a), "nonce": None, "pow_hash": None, "expires": None})
|
1006 |
+
|
1007 |
+
# получаем существующую запись
|
1008 |
existing_addresses = []
|
1009 |
existing_pubkey = None
|
1010 |
existing_capabilities = {}
|
|
|
1028 |
except:
|
1029 |
existing_heard_from = []
|
1030 |
|
|
|
|
|
|
|
1031 |
final_capabilities = capabilities or existing_capabilities
|
|
|
|
|
1032 |
combined_heard_from = list(set(existing_heard_from + (heard_from or [])))
|
1033 |
|
1034 |
+
# Проверка неизменности pubkey
|
1035 |
+
if existing_pubkey and pubkey and existing_pubkey != pubkey:
|
1036 |
+
print(f"[WARN] Peer {peer_id} pubkey mismatch! Possible impersonation attempt.")
|
1037 |
+
return
|
1038 |
+
final_pubkey = existing_pubkey or pubkey
|
1039 |
+
|
1040 |
+
# Объединяем адреса по addr, проверяем PoW
|
1041 |
+
addr_map = {a["addr"]: a for a in existing_addresses if isinstance(a, dict)}
|
1042 |
+
for a in norm_addresses:
|
1043 |
+
addr = a["addr"]
|
1044 |
+
nonce = a.get("nonce")
|
1045 |
+
pow_hash = a.get("pow_hash")
|
1046 |
+
expires = a.get("expires")
|
1047 |
+
# проверка PoW
|
1048 |
+
if nonce is not None and pow_hash is not None:
|
1049 |
+
if not self.verify_pow(peer_id, final_pubkey, addr, nonce, pow_hash, expires):
|
1050 |
+
print(f"[WARN] Peer {peer_id} address {addr} failed PoW validation")
|
1051 |
+
continue
|
1052 |
+
addr_map[addr] = {"addr": addr, "nonce": nonce, "pow_hash": pow_hash, "expires": expires}
|
1053 |
+
|
1054 |
+
combined_addresses = list(addr_map.values())
|
1055 |
+
|
1056 |
+
# Вставка/обновление записи
|
1057 |
c.execute("""
|
1058 |
+
INSERT INTO agent_peers (id, name, addresses, source, status, last_seen, pubkey, capabilities, heard_from)
|
1059 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
1060 |
ON CONFLICT(id) DO UPDATE SET
|
1061 |
name=excluded.name,
|
1062 |
addresses=excluded.addresses,
|
1063 |
source=excluded.source,
|
1064 |
status=excluded.status,
|
1065 |
last_seen=excluded.last_seen,
|
|
|
1066 |
capabilities=excluded.capabilities,
|
|
|
|
|
1067 |
heard_from=excluded.heard_from
|
1068 |
""", (
|
1069 |
peer_id,
|
|
|
1074 |
datetime.now(UTC).isoformat(),
|
1075 |
final_pubkey,
|
1076 |
json.dumps(final_capabilities),
|
|
|
|
|
1077 |
json.dumps(combined_heard_from)
|
1078 |
))
|
1079 |
self.conn.commit()
|