elo-node 0.4.3__tar.gz → 0.4.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {elo_node-0.4.3 → elo_node-0.4.5}/PKG-INFO +1 -1
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/__init__.py +1 -1
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/node.py +111 -15
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/transport/protocol.py +6 -2
- {elo_node-0.4.3 → elo_node-0.4.5}/elo_node.egg-info/PKG-INFO +1 -1
- {elo_node-0.4.3 → elo_node-0.4.5}/elo_node.egg-info/SOURCES.txt +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo_node.egg-info/dependency_links.txt +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo_node.egg-info/requires.txt +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo_node.egg-info/top_level.txt +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/pyproject.toml +1 -1
- {elo_node-0.4.3 → elo_node-0.4.5}/tests/test_unit.py +1 -1
- {elo_node-0.4.3 → elo_node-0.4.5}/README.md +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/__main__.py +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/security.py +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/transport/__init__.py +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/transport/routing.py +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/transport/tcp.py +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/transport/tracker.py +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo/types.py +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/elo_node.egg-info/entry_points.txt +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/setup.cfg +0 -0
- {elo_node-0.4.3 → elo_node-0.4.5}/tests/test_security.py +0 -0
|
@@ -78,7 +78,7 @@ class Node:
|
|
|
78
78
|
peers: list[str] | None = None,
|
|
79
79
|
tracker: str = "public",
|
|
80
80
|
allowlist: list[str] | None = None,
|
|
81
|
-
version: str = "0.4.
|
|
81
|
+
version: str = "0.4.5",
|
|
82
82
|
identity: EphemeralIdentity | None = None,
|
|
83
83
|
verify_peers: bool = True,
|
|
84
84
|
heartbeat_interval_s: int = 30,
|
|
@@ -110,7 +110,7 @@ class Node:
|
|
|
110
110
|
self._task_handler: Callable[[Task], Awaitable[dict[str, Any]]] | None = None
|
|
111
111
|
self._shutdown_event = asyncio.Event()
|
|
112
112
|
self._pending_results: dict[str, asyncio.Future] = {}
|
|
113
|
-
self._pending_queries: dict[str, tuple[asyncio.
|
|
113
|
+
self._pending_queries: dict[str, tuple[list[dict[str, Any]], asyncio.Event, float]] = {}
|
|
114
114
|
|
|
115
115
|
# Cache de pubkeys para verificação de assinatura
|
|
116
116
|
self._pubkey_cache: dict[str, tuple[Any, float]] = {}
|
|
@@ -253,6 +253,10 @@ class Node:
|
|
|
253
253
|
except Exception as e:
|
|
254
254
|
return Result.make_error(task_id, "SEND_ERROR", str(e))
|
|
255
255
|
|
|
256
|
+
# Bug 2: Fallback via tracker antes de desistir
|
|
257
|
+
if not peer:
|
|
258
|
+
return await self.send_task_via_tracker("", target_node, capability, payload, ttl_s=ttl_s)
|
|
259
|
+
|
|
256
260
|
return Result.make_error(task_id, "NO_PEER", f"No peer for: {capability}")
|
|
257
261
|
|
|
258
262
|
async def send_task_async(self, target_node: str, capability: str,
|
|
@@ -292,10 +296,11 @@ class Node:
|
|
|
292
296
|
|
|
293
297
|
# ── descoberta ─────────────────────────────────────────────
|
|
294
298
|
|
|
295
|
-
async def
|
|
296
|
-
"""Return all known peers with capabilities.
|
|
299
|
+
async def get_known_peers_local(self) -> list[dict[str, Any]]:
|
|
300
|
+
"""Return all locally-known peers with capabilities (no network discovery).
|
|
297
301
|
|
|
298
302
|
Merges data from TCP connections (live) and InterestTable (registered).
|
|
303
|
+
Does NOT perform any network queries — only returns what's already known.
|
|
299
304
|
"""
|
|
300
305
|
result: dict[str, dict[str, Any]] = {}
|
|
301
306
|
|
|
@@ -314,6 +319,63 @@ class Node:
|
|
|
314
319
|
|
|
315
320
|
return list(result.values())
|
|
316
321
|
|
|
322
|
+
# Alias de compatibilidade — o método anterior chamava-se discover_peers()
|
|
323
|
+
async def discover_peers(self) -> list[dict[str, Any]]:
|
|
324
|
+
"""[DEPRECATED] Use get_known_peers_local() ou discover_peers_network().
|
|
325
|
+
|
|
326
|
+
Este método apenas consolida peers localmente conhecidos (TCP + InterestTable).
|
|
327
|
+
Não faz descoberta ativa de rede. Prefira discover_peers_network() para
|
|
328
|
+
descoberta via broadcast, ou get_known_peers() para peers com handshake completo.
|
|
329
|
+
"""
|
|
330
|
+
return await self.get_known_peers_local()
|
|
331
|
+
|
|
332
|
+
async def discover_peers_network(self, timeout: float = 5.0) -> list[dict[str, Any]]:
|
|
333
|
+
"""Discover peers on the network via QUERY broadcast.
|
|
334
|
+
|
|
335
|
+
Envia QUERY com capability vazia e agrega todas as respostas
|
|
336
|
+
dentro do timeout. Modelo BitTorrent: o tracker retorna todos
|
|
337
|
+
os peers conhecidos, não só quem matcha a capability.
|
|
338
|
+
"""
|
|
339
|
+
query_id = str(uuid.uuid4())[:8]
|
|
340
|
+
collected: list[dict[str, Any]] = []
|
|
341
|
+
event = asyncio.Event()
|
|
342
|
+
self._pending_queries[query_id] = (collected, event, time.time())
|
|
343
|
+
discovered: dict[str, dict[str, Any]] = {}
|
|
344
|
+
|
|
345
|
+
# Broadcast QUERY for any capability
|
|
346
|
+
await self._tcp.broadcast(query_msg("", query_id, ttl=3))
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
await asyncio.wait_for(event.wait(), timeout=timeout)
|
|
350
|
+
except asyncio.TimeoutError:
|
|
351
|
+
pass
|
|
352
|
+
finally:
|
|
353
|
+
self._pending_queries.pop(query_id, None)
|
|
354
|
+
|
|
355
|
+
# Merge collected peers from all responses
|
|
356
|
+
known_addrs: set[str] = set()
|
|
357
|
+
for entry in collected:
|
|
358
|
+
addr = entry.get("addr", "")
|
|
359
|
+
if addr and addr not in discovered:
|
|
360
|
+
discovered[addr] = {
|
|
361
|
+
"addr": addr,
|
|
362
|
+
"caps": entry.get("caps", []),
|
|
363
|
+
"connected": False,
|
|
364
|
+
"via": "network",
|
|
365
|
+
}
|
|
366
|
+
known_addrs.add(addr)
|
|
367
|
+
|
|
368
|
+
# Merge with locally known peers
|
|
369
|
+
local = await self.get_known_peers_local()
|
|
370
|
+
for entry in local:
|
|
371
|
+
addr = entry["addr"]
|
|
372
|
+
if addr not in discovered:
|
|
373
|
+
discovered[addr] = entry
|
|
374
|
+
else:
|
|
375
|
+
discovered[addr].update(entry)
|
|
376
|
+
|
|
377
|
+
return list(discovered.values())
|
|
378
|
+
|
|
317
379
|
def get_known_peers(self) -> list[dict[str, Any]]:
|
|
318
380
|
"""Return peers registered in InterestTable (completed HELLO handshake).
|
|
319
381
|
|
|
@@ -355,6 +417,18 @@ class Node:
|
|
|
355
417
|
await self._on_hello(peer_addr, msg)
|
|
356
418
|
elif msg_type == MessageType.HELLO_ACK:
|
|
357
419
|
await self._on_hello(peer_addr, msg)
|
|
420
|
+
# Bug 1: Se HELLO_ACK contém known_peers, conectar-se a eles
|
|
421
|
+
known_peers = msg.get("known_peers")
|
|
422
|
+
if known_peers:
|
|
423
|
+
hello = hello_msg(self._node_id, self._tracker.get_public_caps(),
|
|
424
|
+
list(self._routing.local_interests),
|
|
425
|
+
self._tracker.visibility, self._version)
|
|
426
|
+
for peer_info in known_peers:
|
|
427
|
+
addr = peer_info.get("addr", "")
|
|
428
|
+
if addr and addr != peer_addr and addr not in self._tcp.peer_addresses:
|
|
429
|
+
asyncio.create_task(
|
|
430
|
+
self._tcp.connect_to_peer(addr, hello_payload=hello)
|
|
431
|
+
)
|
|
358
432
|
elif msg_type == MessageType.QUERY:
|
|
359
433
|
await self._on_query(peer_addr, msg)
|
|
360
434
|
elif msg_type == MessageType.QUERY_RESP:
|
|
@@ -376,8 +450,14 @@ class Node:
|
|
|
376
450
|
interests = msg.get("interests", [])
|
|
377
451
|
self._routing.register_peer(peer_addr, caps, interests)
|
|
378
452
|
|
|
453
|
+
# Se for tracker (public/private), inclui peers conhecidos no ACK
|
|
454
|
+
known_peers = None
|
|
455
|
+
if msg.get("type") == MessageType.HELLO and self._tracker.visibility in ("public", "private"):
|
|
456
|
+
known_peers = self.get_known_peers()
|
|
457
|
+
|
|
379
458
|
ack = hello_ack_msg(self._node_id, self._tracker.get_caps_for_peer(node_id),
|
|
380
|
-
list(self._routing.local_interests), self._tracker.visibility
|
|
459
|
+
list(self._routing.local_interests), self._tracker.visibility,
|
|
460
|
+
known_peers=known_peers)
|
|
381
461
|
try:
|
|
382
462
|
await self._tcp.send_to(peer_addr, ack)
|
|
383
463
|
except Exception:
|
|
@@ -387,12 +467,23 @@ class Node:
|
|
|
387
467
|
capability = msg.get("capability", "")
|
|
388
468
|
query_id = msg.get("id", "")
|
|
389
469
|
ttl = msg.get("ttl", 5)
|
|
390
|
-
nodes = []
|
|
470
|
+
nodes: list[dict[str, Any]] = []
|
|
471
|
+
|
|
472
|
+
# Modelo BitTorrent: tracker retorna SEMPRE todos os peers conhecidos,
|
|
473
|
+
# não só quem matcha a capability. Isso permite que qualquer nó
|
|
474
|
+
# descubra a mesh completa com discover_peers_network().
|
|
475
|
+
for addr in self._routing.known_peers:
|
|
476
|
+
info = self._routing.get_peer_caps(addr)
|
|
477
|
+
if addr != peer_addr:
|
|
478
|
+
nodes.append({
|
|
479
|
+
"addr": addr,
|
|
480
|
+
"caps": list(info.get("caps", set())),
|
|
481
|
+
})
|
|
482
|
+
|
|
391
483
|
if self._tracker.has_capability(capability):
|
|
392
484
|
# Usa peer_addr (addr real do TCP) em vez de localhost — necessário para WAN/Tailscale
|
|
393
485
|
nodes.append({"node_id": self._node_id[:12], "addr": peer_addr})
|
|
394
|
-
|
|
395
|
-
nodes.append({"addr": p})
|
|
486
|
+
|
|
396
487
|
if nodes:
|
|
397
488
|
try:
|
|
398
489
|
await self._tcp.send_to(peer_addr, query_resp_msg(query_id, nodes))
|
|
@@ -404,10 +495,11 @@ class Node:
|
|
|
404
495
|
async def _on_query_resp(self, peer_addr: str, msg: dict) -> None:
|
|
405
496
|
query_id = msg.get("id", "")
|
|
406
497
|
if query_id in self._pending_queries:
|
|
407
|
-
|
|
498
|
+
collected, event, _ts = self._pending_queries[query_id]
|
|
408
499
|
nodes = msg.get("nodes", [])
|
|
409
|
-
if nodes
|
|
410
|
-
|
|
500
|
+
if nodes:
|
|
501
|
+
collected.extend(nodes)
|
|
502
|
+
event.set()
|
|
411
503
|
|
|
412
504
|
async def _on_interest_update(self, peer_addr: str, msg: dict) -> None:
|
|
413
505
|
existing = self._routing.get_peer_caps(peer_addr)
|
|
@@ -536,15 +628,19 @@ class Node:
|
|
|
536
628
|
async def _query_capability(self, capability: str, ttl: int = 5,
|
|
537
629
|
timeout: float = 5.0) -> str | None:
|
|
538
630
|
query_id = str(uuid.uuid4())[:8]
|
|
539
|
-
|
|
540
|
-
|
|
631
|
+
collected: list[dict[str, Any]] = []
|
|
632
|
+
event = asyncio.Event()
|
|
633
|
+
self._pending_queries[query_id] = (collected, event, time.time())
|
|
541
634
|
await self._tcp.broadcast(query_msg(capability, query_id, ttl))
|
|
542
635
|
try:
|
|
543
|
-
|
|
636
|
+
await asyncio.wait_for(event.wait(), timeout=timeout)
|
|
544
637
|
except asyncio.TimeoutError:
|
|
545
|
-
|
|
638
|
+
pass
|
|
546
639
|
finally:
|
|
547
640
|
self._pending_queries.pop(query_id, None)
|
|
641
|
+
if collected:
|
|
642
|
+
return collected[0].get("addr", None)
|
|
643
|
+
return None
|
|
548
644
|
|
|
549
645
|
async def _get_caller_pubkey(self, node_id: str) -> Any | None:
|
|
550
646
|
now = time.time()
|
|
@@ -89,14 +89,18 @@ def hello_msg(node_id: str, caps: dict, interests: list[str],
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def hello_ack_msg(node_id: str, caps: dict, interests: list[str],
|
|
92
|
-
tracker: str = "public"
|
|
93
|
-
|
|
92
|
+
tracker: str = "public",
|
|
93
|
+
known_peers: list[dict] | None = None) -> dict:
|
|
94
|
+
msg: dict[str, Any] = {
|
|
94
95
|
"type": MessageType.HELLO_ACK,
|
|
95
96
|
"node_id": node_id,
|
|
96
97
|
"caps": caps,
|
|
97
98
|
"interests": interests,
|
|
98
99
|
"tracker": tracker,
|
|
99
100
|
}
|
|
101
|
+
if known_peers is not None:
|
|
102
|
+
msg["known_peers"] = known_peers
|
|
103
|
+
return msg
|
|
100
104
|
|
|
101
105
|
|
|
102
106
|
def query_msg(capability: str, query_id: str, ttl: int = 5) -> dict:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -136,7 +136,7 @@ class TestNodeConstruction:
|
|
|
136
136
|
node = Node("test-node")
|
|
137
137
|
assert node._name == "test-node"
|
|
138
138
|
assert node._port == 7878
|
|
139
|
-
assert node._version == "0.4.
|
|
139
|
+
assert node._version == "0.4.4"
|
|
140
140
|
assert node.connected is False
|
|
141
141
|
assert node.node_id is not None
|
|
142
142
|
assert len(node.node_id) > 20
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|