unaiverse 0.1.6__cp312-cp312-macosx_11_0_arm64.whl
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.
Potentially problematic release.
This version of unaiverse might be problematic. Click here for more details.
- unaiverse/__init__.py +19 -0
- unaiverse/agent.py +2008 -0
- unaiverse/agent_basics.py +1846 -0
- unaiverse/clock.py +191 -0
- unaiverse/dataprops.py +1209 -0
- unaiverse/hsm.py +1880 -0
- unaiverse/modules/__init__.py +18 -0
- unaiverse/modules/cnu/__init__.py +17 -0
- unaiverse/modules/cnu/cnus.py +536 -0
- unaiverse/modules/cnu/layers.py +261 -0
- unaiverse/modules/cnu/psi.py +60 -0
- unaiverse/modules/hl/__init__.py +15 -0
- unaiverse/modules/hl/hl_utils.py +411 -0
- unaiverse/modules/networks.py +1509 -0
- unaiverse/modules/utils.py +680 -0
- unaiverse/networking/__init__.py +16 -0
- unaiverse/networking/node/__init__.py +18 -0
- unaiverse/networking/node/connpool.py +1261 -0
- unaiverse/networking/node/node.py +2223 -0
- unaiverse/networking/node/profile.py +446 -0
- unaiverse/networking/node/tokens.py +79 -0
- unaiverse/networking/p2p/__init__.py +198 -0
- unaiverse/networking/p2p/go.mod +127 -0
- unaiverse/networking/p2p/go.sum +548 -0
- unaiverse/networking/p2p/golibp2p.py +18 -0
- unaiverse/networking/p2p/golibp2p.pyi +135 -0
- unaiverse/networking/p2p/lib.go +2714 -0
- unaiverse/networking/p2p/lib.go.sha256 +1 -0
- unaiverse/networking/p2p/lib_types.py +312 -0
- unaiverse/networking/p2p/message_pb2.py +63 -0
- unaiverse/networking/p2p/messages.py +265 -0
- unaiverse/networking/p2p/mylogger.py +77 -0
- unaiverse/networking/p2p/p2p.py +929 -0
- unaiverse/networking/p2p/proto-go/message.pb.go +616 -0
- unaiverse/networking/p2p/unailib.cpython-312-darwin.so +0 -0
- unaiverse/streamlib/__init__.py +15 -0
- unaiverse/streamlib/streamlib.py +210 -0
- unaiverse/streams.py +770 -0
- unaiverse/utils/__init__.py +16 -0
- unaiverse/utils/ask_lone_wolf.json +27 -0
- unaiverse/utils/lone_wolf.json +19 -0
- unaiverse/utils/misc.py +305 -0
- unaiverse/utils/sandbox.py +293 -0
- unaiverse/utils/server.py +435 -0
- unaiverse/world.py +175 -0
- unaiverse-0.1.6.dist-info/METADATA +365 -0
- unaiverse-0.1.6.dist-info/RECORD +50 -0
- unaiverse-0.1.6.dist-info/WHEEL +6 -0
- unaiverse-0.1.6.dist-info/licenses/LICENSE +43 -0
- unaiverse-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
"""
|
|
2
|
+
█████ █████ ██████ █████ █████ █████ █████ ██████████ ███████████ █████████ ██████████
|
|
3
|
+
░░███ ░░███ ░░██████ ░░███ ░░███ ░░███ ░░███ ░░███░░░░░█░░███░░░░░███ ███░░░░░███░░███░░░░░█
|
|
4
|
+
░███ ░███ ░███░███ ░███ ██████ ░███ ░███ ░███ ░███ █ ░ ░███ ░███ ░███ ░░░ ░███ █ ░
|
|
5
|
+
░███ ░███ ░███░░███░███ ░░░░░███ ░███ ░███ ░███ ░██████ ░██████████ ░░█████████ ░██████
|
|
6
|
+
░███ ░███ ░███ ░░██████ ███████ ░███ ░░███ ███ ░███░░█ ░███░░░░░███ ░░░░░░░░███ ░███░░█
|
|
7
|
+
░███ ░███ ░███ ░░█████ ███░░███ ░███ ░░░█████░ ░███ ░ █ ░███ ░███ ███ ░███ ░███ ░ █
|
|
8
|
+
░░████████ █████ ░░█████░░████████ █████ ░░███ ██████████ █████ █████░░█████████ ██████████
|
|
9
|
+
░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░ ░░░░░ ░░░ ░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░░░░ ░░░░░░░░░░
|
|
10
|
+
A Collectionless AI Project (https://collectionless.ai)
|
|
11
|
+
Registration/Login: https://unaiverse.io
|
|
12
|
+
Code Repositories: https://github.com/collectionlessai/
|
|
13
|
+
Main Developers: Stefano Melacci (Project Leader), Christian Di Maio, Tommaso Guidi
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
import base64
|
|
18
|
+
import logging
|
|
19
|
+
import binascii
|
|
20
|
+
import threading
|
|
21
|
+
from .messages import Msg
|
|
22
|
+
from .lib_types import TypeInterface
|
|
23
|
+
from typing import Optional, List, Dict, Any, TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
# Conditional import for type hinting to avoid circular dependencies
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
try:
|
|
28
|
+
from unaiverse.networking.p2p.golibp2p import GoLibP2P # Assuming this class loads the library
|
|
29
|
+
except ImportError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger('P2P')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class P2PError(Exception):
|
|
36
|
+
"""Custom exception class for P2P library errors."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class P2P:
|
|
41
|
+
"""
|
|
42
|
+
Python wrapper for the Go libp2p shared library.
|
|
43
|
+
|
|
44
|
+
This class initializes a libp2p node, provides methods to interact with the
|
|
45
|
+
p2p network (connect, send/receive messages, pubsub, relay), and manages
|
|
46
|
+
the lifecycle of the underlying Go node.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
libp2p (LibP2P): Static class attribute holding the loaded Go library instance.
|
|
50
|
+
Must be set before instantiating P2P. Example: P2P.libp2p = LibP2P()
|
|
51
|
+
peer_id (str): The Peer ID of the initialized local node.
|
|
52
|
+
addresses (Optional[List[str]]): List of multiaddresses the local node is listening on.
|
|
53
|
+
is_public (bool): Whether the node is publicly reachable.
|
|
54
|
+
peer_map (Dict[str, Any]): A dictionary to potentially store information about connected peers (managed manually or by polling thread).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# --- Class-level state ---
|
|
58
|
+
libp2p: 'GoLibP2P' # Static variable for the loaded Go library
|
|
59
|
+
_type_interface: 'TypeInterface' # Shared type interface for all instances
|
|
60
|
+
|
|
61
|
+
# --- Config class variables for configuration ---
|
|
62
|
+
_MAX_INSTANCES = 32
|
|
63
|
+
_MAX_NUM_CAHNNELS = 100
|
|
64
|
+
_MAX_QUEUE_PER_CHANNEL = 50
|
|
65
|
+
_MAX_MESSAGE_SIZE = 50 * 1024 * 1024 # 50 MB
|
|
66
|
+
|
|
67
|
+
# --- Class-level tracking ---
|
|
68
|
+
_library_initialized = False
|
|
69
|
+
_initialize_lock = threading.Lock()
|
|
70
|
+
_instance_ids = [False, ] * _MAX_INSTANCES
|
|
71
|
+
_instance_lock = threading.Lock()
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def setup_library(cls,
|
|
75
|
+
max_instances: Optional[int] = None,
|
|
76
|
+
max_channels: Optional[int] = None,
|
|
77
|
+
max_queue_per_channel: Optional[int] = None,
|
|
78
|
+
max_message_size: Optional[int] = None,
|
|
79
|
+
enable_logging: bool = False) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Initializes the underlying Go library. Must be called once. This is called automatically.
|
|
82
|
+
"""
|
|
83
|
+
with cls._initialize_lock:
|
|
84
|
+
if cls._library_initialized:
|
|
85
|
+
logger.warning("P2P library is already initialized. Skipping setup.")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if not hasattr(cls, 'libp2p') or cls.libp2p is None:
|
|
89
|
+
raise P2PError("Library not loaded before setup. Check package __init__.py")
|
|
90
|
+
|
|
91
|
+
# Configure Python logging based on the flag
|
|
92
|
+
if not enable_logging:
|
|
93
|
+
logger.setLevel(logging.CRITICAL)
|
|
94
|
+
else:
|
|
95
|
+
logger.setLevel(logging.INFO)
|
|
96
|
+
|
|
97
|
+
logger.info("🐍 Setting up and initializing P2P library core with user settings...")
|
|
98
|
+
cls._type_interface = TypeInterface(cls.libp2p)
|
|
99
|
+
|
|
100
|
+
# Use provided arguments or fall back to class defaults
|
|
101
|
+
_max_instances = max_instances if max_instances is not None else cls._MAX_INSTANCES
|
|
102
|
+
_max_channels = max_channels if max_channels is not None else cls._MAX_NUM_CAHNNELS
|
|
103
|
+
_max_queue = max_queue_per_channel if max_queue_per_channel is not None else cls._MAX_QUEUE_PER_CHANNEL
|
|
104
|
+
_max_msg_size = max_message_size if max_message_size is not None else cls._MAX_MESSAGE_SIZE
|
|
105
|
+
|
|
106
|
+
# Update class attributes if they were overridden
|
|
107
|
+
cls._MAX_INSTANCES = _max_instances
|
|
108
|
+
cls._instance_ids = [False, ] * _max_instances # Resize the tracking list
|
|
109
|
+
|
|
110
|
+
# Call the Go function to set up its internal state
|
|
111
|
+
logger.info("🐍 Initializing Go library core...")
|
|
112
|
+
cls.libp2p.InitializeLibrary(
|
|
113
|
+
cls._type_interface.to_go_int(_max_instances),
|
|
114
|
+
cls._type_interface.to_go_int(_max_channels),
|
|
115
|
+
cls._type_interface.to_go_int(_max_queue),
|
|
116
|
+
cls._type_interface.to_go_int(_max_msg_size),
|
|
117
|
+
cls._type_interface.to_go_bool(enable_logging)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
cls._library_initialized = True
|
|
121
|
+
logger.info("✅ Go library initialized successfully.")
|
|
122
|
+
|
|
123
|
+
def __init__(self,
|
|
124
|
+
identity_dir: str,
|
|
125
|
+
port: int = 0,
|
|
126
|
+
ips: List[str] = None,
|
|
127
|
+
enable_relay_client: bool = True,
|
|
128
|
+
enable_relay_service: bool = False,
|
|
129
|
+
knows_is_public: bool = False,
|
|
130
|
+
max_connections: int = 1000,
|
|
131
|
+
enable_tls: bool = False,
|
|
132
|
+
domain_name: Optional[str] = None,
|
|
133
|
+
tls_cert_path: Optional[str] = None,
|
|
134
|
+
tls_key_path: Optional[str] = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Initializes and starts a new libp2p node.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
identity_dir: Directory path to load/store the node's private key and certificates.
|
|
141
|
+
port: The (first) TCP port to listen on (0 for random).
|
|
142
|
+
ips: A list of specific IP addresses to listen on. Defaults to ["0.0.0.0"].
|
|
143
|
+
enable_relay_client: Enable listening to relayed connections for this node.
|
|
144
|
+
enable_relay_service: Enable relay service capabilities for this node.
|
|
145
|
+
knows_is_public: If you already know that the node is public this forces its public reachability. Otherwise it tries every possible attempt to make the node publicly reachable (UPnP, HolePunching, AutoNat via DHT...).
|
|
146
|
+
max_connections: Maximum number of connections this node can handle.
|
|
147
|
+
enable_tls: Whether to enable AutoTLS certificate management (requires internet access).
|
|
148
|
+
domain_name: Optional domain name for TLS certificate (required if enable_tls is True).
|
|
149
|
+
tls_cert_path: Optional path to a custom TLS certificate file (PEM format).
|
|
150
|
+
tls_key_path: Optional path to a custom TLS private key file (PEM format).
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
P2PError: If the node creation fails in the Go library.
|
|
154
|
+
AttributeError: If P2P.libp2p has not been set before instantiation.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
# --- CRITICAL: Check if library is initialized ---
|
|
158
|
+
if not P2P._library_initialized:
|
|
159
|
+
raise P2PError("P2P library not set up. Call P2P.setup_library() before creating an instance.")
|
|
160
|
+
|
|
161
|
+
# Assign instance ID
|
|
162
|
+
assigned_instance_id = -1
|
|
163
|
+
with P2P._instance_lock:
|
|
164
|
+
for _instance_id, i in enumerate(self._instance_ids):
|
|
165
|
+
if not i:
|
|
166
|
+
self._instance_ids[_instance_id] = True
|
|
167
|
+
assigned_instance_id = _instance_id
|
|
168
|
+
break
|
|
169
|
+
if assigned_instance_id == -1:
|
|
170
|
+
raise P2PError(
|
|
171
|
+
f"Cannot create new P2P instance: Maximum number of instances "
|
|
172
|
+
f"({P2P._MAX_INSTANCES})."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self._instance: int = assigned_instance_id
|
|
176
|
+
logger.info(f"🚀 Attempting to initialize P2P Node with auto-assigned Instance ID: {self._instance}")
|
|
177
|
+
|
|
178
|
+
os.makedirs(identity_dir, exist_ok=True)
|
|
179
|
+
# check if all the ingredients for TLS are provided if any is provided
|
|
180
|
+
if domain_name is not None or tls_cert_path is not None or tls_key_path is not None:
|
|
181
|
+
enable_tls = True
|
|
182
|
+
if domain_name is None or tls_cert_path is None or tls_key_path is None:
|
|
183
|
+
raise ValueError("If any TLS parameter is provided, all of domain_name, tls_cert_path, and tls_key_path must be provided.")
|
|
184
|
+
|
|
185
|
+
self._port = port
|
|
186
|
+
self._ips = ips if ips is not None else []
|
|
187
|
+
self._enable_relay_client = enable_relay_client or enable_relay_service
|
|
188
|
+
self._enable_relay_service = enable_relay_service
|
|
189
|
+
self._knows_is_public = knows_is_public
|
|
190
|
+
self._max_connections = max_connections
|
|
191
|
+
self._peer_id: Optional[str] = None
|
|
192
|
+
self._peer_map: Dict[str, Any] = {} # Map to store peer info {peer_id: info}
|
|
193
|
+
|
|
194
|
+
logger.info(f"🐍 Creating Node (Instance ID: {self._instance})...")
|
|
195
|
+
try:
|
|
196
|
+
|
|
197
|
+
# Call the Go function
|
|
198
|
+
result_ptr = P2P.libp2p.CreateNode(
|
|
199
|
+
P2P._type_interface.to_go_int(self._instance),
|
|
200
|
+
P2P._type_interface.to_go_string(identity_dir),
|
|
201
|
+
P2P._type_interface.to_go_int(port),
|
|
202
|
+
P2P._type_interface.to_go_json(self._ips),
|
|
203
|
+
P2P._type_interface.to_go_bool(enable_relay_client),
|
|
204
|
+
P2P._type_interface.to_go_bool(enable_relay_service),
|
|
205
|
+
P2P._type_interface.to_go_bool(knows_is_public),
|
|
206
|
+
P2P._type_interface.to_go_int(max_connections),
|
|
207
|
+
P2P._type_interface.to_go_bool(enable_tls),
|
|
208
|
+
P2P._type_interface.to_go_string(domain_name if domain_name else ""),
|
|
209
|
+
P2P._type_interface.to_go_string(tls_cert_path if tls_cert_path else ""),
|
|
210
|
+
P2P._type_interface.to_go_string(tls_key_path if tls_key_path else ""),
|
|
211
|
+
)
|
|
212
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
213
|
+
|
|
214
|
+
if result is None:
|
|
215
|
+
err_msg = "Received null result from Go CreateNode."
|
|
216
|
+
logger.error(f"[Instance {self._instance}] {err_msg}")
|
|
217
|
+
raise P2PError(f"[Instance {self._instance}] {err_msg}")
|
|
218
|
+
if result.get('state') == "Error":
|
|
219
|
+
err_msg = result.get('message', 'Unknown Go error on CreateNode')
|
|
220
|
+
logger.error(f"[Instance {self._instance}] Go error: {err_msg}")
|
|
221
|
+
raise P2PError(f"[Instance {self._instance}] Failed to create node: {err_msg}")
|
|
222
|
+
|
|
223
|
+
message_data = result.get('message')
|
|
224
|
+
initial_addresses = message_data.get("addresses", [])
|
|
225
|
+
self._is_public = message_data.get("isPublic", False)
|
|
226
|
+
|
|
227
|
+
# Check teh returned data
|
|
228
|
+
if not isinstance(initial_addresses, list) or not initial_addresses:
|
|
229
|
+
err_msg = "Received empty or invalid addresses list from Go CreateNode."
|
|
230
|
+
logger.error(f"[Instance {self._instance}] {err_msg}")
|
|
231
|
+
raise P2PError(f"[Instance {self._instance}] {err_msg}")
|
|
232
|
+
|
|
233
|
+
self._peer_id = initial_addresses[0].split("/")[-1]
|
|
234
|
+
|
|
235
|
+
# Cache for the dynamic addresses property
|
|
236
|
+
self._address_cache: Optional[List[str]] = initial_addresses
|
|
237
|
+
self._address_cache_time: float = time.monotonic()
|
|
238
|
+
|
|
239
|
+
logger.info(f"✅ [Instance {self._instance}] Node created with ID: {self._peer_id}")
|
|
240
|
+
logger.info(f"👂 [Instance {self._instance}] Listening on: {self._address_cache}")
|
|
241
|
+
logger.info(f"🌐 [Instance {self._instance}] Publicly reachable: {self._is_public}")
|
|
242
|
+
|
|
243
|
+
logger.info(f"🎉 [Instance {self._instance}] Node initialized successfully.")
|
|
244
|
+
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.error(f"❌ [Instance {self._instance}] Node creation failed: {e}")
|
|
247
|
+
|
|
248
|
+
# Reclaim the instance ID using the _instance_ids list
|
|
249
|
+
if self._instance != -1: # Check if an ID was actually assigned
|
|
250
|
+
with P2P._instance_lock:
|
|
251
|
+
P2P._instance_ids[self._instance] = False
|
|
252
|
+
logger.info(f"[Instance {self._instance}] Reclaimed instance ID {self._instance} due to creation failure.")
|
|
253
|
+
raise # Re-raise the exception that caused the failure
|
|
254
|
+
|
|
255
|
+
logger.info("🎉 Node created successfully and background polling started.")
|
|
256
|
+
|
|
257
|
+
# --- Core P2P Operations ---
|
|
258
|
+
|
|
259
|
+
def connect_to(self, multiaddrs: list[str]) -> Dict[str, Any]:
|
|
260
|
+
"""
|
|
261
|
+
Establishes a connection with a remote peer.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
multiaddrs: The list of multiaddress strings of the peer to try to connect to.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
A dictionary containing the connected peer's AddrInfo (ID and Addrs).
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
P2PError: If the connection fails.
|
|
271
|
+
ValueError: If the multiaddr is invalid.
|
|
272
|
+
"""
|
|
273
|
+
if not multiaddrs or not isinstance(multiaddrs, list):
|
|
274
|
+
logger.error("Invalid multiaddr provided.")
|
|
275
|
+
raise ValueError("Invalid multiaddr provided.")
|
|
276
|
+
dest_peer_id = multiaddrs[0].split('/')[-1]
|
|
277
|
+
logger.info(f"📞 Attempting to connect to: {dest_peer_id}...")
|
|
278
|
+
try:
|
|
279
|
+
result_ptr = P2P.libp2p.ConnectTo(
|
|
280
|
+
P2P._type_interface.to_go_int(self._instance),
|
|
281
|
+
P2P._type_interface.to_go_json(multiaddrs)
|
|
282
|
+
)
|
|
283
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
284
|
+
|
|
285
|
+
if result is None:
|
|
286
|
+
logger.error("Failed to connect to peer, received null result.")
|
|
287
|
+
raise P2PError("Failed to connect to peer, received null result.")
|
|
288
|
+
if result.get('state') == "Error":
|
|
289
|
+
logger.error(f"Failed to connect to peer '{dest_peer_id}': {result.get('message', 'Unknown Go error')}")
|
|
290
|
+
raise P2PError(f"Failed to connect to peer '{dest_peer_id}': {result.get('message', 'Unknown Go error')}")
|
|
291
|
+
|
|
292
|
+
peer_info = result.get('message', {})
|
|
293
|
+
logger.info(f"✅ Connection initiated to peer: {peer_info.get('ID', dest_peer_id)}") # Use ID if available
|
|
294
|
+
|
|
295
|
+
# Optionally update internal peer map here
|
|
296
|
+
# self._peer_map[peer_info.get('ID')] = peer_info
|
|
297
|
+
return peer_info
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(f"❌ Connection to {dest_peer_id} failed: {e}")
|
|
301
|
+
raise P2PError(f"Connection to {dest_peer_id} failed") from e
|
|
302
|
+
|
|
303
|
+
def disconnect_from(self, peer_id: str) -> None:
|
|
304
|
+
"""
|
|
305
|
+
Closes connections to a specific peer and removes tracking.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
peer_id: The Peer ID string of the peer to disconnect from.
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
P2PError: If disconnecting fails.
|
|
312
|
+
ValueError: If the peer_id is invalid.
|
|
313
|
+
"""
|
|
314
|
+
if not peer_id or not isinstance(peer_id, str):
|
|
315
|
+
logger.error("Invalid Peer ID provided.")
|
|
316
|
+
raise ValueError("Invalid Peer ID provided.")
|
|
317
|
+
|
|
318
|
+
# Basic peer ID format check (Qm... or 12D3...)
|
|
319
|
+
if not (peer_id.startswith("Qm") or peer_id.startswith("12D3")):
|
|
320
|
+
logger.warning(f"⚠️ Warning: Peer ID '{peer_id}' does not look like a standard v0 or v1 ID.")
|
|
321
|
+
|
|
322
|
+
logger.info(f"🔌 Attempting to disconnect from peer: {peer_id}...")
|
|
323
|
+
try:
|
|
324
|
+
result_ptr = P2P.libp2p.DisconnectFrom(
|
|
325
|
+
P2P._type_interface.to_go_int(self._instance),
|
|
326
|
+
P2P._type_interface.to_go_string(peer_id)
|
|
327
|
+
)
|
|
328
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
329
|
+
|
|
330
|
+
if result is None:
|
|
331
|
+
logger.error("Failed to disconnect from peer, received null result.")
|
|
332
|
+
raise P2PError("Failed to disconnect from peer, received null result.")
|
|
333
|
+
|
|
334
|
+
if result.get('state') == "Error":
|
|
335
|
+
logger.error(f"Failed to disconnect from peer '{peer_id}': {result.get('message', 'Unknown Go error')}")
|
|
336
|
+
raise P2PError(f"Failed to disconnect from peer '{peer_id}': {result.get('message', 'Unknown Go error')}")
|
|
337
|
+
|
|
338
|
+
logger.info(f"✅ Successfully disconnected from {peer_id}")
|
|
339
|
+
|
|
340
|
+
# Optionally remove from internal peer map
|
|
341
|
+
# if peer_id in self._peer_map: del self._peer_map[peer_id]
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.error(f"❌ Disconnection from {peer_id} failed: {e}")
|
|
345
|
+
raise P2PError(f"Disconnection from {peer_id} failed") from e
|
|
346
|
+
|
|
347
|
+
def send_message_to_peer(self, channel: str, msg: Msg) -> None:
|
|
348
|
+
"""
|
|
349
|
+
Sends a direct message to a specific peer.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
channel: The string identifying the channel for the communication.
|
|
353
|
+
msg: The message to send (Msg).
|
|
354
|
+
|
|
355
|
+
Raises:
|
|
356
|
+
P2PError: If message sending fails (based on return code).
|
|
357
|
+
ValueError: If inputs are invalid.
|
|
358
|
+
TypeError: If data is not bytes.
|
|
359
|
+
"""
|
|
360
|
+
if not channel or not isinstance(channel, str):
|
|
361
|
+
logger.error("Invalid channel provided.")
|
|
362
|
+
raise ValueError("Invalid channel provided.")
|
|
363
|
+
|
|
364
|
+
if not isinstance(msg, Msg):
|
|
365
|
+
logger.error("Invalid message provided (must be of type Msg).")
|
|
366
|
+
raise ValueError("Invalid message provided (must be of type Msg).")
|
|
367
|
+
|
|
368
|
+
# Serialize the entire message object to bytes using Protobuf.
|
|
369
|
+
payload_bytes = msg.to_bytes()
|
|
370
|
+
payload_len = len(payload_bytes)
|
|
371
|
+
msg_content_type = msg.content_type
|
|
372
|
+
peer_id = channel.split("::dm:")[1].split('-')[0] # Extract Peer ID from channel format
|
|
373
|
+
|
|
374
|
+
logger.info(f"📤 Sending message (type: {msg_content_type}, len: {payload_len}) to peer: {peer_id}...")
|
|
375
|
+
|
|
376
|
+
# Call the Go function
|
|
377
|
+
try:
|
|
378
|
+
result_ptr = P2P.libp2p.SendMessageToPeer(
|
|
379
|
+
P2P._type_interface.to_go_int(self._instance),
|
|
380
|
+
P2P._type_interface.to_go_string(channel),
|
|
381
|
+
P2P._type_interface.to_go_bytes(payload_bytes), # Pass bytes directly
|
|
382
|
+
P2P._type_interface.to_go_int(payload_len),
|
|
383
|
+
)
|
|
384
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
385
|
+
|
|
386
|
+
if result is None:
|
|
387
|
+
logger.error(f"Failed to send direct message to {peer_id}, received null result.")
|
|
388
|
+
raise P2PError(f"Failed to send direct message to {peer_id}, received null result.")
|
|
389
|
+
|
|
390
|
+
if result.get('state') == "Error":
|
|
391
|
+
logger.error(f"Failed to send direct message to '{peer_id}': {result.get('message', 'Unknown Go error')}")
|
|
392
|
+
raise P2PError(f"Failed to send direct message to '{peer_id}': {result.get('message', 'Unknown Go error')}")
|
|
393
|
+
|
|
394
|
+
logger.info(f"✅ Message sent successfully to {peer_id}.")
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.error(f"❌ Sending direct message to {peer_id} failed: {e}")
|
|
398
|
+
raise P2PError(f"Sending direct message to {peer_id} failed") from e
|
|
399
|
+
|
|
400
|
+
def broadcast_message(self, channel: str, msg: Msg) -> None:
|
|
401
|
+
"""
|
|
402
|
+
Broadcasts a message using PubSub to the node's own topic.
|
|
403
|
+
Peers subscribed to this node's Peer ID topic will receive it.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
channel: The Channel for this topic (e.g., owner_peer_id::ps:topic_name).
|
|
407
|
+
msg: The message to send (Msg).
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
P2PError: If broadcasting fails.
|
|
411
|
+
ValueError: If inputs are invalid.
|
|
412
|
+
TypeError: If data is not bytes.
|
|
413
|
+
"""
|
|
414
|
+
if not channel or not isinstance(channel, str):
|
|
415
|
+
raise ValueError("Invalid channel provided.")
|
|
416
|
+
if not isinstance(msg, Msg):
|
|
417
|
+
raise ValueError("Invalid message provided (must be of type Msg).")
|
|
418
|
+
|
|
419
|
+
# Serialize the entire message object to bytes using Protobuf.
|
|
420
|
+
payload_bytes = msg.to_bytes()
|
|
421
|
+
payload_len = len(payload_bytes)
|
|
422
|
+
msg_content_type = msg.content_type
|
|
423
|
+
|
|
424
|
+
logger.info(f"📢 Broadcasting message (type: {msg_content_type}, len: {payload_len}) to own topic ({self.peer_id})...")
|
|
425
|
+
|
|
426
|
+
# Call SendMessageToPeer with an empty peer_id string for broadcast
|
|
427
|
+
try:
|
|
428
|
+
result_ptr = P2P.libp2p.SendMessageToPeer(
|
|
429
|
+
P2P._type_interface.to_go_int(self._instance),
|
|
430
|
+
P2P._type_interface.to_go_string(channel),
|
|
431
|
+
P2P._type_interface.to_go_bytes(payload_bytes),
|
|
432
|
+
P2P._type_interface.to_go_int(payload_len),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
436
|
+
|
|
437
|
+
if result is None:
|
|
438
|
+
logger.error(f"Failed to broadcast message on channel {channel}, received null result.")
|
|
439
|
+
raise P2PError(f"Failed to broadcast message on channel {channel}, received null result.")
|
|
440
|
+
|
|
441
|
+
if result.get('state') == "Error":
|
|
442
|
+
logger.error(f"Failed to broadcast message on channel '{channel}': {result.get('message', 'Unknown Go error')}")
|
|
443
|
+
raise P2PError(f"Failed to broadcast message on channel '{channel}': {result.get('message', 'Unknown Go error')}")
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
logger.error(f"❌ Broadcasting to {channel} failed: {e}")
|
|
447
|
+
raise P2PError(f"Broadcasting to {channel} failed") from e
|
|
448
|
+
|
|
449
|
+
logger.info(f"✅ Message broadcast successfully to {channel}.")
|
|
450
|
+
|
|
451
|
+
def pop_messages(self) -> List[Msg]:
|
|
452
|
+
"""
|
|
453
|
+
Retrieves and removes the first message from the queue of each channel for this node instance.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
A list of Msg objects. Returns an empty list if no messages were available.
|
|
457
|
+
|
|
458
|
+
Raises:
|
|
459
|
+
P2PError: If popping messages failed internally in Go, or if data
|
|
460
|
+
conversion fails for any message.
|
|
461
|
+
"""
|
|
462
|
+
logger.debug(f"[Instance {self._instance}] Popping message(s)...")
|
|
463
|
+
try:
|
|
464
|
+
go_instance_c = P2P._type_interface.to_go_int(self._instance)
|
|
465
|
+
|
|
466
|
+
result_ptr = P2P.libp2p.PopMessages(go_instance_c)
|
|
467
|
+
|
|
468
|
+
# From_go_ptr_to_json should handle freeing result_ptr
|
|
469
|
+
raw_result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
470
|
+
|
|
471
|
+
if raw_result is None:
|
|
472
|
+
|
|
473
|
+
# This indicates an issue with the C call or JSON conversion in TypeInterface
|
|
474
|
+
logger.error(f"[Instance {self._instance}] PopMessages: Received null/invalid result from TypeInterface.")
|
|
475
|
+
raise P2PError(f"[Instance {self._instance}] PopMessages: Failed to get valid JSON response.")
|
|
476
|
+
|
|
477
|
+
# Check for Go-side error or empty states first
|
|
478
|
+
if isinstance(raw_result, dict):
|
|
479
|
+
state = raw_result.get('state')
|
|
480
|
+
if state == "Empty":
|
|
481
|
+
logger.debug(f"[Instance {self._instance}] PopMessages: Queue is empty.")
|
|
482
|
+
return [] # No messages available
|
|
483
|
+
if state == "Error":
|
|
484
|
+
error_message = raw_result.get('message', 'Unknown Go error during PopMessages')
|
|
485
|
+
logger.error(f"[Instance {self._instance}] PopMessages: {error_message}")
|
|
486
|
+
raise P2PError(f"[Instance {self._instance}] PopMessages: {error_message}")
|
|
487
|
+
|
|
488
|
+
# If it's a dict but not a known state, it's unexpected
|
|
489
|
+
logger.warning(f"[Instance {self._instance}] PopMessages: Unexpected dictionary format: {raw_result}")
|
|
490
|
+
raise P2PError(f"[Instance {self._instance}] PopMessages: Unexpected dictionary response format.")
|
|
491
|
+
|
|
492
|
+
# Expecting a list of messages if not an error/empty dict
|
|
493
|
+
if not isinstance(raw_result, list):
|
|
494
|
+
|
|
495
|
+
# This also covers the case where n=0 and Go returns "[]" which json.loads makes a list
|
|
496
|
+
# If it's not a list at this point, it's an unexpected format.
|
|
497
|
+
logger.error(f"[Instance {self._instance}] PopMessages: Unexpected response format, expected a list or "
|
|
498
|
+
f"specific state dictionary. Got: {type(raw_result)}")
|
|
499
|
+
raise P2PError(f"[Instance {self._instance}] PopMessages: Unexpected response format.")
|
|
500
|
+
|
|
501
|
+
# Process the list of message dictionaries
|
|
502
|
+
processed_messages: List[Msg] = []
|
|
503
|
+
for i, msg_dict in enumerate(raw_result):
|
|
504
|
+
try:
|
|
505
|
+
if not isinstance(msg_dict, dict):
|
|
506
|
+
logger.warning(f"[Instance {self._instance}] PopMessages: Item {i} in list is not a dict: {msg_dict}")
|
|
507
|
+
continue # Skip this malformed entry
|
|
508
|
+
|
|
509
|
+
# Extract and validate required fields from the Go message structure
|
|
510
|
+
# Go structure: {"from":"Qm...", "data":"BASE64_ENCODED_DATA"}
|
|
511
|
+
verified_sender_id = msg_dict.get("from")
|
|
512
|
+
base64_data = msg_dict.get("data")
|
|
513
|
+
|
|
514
|
+
if not all([verified_sender_id is not None, base64_data is not None]): # Type can be empty string
|
|
515
|
+
logger.warning(f"[Instance {self._instance}] PopMessages: Message item {i} missing required fields (from, type, data): {msg_dict}")
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
# Decode data
|
|
519
|
+
decoded_data = base64.b64decode(base64_data)
|
|
520
|
+
|
|
521
|
+
# Attempt to create the higher-level Msg object
|
|
522
|
+
# This assumes Msg.from_bytes can parse your message protocol from decoded_data
|
|
523
|
+
# and that Msg objects store sender, type, channel intrinsically or can be set.
|
|
524
|
+
msg_obj = Msg.from_bytes(decoded_data)
|
|
525
|
+
|
|
526
|
+
# --- CRITICAL SECURITY CHECK ---
|
|
527
|
+
# Verify that the sender claimed inside the message payload
|
|
528
|
+
# matches the cryptographically verified sender from the network layer.
|
|
529
|
+
if msg_obj.sender != verified_sender_id:
|
|
530
|
+
logger.error(f"SENDER MISMATCH! Network sender '{verified_sender_id}' does not match "
|
|
531
|
+
f"payload sender '{msg_obj.sender}'. Discarding message.")
|
|
532
|
+
|
|
533
|
+
# In a real-world scenario, you might also want to penalize or disconnect
|
|
534
|
+
# from a peer that sends such malformed/spoofed messages.
|
|
535
|
+
continue # Discard this message
|
|
536
|
+
|
|
537
|
+
logger.debug(f"[Instance {self._instance}] Message popped from {verified_sender_id} on {msg_obj.channel}, "
|
|
538
|
+
f"content_type: {msg_obj.content_type}, len: {len(decoded_data)})")
|
|
539
|
+
processed_messages.append(msg_obj)
|
|
540
|
+
|
|
541
|
+
except ValueError as ve:
|
|
542
|
+
logger.error(f"Invalid message created, stopping. Error: {ve}")
|
|
543
|
+
continue # Skip problematic message
|
|
544
|
+
except (TypeError, binascii.Error) as decode_err:
|
|
545
|
+
logger.error(f"[Instance {self._instance}] PopMessages: Failed to decode Base64 data for a message in batch: {decode_err}. Message dict: {msg_dict}")
|
|
546
|
+
continue # Skip problematic message
|
|
547
|
+
except Exception as msg_proc_err: # Catch errors from Msg.from_bytes or attribute setting
|
|
548
|
+
logger.error(f"[Instance {self._instance}] PopMessages: Error processing popped message item {i}: {msg_proc_err}. Message dict: {msg_dict}")
|
|
549
|
+
continue # Skip problematic message
|
|
550
|
+
|
|
551
|
+
if len(raw_result) > 0 and len(processed_messages) == 0:
|
|
552
|
+
logger.warning(f"[Instance {self._instance}] PopMessages: Received {len(raw_result)} messages from Go, but none could be processed into Msg objects.")
|
|
553
|
+
|
|
554
|
+
# This could indicate a persistent issue with message format or Msg class.
|
|
555
|
+
|
|
556
|
+
return processed_messages
|
|
557
|
+
|
|
558
|
+
except P2PError: # Re-raise P2PError directly
|
|
559
|
+
raise
|
|
560
|
+
except Exception as e:
|
|
561
|
+
|
|
562
|
+
# Catch potential JSON parsing errors from TypeInterface or other unexpected errors
|
|
563
|
+
logger.error(f"[Instance {self._instance}] ❌ Error during pop_message: {e}")
|
|
564
|
+
raise P2PError(f"[Instance {self._instance}] Unexpected error during pop_message: {e}") from e
|
|
565
|
+
|
|
566
|
+
# --- PubSub Operations ---
|
|
567
|
+
|
|
568
|
+
def subscribe_to_topic(self, channel: str) -> None:
|
|
569
|
+
"""
|
|
570
|
+
Subscribes to a PubSub topic to receive messages.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
channel: The Channel for this topic (e.g., owner_peer_id::ps:topic_name).
|
|
574
|
+
|
|
575
|
+
Raises:
|
|
576
|
+
P2PError: If subscribing fails.
|
|
577
|
+
ValueError: If topic_name is invalid.
|
|
578
|
+
"""
|
|
579
|
+
if not channel or not isinstance(channel, str):
|
|
580
|
+
logger.error("Invalid topic name provided.")
|
|
581
|
+
raise ValueError("Invalid topic name provided.")
|
|
582
|
+
logger.info(f"<sub> Subscribing to topic: {channel}...")
|
|
583
|
+
try:
|
|
584
|
+
result_ptr = P2P.libp2p.SubscribeToTopic(
|
|
585
|
+
P2P._type_interface.to_go_int(self._instance),
|
|
586
|
+
P2P._type_interface.to_go_string(channel)
|
|
587
|
+
)
|
|
588
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
589
|
+
|
|
590
|
+
if result is None:
|
|
591
|
+
logger.error("Failed to subscribe to topic, received null result.")
|
|
592
|
+
raise P2PError("Failed to subscribe to topic, received null result.")
|
|
593
|
+
if result.get('state') == "Error":
|
|
594
|
+
logger.error(f"Failed to subscribe to topic '{channel}': {result.get('message', 'Unknown Go error')}")
|
|
595
|
+
raise P2PError(f"Failed to subscribe to topic '{channel}': {result.get('message', 'Unknown Go error')}")
|
|
596
|
+
|
|
597
|
+
logger.info(f"✅ Successfully subscribed to {channel}")
|
|
598
|
+
|
|
599
|
+
except Exception as e:
|
|
600
|
+
logger.error(f"❌ Subscription to {channel} failed: {e}")
|
|
601
|
+
raise P2PError(f"Subscription to {channel} failed") from e
|
|
602
|
+
|
|
603
|
+
def unsubscribe_from_topic(self, channel: str) -> None:
|
|
604
|
+
"""
|
|
605
|
+
Unsubscribes from a PubSub topic.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
channel: The Channel for this topic (e.g., owner_peer_id::ps:topic_name).
|
|
609
|
+
|
|
610
|
+
Raises:
|
|
611
|
+
P2PError: If unsubscribing fails.
|
|
612
|
+
ValueError: If topic_name is invalid.
|
|
613
|
+
"""
|
|
614
|
+
if not channel or not isinstance(channel, str):
|
|
615
|
+
logger.error("Invalid topic name provided.")
|
|
616
|
+
raise ValueError("Invalid topic name provided.")
|
|
617
|
+
logger.info(f"</sub> Unsubscribing from topic: {channel}...")
|
|
618
|
+
try:
|
|
619
|
+
result_ptr = P2P.libp2p.UnsubscribeFromTopic(
|
|
620
|
+
P2P._type_interface.to_go_int(self._instance),
|
|
621
|
+
P2P._type_interface.to_go_string(channel)
|
|
622
|
+
)
|
|
623
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
624
|
+
|
|
625
|
+
if result is None:
|
|
626
|
+
logger.error("Failed to unsubscribe from topic, received null result.")
|
|
627
|
+
raise P2PError("Failed to unsubscribe from topic, received null result.")
|
|
628
|
+
if result.get('state') == "Error":
|
|
629
|
+
logger.error(f"Failed to unsubscribe from topic '{channel}': {result.get('message', 'Unknown Go error')}")
|
|
630
|
+
raise P2PError(f"Failed to unsubscribe from topic '{channel}': {result.get('message', 'Unknown Go error')}")
|
|
631
|
+
|
|
632
|
+
logger.info(f"✅ Successfully unsubscribed from {channel}")
|
|
633
|
+
|
|
634
|
+
except Exception as e:
|
|
635
|
+
logger.error(f"❌ Unsubscription from {channel} failed: {e}")
|
|
636
|
+
raise P2PError(f"Unsubscription from {channel} failed") from e
|
|
637
|
+
|
|
638
|
+
# --- Relay Operations ---
|
|
639
|
+
|
|
640
|
+
def reserve_on_relay(self, relay_peer_id: str) -> str:
|
|
641
|
+
"""
|
|
642
|
+
Attempts to reserve a slot on a specified relay node.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
relay_peer_id: The peerID of the relay node
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
The UTC expiration timestamp of the reservation as an ISO 8601 string.
|
|
649
|
+
|
|
650
|
+
Raises:
|
|
651
|
+
P2PError: If the reservation fails.
|
|
652
|
+
ValueError: If the relay_multiaddr is invalid.
|
|
653
|
+
"""
|
|
654
|
+
if not relay_peer_id or not isinstance(relay_peer_id, str):
|
|
655
|
+
logger.error("Invalid relay multiaddr provided.")
|
|
656
|
+
raise ValueError("Invalid relay multiaddr provided.")
|
|
657
|
+
logger.info(f"🅿️ Attempting to reserve slot on relay: {relay_peer_id}...")
|
|
658
|
+
try:
|
|
659
|
+
result_ptr = P2P.libp2p.ReserveOnRelay(
|
|
660
|
+
P2P._type_interface.to_go_int(self._instance),
|
|
661
|
+
P2P._type_interface.to_go_string(relay_peer_id)
|
|
662
|
+
)
|
|
663
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
664
|
+
|
|
665
|
+
if result is None:
|
|
666
|
+
logger.error("Failed to reserve on relay, received null result.")
|
|
667
|
+
raise P2PError("Failed to reserve on relay, received null result.")
|
|
668
|
+
if result.get('state') == "Error":
|
|
669
|
+
logger.error(f"Failed to reserve on relay '{relay_peer_id}': {result.get('message', 'Unknown Go error')}")
|
|
670
|
+
raise P2PError(f"Failed to reserve on relay '{relay_peer_id}': {result.get('message', 'Unknown Go error')}")
|
|
671
|
+
|
|
672
|
+
expiration_utc = result.get('message', {})
|
|
673
|
+
if not isinstance(expiration_utc, str):
|
|
674
|
+
raise P2PError(f"Expected expiration timestamp string, but got {type(expiration_utc)}")
|
|
675
|
+
|
|
676
|
+
logger.info(f"✅ Reservation successful. Expires at: {expiration_utc}")
|
|
677
|
+
return expiration_utc
|
|
678
|
+
|
|
679
|
+
except Exception as e:
|
|
680
|
+
logger.error(f"❌ Reservation on {relay_peer_id} failed: {e}")
|
|
681
|
+
raise P2PError(f"Reservation on {relay_peer_id} failed") from e
|
|
682
|
+
|
|
683
|
+
# --- Node Information ---
|
|
684
|
+
|
|
685
|
+
@property
|
|
686
|
+
def peer_id(self) -> Optional[str]:
|
|
687
|
+
"""Returns the Peer ID of the local node."""
|
|
688
|
+
return self._peer_id
|
|
689
|
+
|
|
690
|
+
@property
|
|
691
|
+
def addresses(self) -> Optional[List[str]]:
|
|
692
|
+
"""
|
|
693
|
+
Returns the list of multiaddresses the local node is listening on.
|
|
694
|
+
This property queries the Go layer directly, caching the result for performance.
|
|
695
|
+
"""
|
|
696
|
+
now = time.monotonic()
|
|
697
|
+
if self._address_cache is None or (now - self._address_cache_time > 5.0):
|
|
698
|
+
try:
|
|
699
|
+
self._address_cache = self.get_node_addresses()
|
|
700
|
+
self._address_cache_time = now
|
|
701
|
+
except P2PError as e:
|
|
702
|
+
logger.warning(f"Failed to refresh address cache, returning stale data. Error: {e}")
|
|
703
|
+
return self._address_cache # This could be None if initial fetch failed or if the property was called before node creation
|
|
704
|
+
|
|
705
|
+
@property
|
|
706
|
+
def is_public(self) -> Optional[bool]:
|
|
707
|
+
"""Returns a boolean stating whether the local node is publicly reachable."""
|
|
708
|
+
return self._is_public
|
|
709
|
+
|
|
710
|
+
@property
|
|
711
|
+
def relay_is_enabled(self) -> bool:
|
|
712
|
+
"""Returns whether the relay client functionality is enabled for this node."""
|
|
713
|
+
return self._enable_relay_client
|
|
714
|
+
|
|
715
|
+
def get_node_addresses(self, peer_id: str = "") -> List[str]:
|
|
716
|
+
"""
|
|
717
|
+
Gets the known multiaddresses for the local node or a specific peer.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
peer_id: The Peer ID string of the target peer. If empty, gets
|
|
721
|
+
addresses for the local node.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
A list of multiaddress strings (including the /p2p/PeerID suffix).
|
|
725
|
+
|
|
726
|
+
Raises:
|
|
727
|
+
P2PError: If fetching addresses fails.
|
|
728
|
+
"""
|
|
729
|
+
target = "local node" if not peer_id else f"peer {peer_id}"
|
|
730
|
+
logger.info(f"ℹ️ Fetching addresses for {target}...")
|
|
731
|
+
try:
|
|
732
|
+
result_ptr = P2P.libp2p.GetNodeAddresses(
|
|
733
|
+
P2P._type_interface.to_go_int(self._instance),
|
|
734
|
+
P2P._type_interface.to_go_string(peer_id)
|
|
735
|
+
)
|
|
736
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
737
|
+
|
|
738
|
+
if result is None:
|
|
739
|
+
logger.error("Failed to get node addresses, received null result.")
|
|
740
|
+
raise P2PError("Failed to get node addresses, received null result.")
|
|
741
|
+
if result.get('state') == "Error":
|
|
742
|
+
logger.error(f"Failed to get addresses for '{target}': {result.get('message', 'Unknown Go error')}")
|
|
743
|
+
raise P2PError(f"Failed to get addresses for '{target}': {result.get('message', 'Unknown Go error')}")
|
|
744
|
+
|
|
745
|
+
addr_list = result.get('message', [])
|
|
746
|
+
logger.info(f"✅ Found addresses for {target}: {addr_list}")
|
|
747
|
+
return addr_list
|
|
748
|
+
|
|
749
|
+
except Exception as e:
|
|
750
|
+
logger.error(f"❌ Failed to get addresses for {target}: {e}")
|
|
751
|
+
raise P2PError(f"Failed to get addresses for {target}") from e
|
|
752
|
+
|
|
753
|
+
def get_connected_peers_info(self) -> List[Dict[str, Any]]:
|
|
754
|
+
"""
|
|
755
|
+
Gets information about currently connected peers from the Go library.
|
|
756
|
+
|
|
757
|
+
Returns:
|
|
758
|
+
A list of dictionaries, each representing a connected peer with
|
|
759
|
+
keys like 'addr_info' (containing 'ID', 'Addrs'), 'connected_at', 'direction', and 'misc'.
|
|
760
|
+
|
|
761
|
+
Raises:
|
|
762
|
+
P2PError: If fetching connected peers fails.
|
|
763
|
+
"""
|
|
764
|
+
|
|
765
|
+
# Logger.info("ℹ️ Fetching connected peers info...") # Can be noisy
|
|
766
|
+
try:
|
|
767
|
+
|
|
768
|
+
# GetConnectedPeers takes no arguments in Go
|
|
769
|
+
result_ptr = P2P.libp2p.GetConnectedPeers(P2P._type_interface.to_go_int(self._instance))
|
|
770
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
771
|
+
|
|
772
|
+
if result is None:
|
|
773
|
+
logger.error("Failed to get connected peers, received null result.")
|
|
774
|
+
raise P2PError("Failed to get connected peers, received null result.")
|
|
775
|
+
if result.get('state') == "Error":
|
|
776
|
+
logger.error(f"Failed to get connected peers: {result.get('message', 'Unknown Go error')}")
|
|
777
|
+
raise P2PError(f"Failed to get connected peers: {result.get('message', 'Unknown Go error')}")
|
|
778
|
+
|
|
779
|
+
peers_list = result.get('message', [])
|
|
780
|
+
|
|
781
|
+
# Update internal map (optional)
|
|
782
|
+
# logger.info(f" Connected peers count: {len(peers_list)}") # Can be noisy
|
|
783
|
+
return peers_list
|
|
784
|
+
|
|
785
|
+
except Exception as e:
|
|
786
|
+
|
|
787
|
+
# Avoid crashing the polling thread, just log the error
|
|
788
|
+
logger.error(f"❌ Error fetching connected peers info: {e}")
|
|
789
|
+
|
|
790
|
+
# Optionally raise P2PError(f"Failed to get connected peers info") from e if called directly
|
|
791
|
+
return [] # Return empty list on error during polling
|
|
792
|
+
|
|
793
|
+
def get_rendezvous_peers_info(self) -> Dict[str, Any] | None:
|
|
794
|
+
"""
|
|
795
|
+
Gets the full rendezvous state from the Go library, including peers and metadata.
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
- A dictionary representing the RendezvousState (containing 'peers',
|
|
799
|
+
'update_count', 'last_updated') if an update has been received.
|
|
800
|
+
- None if no rendezvous topic is active or no updates have arrived yet.
|
|
801
|
+
|
|
802
|
+
Raises:
|
|
803
|
+
P2PError: If fetching the state fails in Go.
|
|
804
|
+
"""
|
|
805
|
+
try:
|
|
806
|
+
result_ptr = P2P.libp2p.GetRendezvousPeers(P2P._type_interface.to_go_int(self._instance))
|
|
807
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
808
|
+
|
|
809
|
+
if result is None:
|
|
810
|
+
logger.error("Failed to get rendezvous peers, received null result.")
|
|
811
|
+
raise P2PError("Failed to get rendezvous peers, received null result.")
|
|
812
|
+
|
|
813
|
+
state = result.get('state')
|
|
814
|
+
if state == "Empty":
|
|
815
|
+
logger.debug(f"[Instance {self._instance}] GetRendezvousPeers: No rendezvous messages received yet.")
|
|
816
|
+
return None # Return None for the "empty" state
|
|
817
|
+
elif state == "Error":
|
|
818
|
+
error_msg = result.get('message', 'Unknown Go error')
|
|
819
|
+
logger.error(f"Failed to get rendezvous peers: {error_msg}")
|
|
820
|
+
raise P2PError(f"Failed to get rendezvous peers: {error_msg}")
|
|
821
|
+
elif state == "Success":
|
|
822
|
+
|
|
823
|
+
# The message payload is the full RendezvousState object
|
|
824
|
+
rendezvous_state = result.get('message', {})
|
|
825
|
+
return rendezvous_state
|
|
826
|
+
else:
|
|
827
|
+
logger.error(f"[Instance {self._instance}] GetRendezvousPeers: Received invalid state '{state}'.")
|
|
828
|
+
raise P2PError(f"[Instance {self._instance}] GetRendezvousPeers: Received invalid state.")
|
|
829
|
+
|
|
830
|
+
except Exception as e:
|
|
831
|
+
|
|
832
|
+
# Avoid crashing the polling thread, just log the error
|
|
833
|
+
logger.error(f"❌ Error fetching rendezvous peers info: {e}")
|
|
834
|
+
|
|
835
|
+
# Optionally raise P2PError(f"Failed to get rendezvous peers info") from e if called directly
|
|
836
|
+
return [] # Return empty list on error during polling
|
|
837
|
+
|
|
838
|
+
def get_message_queue_length(self) -> int:
|
|
839
|
+
"""
|
|
840
|
+
Gets the current number of messages in the incoming queue.
|
|
841
|
+
|
|
842
|
+
Returns:
|
|
843
|
+
The number of messages waiting.
|
|
844
|
+
|
|
845
|
+
Raises:
|
|
846
|
+
P2PError: If querying the length fails (should be rare).
|
|
847
|
+
"""
|
|
848
|
+
try:
|
|
849
|
+
|
|
850
|
+
# Call Go function, returns C.int directly
|
|
851
|
+
length_cint = P2P.libp2p.MessageQueueLength(P2P._type_interface.to_go_int(self._instance))
|
|
852
|
+
length = P2P._type_interface.from_go_int(length_cint)
|
|
853
|
+
|
|
854
|
+
# Print(f" Current Message Queue Len: {length}") # Can be noisy
|
|
855
|
+
return length
|
|
856
|
+
except Exception as e:
|
|
857
|
+
|
|
858
|
+
# Avoid crashing polling thread
|
|
859
|
+
logger.error(f"❌ Error fetching message queue length: {e}")
|
|
860
|
+
return -1 # Indicate error
|
|
861
|
+
|
|
862
|
+
# --- Lifecycle Management ---
|
|
863
|
+
|
|
864
|
+
def close(self, close_all: bool = False) -> None:
|
|
865
|
+
"""
|
|
866
|
+
Gracefully shuts down the libp2p node and stops background threads.
|
|
867
|
+
|
|
868
|
+
Args:
|
|
869
|
+
close_all: If True, closes all instances of the node. Default is False.
|
|
870
|
+
"""
|
|
871
|
+
logger.info("🛑 Closing node...")
|
|
872
|
+
|
|
873
|
+
# 1. Signal background threads to stop
|
|
874
|
+
logger.info(" - Stopping background threads...")
|
|
875
|
+
|
|
876
|
+
# 2. Wait briefly for threads to finish (optional, they are daemons)
|
|
877
|
+
# self._get_connected_peers_thread.join(timeout=2)
|
|
878
|
+
# self._check_message_queue_thread.join(timeout=2)
|
|
879
|
+
# print(" - Background threads signaled.")
|
|
880
|
+
|
|
881
|
+
# 3. Call the Go CloseNode function
|
|
882
|
+
try:
|
|
883
|
+
if close_all:
|
|
884
|
+
result_ptr = P2P.libp2p.CloseNode(P2P._type_interface.to_go_int(-1))
|
|
885
|
+
else:
|
|
886
|
+
result_ptr = P2P.libp2p.CloseNode(P2P._type_interface.to_go_int(self._instance))
|
|
887
|
+
result = P2P._type_interface.from_go_ptr_to_json(result_ptr)
|
|
888
|
+
|
|
889
|
+
if result is None:
|
|
890
|
+
logger.error("Node closure failed: received null result.")
|
|
891
|
+
raise P2PError("Node closure failed: received null result.")
|
|
892
|
+
if result.get('state') == "Error":
|
|
893
|
+
logger.error(f"Node closure failed: {result.get('message', 'Unknown Go error')}")
|
|
894
|
+
raise P2PError(f"Node closure failed: {result.get('message', 'Unknown Go error')}")
|
|
895
|
+
|
|
896
|
+
close_msg = f"Node closed successfully ({'all instances' if close_all else f'instance {str(self._instance)}'})."
|
|
897
|
+
logger.info(f"✅ {close_msg}")
|
|
898
|
+
|
|
899
|
+
except Exception as e:
|
|
900
|
+
logger.error(f"❌ Error closing node: {e}")
|
|
901
|
+
raise P2PError(f"Error closing node: {e}") from e
|
|
902
|
+
|
|
903
|
+
# 4. Clear internal state
|
|
904
|
+
self._peer_id = None
|
|
905
|
+
self._address_cache = None
|
|
906
|
+
self._address_cache_time = time.monotonic()
|
|
907
|
+
self._peer_map = {}
|
|
908
|
+
with P2P._instance_lock:
|
|
909
|
+
if close_all:
|
|
910
|
+
|
|
911
|
+
# Also apply the lock here and use the corrected logic
|
|
912
|
+
P2P._instance_ids = [False] * P2P._MAX_INSTANCES
|
|
913
|
+
logger.info("🐍 All instance slots have been marked as free.")
|
|
914
|
+
else:
|
|
915
|
+
if self._instance != -1: # Ensure instance was set
|
|
916
|
+
P2P._instance_ids[self._instance] = False
|
|
917
|
+
logger.info(f"🐍 Instance slot {self._instance} has been marked as free.")
|
|
918
|
+
|
|
919
|
+
logger.info("🐍 Python P2P object state cleared.")
|
|
920
|
+
|
|
921
|
+
return close_msg
|
|
922
|
+
|
|
923
|
+
def __enter__(self):
|
|
924
|
+
"""Enter context manager."""
|
|
925
|
+
return self
|
|
926
|
+
|
|
927
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
928
|
+
"""Exit context manager, ensuring node closure."""
|
|
929
|
+
self.close()
|