unaiverse 0.1.8__cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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.

Files changed (50) hide show
  1. unaiverse/__init__.py +19 -0
  2. unaiverse/agent.py +2008 -0
  3. unaiverse/agent_basics.py +2041 -0
  4. unaiverse/clock.py +191 -0
  5. unaiverse/dataprops.py +1209 -0
  6. unaiverse/hsm.py +1889 -0
  7. unaiverse/modules/__init__.py +18 -0
  8. unaiverse/modules/cnu/__init__.py +17 -0
  9. unaiverse/modules/cnu/cnus.py +536 -0
  10. unaiverse/modules/cnu/layers.py +261 -0
  11. unaiverse/modules/cnu/psi.py +60 -0
  12. unaiverse/modules/hl/__init__.py +15 -0
  13. unaiverse/modules/hl/hl_utils.py +411 -0
  14. unaiverse/modules/networks.py +1509 -0
  15. unaiverse/modules/utils.py +710 -0
  16. unaiverse/networking/__init__.py +16 -0
  17. unaiverse/networking/node/__init__.py +18 -0
  18. unaiverse/networking/node/connpool.py +1261 -0
  19. unaiverse/networking/node/node.py +2299 -0
  20. unaiverse/networking/node/profile.py +447 -0
  21. unaiverse/networking/node/tokens.py +79 -0
  22. unaiverse/networking/p2p/__init__.py +188 -0
  23. unaiverse/networking/p2p/go.mod +127 -0
  24. unaiverse/networking/p2p/go.sum +548 -0
  25. unaiverse/networking/p2p/golibp2p.py +18 -0
  26. unaiverse/networking/p2p/golibp2p.pyi +135 -0
  27. unaiverse/networking/p2p/lib.go +2527 -0
  28. unaiverse/networking/p2p/lib.go.sha256 +1 -0
  29. unaiverse/networking/p2p/lib_types.py +312 -0
  30. unaiverse/networking/p2p/message_pb2.py +63 -0
  31. unaiverse/networking/p2p/messages.py +268 -0
  32. unaiverse/networking/p2p/mylogger.py +77 -0
  33. unaiverse/networking/p2p/p2p.py +929 -0
  34. unaiverse/networking/p2p/proto-go/message.pb.go +616 -0
  35. unaiverse/networking/p2p/unailib.cpython-312-aarch64-linux-gnu.so +0 -0
  36. unaiverse/streamlib/__init__.py +15 -0
  37. unaiverse/streamlib/streamlib.py +210 -0
  38. unaiverse/streams.py +770 -0
  39. unaiverse/utils/__init__.py +16 -0
  40. unaiverse/utils/ask_lone_wolf.json +27 -0
  41. unaiverse/utils/lone_wolf.json +19 -0
  42. unaiverse/utils/misc.py +492 -0
  43. unaiverse/utils/sandbox.py +293 -0
  44. unaiverse/utils/server.py +435 -0
  45. unaiverse/world.py +353 -0
  46. unaiverse-0.1.8.dist-info/METADATA +365 -0
  47. unaiverse-0.1.8.dist-info/RECORD +50 -0
  48. unaiverse-0.1.8.dist-info/WHEEL +7 -0
  49. unaiverse-0.1.8.dist-info/licenses/LICENSE +43 -0
  50. unaiverse-0.1.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,447 @@
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 json
16
+ import psutil
17
+ import hashlib
18
+ import platform
19
+ import datetime
20
+ import requests
21
+ import ipaddress
22
+ from datetime import timezone
23
+
24
+
25
+ class NodeProfile:
26
+ """
27
+ Profile information for a node.
28
+ """
29
+
30
+ def __init__(self,
31
+ static: dict,
32
+ dynamic: dict,
33
+ cv: dict):
34
+
35
+ # Checking provided data
36
+ if not static:
37
+ raise ValueError("Missing static profile data")
38
+
39
+ # Forcing key order (important! otherwise the hash operation will not be consistent with the one on the server)
40
+ cv = [{k: _cv[k] for k in sorted(_cv)} for _cv in cv]
41
+
42
+ self._profile_data = \
43
+ {
44
+ 'static': {
45
+ 'node_id': None,
46
+ 'node_type': None,
47
+ 'node_name': None,
48
+ 'node_description': None,
49
+ 'created_utc': None,
50
+ 'name': None,
51
+ 'surname': None,
52
+ 'title': None,
53
+ 'organization': None,
54
+ 'email': None,
55
+ 'max_nr_connections': None,
56
+ 'allowed_node_ids': None,
57
+ 'world_masters_node_ids': None,
58
+ 'certified': None,
59
+ 'inspector_node_id': None
60
+ },
61
+ 'dynamic': {
62
+ 'os': None,
63
+ 'cpu_cores': None,
64
+ 'logical_cpus': None,
65
+ 'memory_gb': None,
66
+ 'memory_avail': None,
67
+ 'memory_used': None,
68
+ 'timestamp': None,
69
+ 'public_ip_address': None,
70
+ 'guessed_location': None,
71
+ 'peer_id': None,
72
+ 'peer_addresses': None,
73
+ 'private_peer_id': None,
74
+ 'private_peer_addresses': None,
75
+ 'proc_inputs': None,
76
+ 'proc_outputs': None,
77
+ 'streams': None,
78
+ 'connections': {
79
+ 'public_agents': None, # List of dict
80
+ 'world_agents': None, # List of dict
81
+ 'world_masters': None, # List of dict
82
+ 'world_peer_id': None, # Str
83
+ 'role': None # Str
84
+ },
85
+ 'world_summary': {
86
+ "world_title": None,
87
+ "world_agents": None,
88
+ "world_masters": None,
89
+ "world_agents_count": None,
90
+ "world_masters_count": None,
91
+ "total_agents": None,
92
+ "agent_badges_count": None,
93
+ "agent_badges": None,
94
+ "streams_count": None
95
+ },
96
+ "world_roles_fsm": None, # Dict of FSMs for world roles
97
+ "world_stats_dynamic": None,
98
+ "hidden": None
99
+ },
100
+ 'cv': cv
101
+ }
102
+
103
+ # Checking the presence of basic static profile info
104
+ for k in self._profile_data['static'].keys():
105
+ if (k not in static and k != "certified" and
106
+ k != "allowed_node_ids" and k != "world_masters_node_ids" and k != "inspector_node_id"): # Patch
107
+ raise ValueError("Missing required static profile info: " + str(k))
108
+
109
+ # Filling static profile info (there might be more information that the one shown above)
110
+ for k, v in static.items():
111
+ self._profile_data['static'][k] = v
112
+
113
+ # Including the provided dynamic info, only considering the expected keys
114
+ # (the provided "dynamic" argument will contain all or just a sub-portion of the expected keys)
115
+ for k, v in dynamic.items():
116
+ if k == 'connections' and v is not None and isinstance(v, dict):
117
+ for kk, vv in v.items():
118
+ if (kk in self._profile_data['dynamic']['connections'] and
119
+ self._profile_data['dynamic']['connections'][kk] is None):
120
+ self._profile_data['dynamic']['connections'][kk] = vv
121
+ elif k == 'world_summary' and v is not None and isinstance(v, dict):
122
+ for kk, vv in v.items():
123
+ if (kk in self._profile_data['dynamic']['world_summary'] and
124
+ self._profile_data['dynamic']['world_summary'][kk] is None):
125
+ self._profile_data['dynamic']['world_summary'][kk] = vv
126
+ elif k in self._profile_data['dynamic'] and self._profile_data['dynamic'][k] is None:
127
+ self._profile_data['dynamic'][k] = v
128
+ elif k.startswith('tmp_'):
129
+ self._profile_data['dynamic'][k] = v
130
+
131
+ # Internally required attributes
132
+ self._profile_last_updated = None # Will be set by calling _fill_missing_specs or check_and_update_specs
133
+ self._geolocation_cache = {} # Will be needed to avoid too many IP-related lookups
134
+
135
+ # Filling the missing information (machine-level information, specs) that can be automatically extracted
136
+ self._fill_missing_specs()
137
+
138
+ # Flag
139
+ self._connections_updated = False
140
+
141
+ def update_cv(self, new_cv):
142
+ self._profile_data['cv'] = new_cv
143
+
144
+ @classmethod
145
+ def from_dict(cls, combined_data: dict) -> 'NodeProfile':
146
+ """Factory method to create a NodeProfile instance from a dictionary
147
+ containing combined profile data (static, specs, and CV list of dicts).
148
+
149
+ Args:
150
+ combined_data (dict): A dictionary representing the node profile,
151
+ typically loaded from JSON or received over the network.
152
+ Expected to contain 'node_id', 'cv' (list of dicts),
153
+ 'node_specification' (dict), 'peer_id', 'peer_addresses'
154
+ and other profile keys.
155
+
156
+ Returns:
157
+ NodeProfile: A new instance of NodeProfile populated from the dictionary.
158
+
159
+ Raises:
160
+ ValueError: If 'node_id' is missing in the input dictionary.
161
+ TypeError: If the 'cv' data is present but not a list.
162
+ """
163
+
164
+ # Ensure essential 'node_id' is present
165
+ node_id = combined_data.get('static').get('node_id')
166
+ if not node_id:
167
+ raise ValueError("Input dictionary must contain a 'node_id'.")
168
+
169
+ profile_instance = cls(
170
+ static=combined_data['static'],
171
+ dynamic=combined_data['dynamic'],
172
+ cv=combined_data['cv']
173
+ )
174
+
175
+ return profile_instance
176
+
177
+ # Get operating system information
178
+ @staticmethod
179
+ def _get_os_spec():
180
+ """Extracts operating system information."""
181
+ return platform.platform()
182
+
183
+ # Get cpu information
184
+ @staticmethod
185
+ def _get_cpu_info():
186
+ """Extracts CPU core information."""
187
+ try:
188
+ return {
189
+ 'physical_cores': psutil.cpu_count(logical=False),
190
+ 'logical_cores': psutil.cpu_count(logical=True)
191
+ }
192
+ except Exception as e:
193
+ print(f"Error getting CPU info: {e}")
194
+ return {'physical_cores': None, 'logical_cores': None}
195
+
196
+ # Get memory information
197
+ @staticmethod
198
+ def _get_memory_info():
199
+ """Extracts memory information in GB."""
200
+ try:
201
+ mem = psutil.virtual_memory()
202
+ total_gb = mem.total / (1024 ** 3)
203
+ available_gb = mem.available / (1024 ** 3)
204
+ used_gb = mem.used / (1024 ** 3)
205
+ return {
206
+ 'total': float(total_gb),
207
+ 'available': float(available_gb),
208
+ 'used': float(used_gb)
209
+ }
210
+ except Exception as e:
211
+ print(f"Error getting memory info: {e}")
212
+ return {'total': 0.0, 'available': 0.0, 'used': 0.0}
213
+
214
+ # Get public ip address
215
+ @staticmethod
216
+ def _get_public_ip_address() -> str | None:
217
+ """Attempts to retrieve the public IP address using an external web service.
218
+ Uses multiple services as fallbacks.
219
+ Returns the public IP address string or None if retrieval fails.
220
+ """
221
+
222
+ # List of reliable services that return the public IP as plain text
223
+ services = [
224
+ "https://api.ipify.org",
225
+ "https://icanhazip.com",
226
+ "https://ident.me",
227
+ "https://checkip.amazonaws.com",
228
+ ]
229
+
230
+ # Print("Attempting to retrieve public IP address...")
231
+ for url in services:
232
+ try:
233
+
234
+ # Make a GET request to the service URL with a timeout
235
+ response = requests.get(url, timeout=5)
236
+
237
+ # Raise an HTTPError for bad responses (4xx or 5xx status codes)
238
+ response.raise_for_status()
239
+
240
+ # Get the response text, which should be the IP address, and strip any whitespace
241
+ public_ip = response.text.strip()
242
+
243
+ # Basic validation - check if the result looks like a valid IP address
244
+ try:
245
+ ipaddress.ip_address(public_ip) # This checks if it's a valid IPv4 or IPv6 address
246
+
247
+ return public_ip # Return the first valid IP found
248
+
249
+ except ValueError:
250
+
251
+ # If ipaddress.ip_address raises ValueError, it's not a valid format
252
+ continue # Try the next service if validation fails
253
+
254
+ except requests.exceptions.RequestException:
255
+
256
+ # Catch any request-related errors (e.g., network issues, timeout, bad status)
257
+ continue # Try the next service on error
258
+
259
+ except Exception:
260
+
261
+ # Catch any other unexpected errors
262
+ continue # Try the next service on error
263
+
264
+ return 'Public IP not available.' # Return None if all services fail
265
+
266
+ # Get guessed location based on IP address
267
+ def _get_geolocation_from_ip(self, ip_address):
268
+ """Retrieves geolocation data (same as before)."""
269
+
270
+ # Added a check for local/private IPs to avoid unnecessary API calls
271
+ try:
272
+ ip_obj = ipaddress.ip_address(ip_address)
273
+ if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_unspecified:
274
+ return {"message": "Private, loopback, or unspecified IP address. Geolocation not applicable."}
275
+ except ValueError:
276
+ return {"error": f"Invalid IP address format: {ip_address}"}
277
+
278
+ # Added a simple cache to avoid repeated API calls for the same IP
279
+ if hasattr(self, '_geolocation_cache') and ip_address in self._geolocation_cache:
280
+
281
+ # Print(f"Using cached geolocation for {ip_address}") # Optional: for debugging
282
+ return self._geolocation_cache[ip_address]
283
+
284
+ try:
285
+ url = f"http://ip-api.com/json/{ip_address}"
286
+ response = requests.get(url)
287
+ response.raise_for_status()
288
+ data = response.json()
289
+ if data.get("status") == "success":
290
+ geo_data = {
291
+ "country": data.get("country"),
292
+ "countryCode": data.get("countryCode"),
293
+ "region": data.get("region"),
294
+ "regionName": data.get("regionName"),
295
+ "city": data.get("city"),
296
+ "zip": data.get("zip"),
297
+ "latitude": data.get("lat"),
298
+ "longitude": data.get("lon"),
299
+ "timezone": data.get("timezone"),
300
+ "isp": data.get("isp")
301
+ }
302
+
303
+ # Cache the result
304
+ if not hasattr(self, '_geolocation_cache'):
305
+ self._geolocation_cache = {}
306
+ self._geolocation_cache[ip_address] = geo_data
307
+ return geo_data
308
+ else:
309
+ error_data = {"error": data.get("message", "Geolocation lookup failed.")}
310
+
311
+ # Cache the error result too
312
+ if not hasattr(self, '_geolocation_cache'):
313
+ self._geolocation_cache = {}
314
+ self._geolocation_cache[ip_address] = error_data
315
+ return error_data
316
+
317
+ except requests.exceptions.RequestException as e:
318
+ error_data = {"error": f"Request failed: {e}"}
319
+ if not hasattr(self, '_geolocation_cache'):
320
+ self._geolocation_cache = {}
321
+ self._geolocation_cache[ip_address] = error_data
322
+ return error_data
323
+
324
+ except json.JSONDecodeError:
325
+ error_data = {"error": "Failed to decode JSON response from geolocation API"}
326
+ if not hasattr(self, '_geolocation_cache'):
327
+ self._geolocation_cache = {}
328
+ self._geolocation_cache[ip_address] = error_data
329
+ return error_data
330
+
331
+ except Exception as e:
332
+ error_data = {"error": f"An unexpected error occurred during geolocation lookup: {e}"}
333
+ if not hasattr(self, '_geolocation_cache'):
334
+ self._geolocation_cache = {}
335
+ self._geolocation_cache[ip_address] = error_data
336
+ return error_data
337
+
338
+ # This is the function that collects all the information for the 'node_specification'
339
+ def _get_current_specs(self) -> dict:
340
+ """Gathers current system specifications.
341
+ """
342
+ cpu_info = self._get_cpu_info()
343
+ memory_info = self._get_memory_info()
344
+
345
+ return {
346
+ 'timestamp': datetime.datetime.now(timezone.utc).isoformat(),
347
+ 'os': self._get_os_spec(),
348
+ 'cpu_cores': cpu_info.get('physical_cores'),
349
+ 'logical_cpus': cpu_info.get('logical_cores'),
350
+ 'memory_gb': memory_info.get('total'),
351
+ 'memory_avail': memory_info.get('available'),
352
+ 'memory_used': memory_info.get('used'),
353
+ 'public_ip_address': self._get_public_ip_address(),
354
+ 'guessed_location': self._get_geolocation_from_ip(self._get_public_ip_address()),
355
+ }
356
+
357
+ def _fill_missing_specs(self):
358
+ dynamic_profile = self.get_dynamic_profile()
359
+ current_specs = None
360
+ for k in dynamic_profile.keys():
361
+ if dynamic_profile[k] is None:
362
+ if current_specs is None:
363
+ current_specs = self._get_current_specs()
364
+ if k in current_specs:
365
+ dynamic_profile[k] = current_specs[k]
366
+
367
+ self._profile_last_updated = datetime.datetime.now(timezone.utc) # Mark profile as checked/updated
368
+
369
+ def check_and_update_specs(self, update_only: bool = True) -> bool:
370
+ """Checks current specs against saved specs. Updates profile data."""
371
+
372
+ current_specs = self._get_current_specs()
373
+ specs_changed = False
374
+
375
+ if update_only:
376
+ self._profile_data['dynamic'] |= current_specs
377
+ else:
378
+ saved_specs = self._profile_data['dynamic'].copy()
379
+ change_details = []
380
+
381
+ if saved_specs is None:
382
+
383
+ # No previous specification exists, capture the current one
384
+ self._profile_data['dynamic'] |= current_specs
385
+ specs_changed = True
386
+ change_details.append("Initial specification captured")
387
+
388
+ else:
389
+
390
+ # Compare current specs with saved specs (ignore timestamp for comparison)
391
+ keys_to_compare = current_specs.keys()
392
+
393
+ for key in keys_to_compare:
394
+ if key == 'timestamp':
395
+ continue
396
+
397
+ saved_value = saved_specs.get(key)
398
+ current_value = current_specs.get(key)
399
+
400
+ # Handle float comparison with tolerance
401
+ if isinstance(saved_value, float) and isinstance(current_value, float):
402
+ if abs(current_value - saved_value) > 1e-6: # Tolerance for float changes
403
+ change_details.append(f"{key}: from {saved_value:.2f} to {current_value:.2f}")
404
+ specs_changed = True
405
+
406
+ elif saved_value != current_value:
407
+ change_details.append(f"{key}: from {saved_value} to {current_value}")
408
+ specs_changed = True
409
+
410
+ # Comparing total resources (OS, CPU, total RAM/Disk) is more typical for 'specification' changes.
411
+ if specs_changed:
412
+
413
+ # Update the specification in the profile data with the new current specs
414
+ self._profile_data['dynamic'] |= current_specs
415
+ change_summary = ", ".join(change_details)
416
+ print(f"Specs changed for '{self._profile_data['static']['node_id']}': {change_summary}")
417
+
418
+ self._profile_last_updated = datetime.datetime.now(timezone.utc) # Mark profile as checked/updated
419
+
420
+ return specs_changed
421
+
422
+ # Get profile data as dict: cv, dynamic_profile, static_profile
423
+ def get_static_profile(self) -> dict:
424
+ return self._profile_data['static']
425
+
426
+ def get_dynamic_profile(self) -> dict:
427
+ return self._profile_data['dynamic']
428
+
429
+ def get_cv(self):
430
+ return self._profile_data['cv']
431
+
432
+ def get_all_profile(self):
433
+ return self._profile_data
434
+
435
+ def mark_change_in_connections(self):
436
+ self._connections_updated = True
437
+
438
+ def unmark_change_in_connections(self):
439
+ self._connections_updated = False
440
+
441
+ def connections_changed(self):
442
+ return self._connections_updated
443
+
444
+ def verify_cv_hash(self, cv_hash: str):
445
+ computed_hash = hashlib.blake2b(json.dumps(self._profile_data['cv']).encode("utf-8"),
446
+ digest_size=16).hexdigest()
447
+ return cv_hash == computed_hash, (cv_hash, computed_hash)
@@ -0,0 +1,79 @@
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 jwt
16
+
17
+
18
+ class TokenVerifier:
19
+ def __init__(self, public_key: str | bytes):
20
+ """Initializes the `TokenVerifier` with a public key.
21
+
22
+ This key is essential for securely decoding and verifying JSON Web Tokens (JWTs) issued by a corresponding
23
+ private key. The public key can be provided as either a string or a bytes object.
24
+
25
+ Args:
26
+ public_key: The public key used for decoding and verification.
27
+ """
28
+ self.public_key = public_key
29
+
30
+ def verify_token(self, token: str | bytes,
31
+ node_id: str | None = None, ip: str | None = None,
32
+ hostname: str | None = None,
33
+ port: int | None = None,
34
+ p2p_peer: str | None = None):
35
+ """Verifies a JSON Web Token (JWT) against a set of criteria.
36
+
37
+ The method first attempts to decode the token using the provided public key and the RS256 algorithm,
38
+ handling `DecodeError` and `ExpiredSignatureError`. It then performs optional checks to ensure that
39
+ the token's payload matches specific network identifiers, such as `node_id`, `ip`, `hostname`, and `port`.
40
+ It can also verify if a specific peer is present in the token's list of `p2p_peers`.
41
+
42
+ Args:
43
+ token: The JWT to verify, as a string or bytes object.
44
+ node_id: Optional `node_id` to check against the token's payload.
45
+ ip: Optional IP address to check.
46
+ hostname: Optional hostname to check.
47
+ port: Optional port number to check.
48
+ p2p_peer: Optional peer identifier to check within the `p2p_peers` list.
49
+
50
+ Returns:
51
+ A tuple containing the `node_id` and `cv_hash` from the token's payload if all checks pass. Otherwise,
52
+ it returns a tuple of `(None, None)`.
53
+ """
54
+
55
+ # Decoding token using the public key
56
+ try:
57
+ payload = jwt.decode(token, self.public_key, algorithms=["RS256"])
58
+ except jwt.DecodeError as e:
59
+ return None, None
60
+ except jwt.ExpiredSignatureError as e: # This checks expiration time (required)
61
+ return None, None
62
+
63
+ # Checking optional information
64
+ if node_id is not None and payload["node_id"] != node_id:
65
+ return None, None
66
+ if ip is not None and payload["ip"] != ip:
67
+ return None, None
68
+ if hostname is not None and payload["hostname"] != hostname:
69
+ return None, None
70
+ if port is not None and payload["port"] != port:
71
+ return None, None
72
+ if p2p_peer is not None and p2p_peer not in payload["p2p_peers"]:
73
+ return None, None
74
+
75
+ # All ok
76
+ return payload["node_id"], payload["cv_hash"]
77
+
78
+ def __str__(self):
79
+ return f"[{self.__class__.__name__}] public_key: {self.public_key[0:50] + b'...'}"