astreum 0.1.20__py3-none-any.whl → 0.2.0__py3-none-any.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 astreum might be problematic. Click here for more details.

Files changed (41) hide show
  1. astreum/__init__.py +1 -2
  2. astreum/{node → _node}/relay/envelope.py +1 -1
  3. astreum/{node → _node}/relay/message.py +1 -1
  4. astreum/{node → _node}/storage/merkle.py +3 -3
  5. astreum/{node → _node}/storage/patricia.py +3 -3
  6. astreum/{node → _node}/storage/storage.py +2 -0
  7. astreum/{node → _node}/utils.py +1 -1
  8. astreum/{node → _node}/validation/account.py +3 -3
  9. astreum/{node → _node}/validation/transaction.py +3 -3
  10. astreum/lispeum/__init__.py +2 -0
  11. astreum/lispeum/parser.py +1 -1
  12. astreum/machine/environment.py +0 -25
  13. astreum/node.py +1021 -0
  14. astreum-0.2.0.dist-info/METADATA +144 -0
  15. astreum-0.2.0.dist-info/RECORD +57 -0
  16. {astreum-0.1.20.dist-info → astreum-0.2.0.dist-info}/WHEEL +1 -1
  17. astreum/utils/__init__.py +0 -0
  18. astreum-0.1.20.dist-info/METADATA +0 -90
  19. astreum-0.1.20.dist-info/RECORD +0 -57
  20. /astreum/{node → _node}/__init__.py +0 -0
  21. /astreum/{node → _node}/relay/__init__.py +0 -0
  22. /astreum/{node → _node}/relay/bucket.py +0 -0
  23. /astreum/{node → _node}/relay/peer.py +0 -0
  24. /astreum/{node → _node}/relay/route.py +0 -0
  25. /astreum/{node/crypto → _node/storage}/__init__.py +0 -0
  26. /astreum/{node → _node}/storage/utils.py +0 -0
  27. /astreum/{node/storage → _node/validation}/__init__.py +0 -0
  28. /astreum/{node/validation → _node/validation/_block}/__init__.py +0 -0
  29. /astreum/{node → _node}/validation/_block/create.py +0 -0
  30. /astreum/{node → _node}/validation/_block/model.py +0 -0
  31. /astreum/{node → _node}/validation/_block/validate.py +0 -0
  32. /astreum/{node → _node}/validation/block.py +0 -0
  33. /astreum/{node → _node}/validation/constants.py +0 -0
  34. /astreum/{node → _node}/validation/stake.py +0 -0
  35. /astreum/{node → _node}/validation/vdf.py +0 -0
  36. /astreum/{node/validation/_block → crypto}/__init__.py +0 -0
  37. /astreum/{node/crypto → crypto}/ed25519.py +0 -0
  38. /astreum/{node/crypto → crypto}/x25519.py +0 -0
  39. /astreum/{utils/bytes_format.py → format.py} +0 -0
  40. {astreum-0.1.20.dist-info → astreum-0.2.0.dist-info}/licenses/LICENSE +0 -0
  41. {astreum-0.1.20.dist-info → astreum-0.2.0.dist-info}/top_level.txt +0 -0
astreum/node.py ADDED
@@ -0,0 +1,1021 @@
1
+ import socket
2
+ import threading
3
+ import time
4
+ from queue import Queue
5
+ from pathlib import Path
6
+ from typing import Tuple, Dict, Union, Optional, List
7
+ from datetime import datetime, timedelta, timezone
8
+ import uuid
9
+ from astreum import format
10
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
11
+ from cryptography.hazmat.primitives import serialization
12
+ from astreum.crypto import ed25519, x25519
13
+ from enum import IntEnum
14
+ import blake3
15
+ import struct
16
+
17
+ class ObjectRequestType(IntEnum):
18
+ OBJECT_GET = 0
19
+ OBJECT_PUT = 1
20
+
21
+ class ObjectRequest:
22
+ type: ObjectRequestType
23
+ data: bytes
24
+ hash: bytes
25
+
26
+ def __init__(self, type: ObjectRequestType, data: bytes, hash: bytes = None):
27
+ self.type = type
28
+ self.data = data
29
+ self.hash = hash
30
+
31
+ def to_bytes(self):
32
+ return format.encode([self.type.value, self.data, self.hash])
33
+
34
+ @classmethod
35
+ def from_bytes(cls, data: bytes):
36
+ type_val, data_val, hash_val = format.decode(data)
37
+ return cls(type=ObjectRequestType(type_val[0]), data=data_val, hash=hash_val)
38
+
39
+ class ObjectResponseType(IntEnum):
40
+ OBJECT_FOUND = 0
41
+ OBJECT_PROVIDER = 1
42
+ OBJECT_NEAREST_PEER = 2
43
+
44
+ class ObjectResponse:
45
+ type: ObjectResponseType
46
+ data: bytes
47
+ hash: bytes
48
+
49
+ def __init__(self, type: ObjectResponseType, data: bytes, hash: bytes = None):
50
+ self.type = type
51
+ self.data = data
52
+ self.hash = hash
53
+
54
+ def to_bytes(self):
55
+ return format.encode([self.type.value, self.data, self.hash])
56
+
57
+ @classmethod
58
+ def from_bytes(cls, data: bytes):
59
+ type_val, data_val, hash_val = format.decode(data)
60
+ return cls(type=ObjectResponseType(type_val[0]), data=data_val, hash=hash_val)
61
+
62
+ class MessageTopic(IntEnum):
63
+ PING = 0
64
+ OBJECT_REQUEST = 1
65
+ OBJECT_RESPONSE = 2
66
+ ROUTE_REQUEST = 3
67
+ ROUTE_RESPONSE = 4
68
+
69
+ class Message:
70
+ body: bytes
71
+ topic: MessageTopic
72
+
73
+ def to_bytes(self):
74
+ return format.encode([self.body, [self.topic.value]])
75
+
76
+ @classmethod
77
+ def from_bytes(cls, data: bytes):
78
+ body, topic = format.decode(data)
79
+ return cls(body=body, topic=MessageTopic(topic[0]))
80
+
81
+ class Envelope:
82
+ encrypted: bool
83
+ message: Message
84
+ nonce: int
85
+ sender: X25519PublicKey
86
+ timestamp: datetime
87
+
88
+ def __init__(self, message: Message, sender: X25519PublicKey, encrypted: bool = False, nonce: int = 0, timestamp: Union[int, datetime, None] = None, difficulty: int = 1):
89
+ self.encrypted = encrypted
90
+ encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
91
+
92
+ self.message = message
93
+
94
+ self.sender = sender
95
+ self.sender_bytes = sender.public_bytes()
96
+
97
+ self.nonce = nonce
98
+
99
+ if timestamp is None:
100
+ self.timestamp = datetime.now(timezone.utc)
101
+ timestamp_int = int(self.timestamp.timestamp())
102
+ elif isinstance(timestamp, int):
103
+ self.timestamp = datetime.fromtimestamp(timestamp, timezone.utc)
104
+ timestamp_int = timestamp
105
+ elif isinstance(timestamp, datetime):
106
+ self.timestamp = timestamp
107
+ timestamp_int = int(timestamp.timestamp())
108
+ else:
109
+ raise TypeError("Timestamp must be an int (Unix timestamp), datetime object, or None")
110
+
111
+ def count_leading_zero_bits(data: bytes) -> int:
112
+ count = 0
113
+ for b in data:
114
+ if b == 0:
115
+ count += 8
116
+ else:
117
+ count += 8 - b.bit_length()
118
+ break
119
+ return count
120
+
121
+ while True:
122
+ envelope_bytes = format.encode([
123
+ encrypted_bytes,
124
+ message_bytes,
125
+ self.nonce,
126
+ self.sender_bytes,
127
+ timestamp_int
128
+ ])
129
+ envelope_hash = utils.blake3_hash(envelope_bytes)
130
+ if count_leading_zero_bits(envelope_hash) >= difficulty:
131
+ self.hash = envelope_hash
132
+ break
133
+ self.nonce += 1
134
+
135
+ def to_bytes(self):
136
+ encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
137
+
138
+ return format.encode([
139
+ encrypted_bytes,
140
+ self.message.to_bytes(),
141
+ self.nonce,
142
+ self.sender.public_bytes(),
143
+ int(self.timestamp.timestamp())
144
+ ])
145
+
146
+ @classmethod
147
+ def from_bytes(cls, data: bytes):
148
+ encrypted_bytes, message_bytes, nonce, sender_bytes, timestamp_int = format.decode(data)
149
+ return cls(
150
+ encrypted=(encrypted_bytes == b'\x01'),
151
+ message=Message.from_bytes(message_bytes),
152
+ nonce=nonce,
153
+ sender=X25519PublicKey.from_public_bytes(sender_bytes),
154
+ timestamp=datetime.fromtimestamp(timestamp_int, timezone.utc)
155
+ )
156
+
157
+ class Peer:
158
+ def __init__(self, node_secret_key: X25519PrivateKey, peer_public_key: X25519PublicKey, address: Tuple[str, int]):
159
+ self.shared_key = x25519.generate_shared_key(node_secret_key, peer_public_key)
160
+ self.address = address
161
+ self.timestamp = datetime.now(timezone.utc)
162
+
163
+ class Route:
164
+ def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
165
+ self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
166
+ self.bucket_size = bucket_size
167
+ self.buckets: Dict[int, List[X25519PublicKey]] = {
168
+ i: [] for i in range(len(self.relay_public_key_bytes) * 8)
169
+ }
170
+ self.peers = {}
171
+
172
+ @staticmethod
173
+ def _matching_leading_bits(a: bytes, b: bytes) -> int:
174
+ for byte_index, (ba, bb) in enumerate(zip(a, b)):
175
+ diff = ba ^ bb
176
+ if diff:
177
+ return byte_index * 8 + (8 - diff.bit_length())
178
+ return len(a) * 8
179
+
180
+ def add_peer(self, peer_public_key: X25519PublicKey):
181
+ peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
182
+ bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
183
+ if len(self.buckets[bucket_idx]) < self.bucket_size:
184
+ self.buckets[bucket_idx].append(peer_public_key)
185
+
186
+
187
+ def encode_ip_address(host: str, port: int) -> bytes:
188
+ ip_bytes = socket.inet_pton(socket.AF_INET6 if ':' in host else socket.AF_INET, host)
189
+ port_bytes = struct.pack("!H", port)
190
+ return ip_bytes + port_bytes
191
+
192
+ def decode_ip_address(data: bytes) -> tuple[str, int]:
193
+ if len(data) == 6:
194
+ ip = socket.inet_ntop(socket.AF_INET, data[:4])
195
+ port = struct.unpack("!H", data[4:6])[0]
196
+ elif len(data) == 18:
197
+ ip = socket.inet_ntop(socket.AF_INET6, data[:16])
198
+ port = struct.unpack("!H", data[16:18])[0]
199
+ else:
200
+ raise ValueError("Invalid address byte format")
201
+ return ip, port
202
+
203
+ # =========
204
+ # MACHINE
205
+ # =========
206
+
207
+ class Expr:
208
+ class ListExpr:
209
+ def __init__(self, elements: List['Expr']):
210
+ self.elements = elements
211
+
212
+ def __eq__(self, other):
213
+ if not isinstance(other, Expr.ListExpr):
214
+ return NotImplemented
215
+ return self.elements == other.elements
216
+
217
+ def __ne__(self, other):
218
+ return not self.__eq__(other)
219
+
220
+ @property
221
+ def value(self):
222
+ inner = " ".join(str(e) for e in self.elements)
223
+ return f"({inner})"
224
+
225
+
226
+ def __repr__(self):
227
+ if not self.elements:
228
+ return "()"
229
+
230
+ inner = " ".join(str(e) for e in self.elements)
231
+ return f"({inner})"
232
+
233
+ def __iter__(self):
234
+ return iter(self.elements)
235
+
236
+ def __getitem__(self, index: Union[int, slice]):
237
+ return self.elements[index]
238
+
239
+ def __len__(self):
240
+ return len(self.elements)
241
+
242
+ class Symbol:
243
+ def __init__(self, value: str):
244
+ self.value = value
245
+
246
+ def __repr__(self):
247
+ return self.value
248
+
249
+ class Integer:
250
+ def __init__(self, value: int):
251
+ self.value = value
252
+
253
+ def __repr__(self):
254
+ return str(self.value)
255
+
256
+ class String:
257
+ def __init__(self, value: str):
258
+ self.value = value
259
+
260
+ def __repr__(self):
261
+ return f'"{self.value}"'
262
+
263
+ class Boolean:
264
+ def __init__(self, value: bool):
265
+ self.value = value
266
+
267
+ def __repr__(self):
268
+ return "true" if self.value else "false"
269
+
270
+ class Function:
271
+ def __init__(self, params: List[str], body: 'Expr'):
272
+ self.params = params
273
+ self.body = body
274
+
275
+ def __repr__(self):
276
+ params_str = " ".join(self.params)
277
+ body_str = str(self.body)
278
+ return f"(fn ({params_str}) {body_str})"
279
+
280
+ class Error:
281
+ def __init__(self, message: str, origin: 'Expr' | None = None):
282
+ self.message = message
283
+ self.origin = origin
284
+
285
+ def __repr__(self):
286
+ if self.origin is None:
287
+ return f'(error "{self.message}")'
288
+ return f'(error "{self.message}" in {self.origin})'
289
+
290
+ class Env:
291
+ def __init__(self, parent: 'Env' = None):
292
+ self.data: Dict[str, Expr] = {}
293
+ self.parent = parent
294
+
295
+ def put(self, name: str, value: Expr):
296
+ self.data[name] = value
297
+
298
+ def get(self, name: str) -> Optional[Expr]:
299
+ if name in self.data:
300
+ return self.data[name]
301
+ elif self.parent is not None:
302
+ return self.parent.get(name)
303
+ else:
304
+ return None
305
+
306
+ def __repr__(self):
307
+ return f"Env({self.data})"
308
+
309
+
310
+ class Node:
311
+ def __init__(self, config: dict):
312
+ self._machine_setup()
313
+ machine_only = bool(config.get('machine-only', True))
314
+ if not machine_only:
315
+ self._storage_setup(config=config)
316
+ self._relay_setup(config=config)
317
+
318
+ # STORAGE METHODS
319
+ def _storage_setup(self, config: dict):
320
+ storage_path_str = config.get('storage_path')
321
+ if storage_path_str is None:
322
+ self.storage_path = None
323
+ self.memory_storage = {}
324
+ else:
325
+ self.storage_path = Path(storage_path_str)
326
+ self.storage_path.mkdir(parents=True, exist_ok=True)
327
+ self.memory_storage = None
328
+
329
+ self.storage_get_relay_timeout = config.get('storage_get_relay_timeout', 5)
330
+ # STORAGE INDEX: (object_hash, encoded (provider_public_key, provider_address))
331
+ self.storage_index = Dict[bytes, bytes]
332
+
333
+ def _relay_setup(self, config: dict):
334
+ self.use_ipv6 = config.get('use_ipv6', False)
335
+ incoming_port = config.get('incoming_port', 7373)
336
+
337
+ if 'relay_secret_key' in config:
338
+ try:
339
+ private_key_bytes = bytes.fromhex(config['relay_secret_key'])
340
+ self.relay_secret_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key_bytes)
341
+ except Exception as e:
342
+ raise Exception(f"Error loading relay secret key provided: {e}")
343
+ else:
344
+ self.relay_secret_key = ed25519.Ed25519PrivateKey.generate()
345
+
346
+ self.relay_public_key = self.relay_secret_key.public_key()
347
+
348
+ if 'validation_secret_key' in config:
349
+ try:
350
+ private_key_bytes = bytes.fromhex(config['validation_secret_key'])
351
+ self.validation_secret_key = x25519.X25519PrivateKey.from_private_bytes(private_key_bytes)
352
+ except Exception as e:
353
+ raise Exception(f"Error loading validation secret key provided: {e}")
354
+
355
+ # setup peer route and validation route
356
+ self.peer_route = Route(self.relay_public_key)
357
+ if self.validation_secret_key:
358
+ self.validation_route = Route(self.relay_public_key)
359
+
360
+ # Choose address family based on IPv4 or IPv6
361
+ family = socket.AF_INET6 if self.use_ipv6 else socket.AF_INET
362
+
363
+ self.incoming_socket = socket.socket(family, socket.SOCK_DGRAM)
364
+ if self.use_ipv6:
365
+ self.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
366
+ bind_address = "::" if self.use_ipv6 else "0.0.0.0"
367
+ self.incoming_socket.bind((bind_address, incoming_port or 0))
368
+ self.incoming_port = self.incoming_socket.getsockname()[1]
369
+ self.incoming_queue = Queue()
370
+
371
+ self.incoming_populate_thread = threading.Thread(target=self._relay_incoming_queue_populating)
372
+ self.incoming_populate_thread.daemon = True
373
+ self.incoming_populate_thread.start()
374
+
375
+ self.incoming_process_thread = threading.Thread(target=self._relay_incoming_queue_processing)
376
+ self.incoming_process_thread.daemon = True
377
+ self.incoming_process_thread.start()
378
+
379
+ # outgoing thread
380
+ self.outgoing_socket = socket.socket(family, socket.SOCK_DGRAM)
381
+ self.outgoing_queue = Queue()
382
+ self.outgoing_thread = threading.Thread(target=self._relay_outgoing_queue_processor)
383
+ self.outgoing_thread.daemon = True
384
+ self.outgoing_thread.start()
385
+
386
+ self.object_request_queue = Queue()
387
+
388
+ self.peer_manager_thread = threading.Thread(target=self._relay_peer_manager)
389
+ self.peer_manager_thread.daemon = True
390
+ self.peer_manager_thread.start()
391
+
392
+ self.peers = Dict[X25519PublicKey, Peer]
393
+
394
+ if 'bootstrap' in config:
395
+ for addr in config['bootstrap']:
396
+ self._send_ping(addr)
397
+
398
+
399
+ def _local_object_get(self, data_hash: bytes) -> Optional[bytes]:
400
+ if self.memory_storage is not None:
401
+ return self.memory_storage.get(data_hash)
402
+
403
+ file_path = self.storage_path / data_hash.hex()
404
+ if file_path.exists():
405
+ return file_path.read_bytes()
406
+ return None
407
+
408
+ def _local_object_put(self, hash: bytes, data: bytes) -> bool:
409
+ if self.memory_storage is not None:
410
+ self.memory_storage[hash] = data
411
+ return True
412
+
413
+ file_path = self.storage_path / hash.hex()
414
+ file_path.write_bytes(data)
415
+ return True
416
+
417
+ def _object_get(self, hash: bytes) -> Optional[bytes]:
418
+ local_data = self._local_object_get(hash)
419
+ if local_data:
420
+ return local_data
421
+
422
+ # find the nearest peer route node to the hash and send an object request
423
+ closest_peer = self._get_closest_local_peer(hash)
424
+ if closest_peer:
425
+ object_request_message = Message(topic=MessageTopic.OBJECT_REQUEST, body=hash)
426
+ object_request_envelope = Envelope(message=object_request_message, sender=self.relay_public_key)
427
+ self.outgoing_queue.put((object_request_envelope.to_bytes(), self.peers[closest_peer].address))
428
+
429
+ # wait for upto self.storage_get_relay_timeout seconds for the object to be stored/until local_object_get returns something
430
+ start_time = time.time()
431
+ while time.time() - start_time < self.storage_get_relay_timeout:
432
+ # Check if the object has been stored locally
433
+ local_data = self._local_object_get(hash)
434
+ if local_data:
435
+ return local_data
436
+ # Sleep briefly to avoid hammering the local storage
437
+ time.sleep(0.1)
438
+
439
+ # If we reach here, the object was not received within the timeout period
440
+ return None
441
+
442
+ # RELAY METHODS
443
+ def _relay_incoming_queue_populating(self):
444
+ while True:
445
+ try:
446
+ data, addr = self.incoming_socket.recvfrom(4096)
447
+ self.incoming_queue.put((data, addr))
448
+ except Exception as e:
449
+ print(f"Error in _relay_populate_incoming_queue: {e}")
450
+
451
+ def _relay_incoming_queue_processing(self):
452
+ while True:
453
+ try:
454
+ data, addr = self.incoming_queue.get()
455
+ envelope = Envelope.from_bytes(data)
456
+ match envelope.message.topic:
457
+ case MessageTopic.PING:
458
+ if envelope.sender in self.peers:
459
+ self.peers[envelope.sender].timestamp = datetime.now(timezone.utc)
460
+ continue
461
+
462
+ is_validator_flag = format.decode(envelope.message.body)
463
+
464
+ if envelope.sender not in self.peers:
465
+ self._send_ping(addr)
466
+
467
+ peer = Peer(self.relay_secret_key, envelope.sender, addr)
468
+ self.peers[peer.sender] = peer
469
+ self.peer_route.add_peer(envelope.sender)
470
+ if is_validator_flag == [1]:
471
+ self.validation_route.add_peer(envelope.sender)
472
+
473
+ if peer.timestamp < datetime.now(timezone.utc) - timedelta(minutes=5.0):
474
+ self._send_ping(addr)
475
+
476
+ case MessageTopic.OBJECT_REQUEST:
477
+ try:
478
+ object_request = ObjectRequest.from_bytes(envelope.message.body)
479
+
480
+ match object_request.type:
481
+ # -------------- OBJECT_GET --------------
482
+ case ObjectRequestType.OBJECT_GET:
483
+ object_hash = object_request.hash
484
+
485
+ # 1. If we already have the object, return it.
486
+ local_data = self._local_object_get(object_hash)
487
+ if local_data is not None:
488
+ resp = ObjectResponse(
489
+ type=ObjectResponseType.OBJECT_FOUND,
490
+ data=local_data,
491
+ hash=object_hash
492
+ )
493
+ msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
494
+ env = Envelope(message=msg, sender=self.relay_public_key)
495
+ self.outgoing_queue.put((env.to_bytes(), addr))
496
+ return # done
497
+
498
+ # 2. If we know a provider, tell the requester.
499
+ if not hasattr(self, "storage_index") or not isinstance(self.storage_index, dict):
500
+ self.storage_index = {}
501
+ if object_hash in self.storage_index:
502
+ provider_bytes = self.storage_index[object_hash]
503
+ resp = ObjectResponse(
504
+ type=ObjectResponseType.OBJECT_PROVIDER,
505
+ data=provider_bytes,
506
+ hash=object_hash
507
+ )
508
+ msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
509
+ env = Envelope(message=msg, sender=self.relay_public_key)
510
+ self.outgoing_queue.put((env.to_bytes(), addr))
511
+ return # done
512
+
513
+ # 3. Otherwise, direct the requester to a peer nearer to the hash.
514
+ nearest = self._get_closest_local_peer(object_hash)
515
+ if nearest:
516
+ nearest_key, nearest_peer = nearest
517
+ peer_info = format.encode([
518
+ nearest_key.public_bytes(
519
+ encoding=serialization.Encoding.Raw,
520
+ format=serialization.PublicFormat.Raw
521
+ ),
522
+ encode_ip_address(*nearest_peer.address)
523
+ ])
524
+ resp = ObjectResponse(
525
+ type=ObjectResponseType.OBJECT_NEAREST_PEER,
526
+ data=peer_info,
527
+ hash=object_hash
528
+ )
529
+ msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
530
+ env = Envelope(message=msg, sender=self.relay_public_key)
531
+ self.outgoing_queue.put((env.to_bytes(), addr))
532
+
533
+ # -------------- OBJECT_PUT --------------
534
+ case ObjectRequestType.OBJECT_PUT:
535
+ # Ensure the hash is present / correct.
536
+ obj_hash = object_request.hash or blake3.blake3(object_request.data).digest()
537
+
538
+ nearest = self._get_closest_local_peer(obj_hash)
539
+ # If a strictly nearer peer exists, forward the PUT.
540
+ if nearest and self._is_closer_than_local_peers(obj_hash, nearest[0]):
541
+ fwd_req = ObjectRequest(
542
+ type=ObjectRequestType.OBJECT_PUT,
543
+ data=object_request.data,
544
+ hash=obj_hash
545
+ )
546
+ fwd_msg = Message(topic=MessageTopic.OBJECT_REQUEST, body=fwd_req.to_bytes())
547
+ fwd_env = Envelope(message=fwd_msg, sender=self.relay_public_key)
548
+ self.outgoing_queue.put((fwd_env.to_bytes(), nearest[1].address))
549
+ else:
550
+ # We are closest → remember who can provide the object.
551
+ provider_record = format.encode([
552
+ envelope.sender.public_bytes(),
553
+ encode_ip_address(*addr)
554
+ ])
555
+ if not hasattr(self, "storage_index") or not isinstance(self.storage_index, dict):
556
+ self.storage_index = {}
557
+ self.storage_index[obj_hash] = provider_record
558
+
559
+ except Exception as e:
560
+ print(f"Error processing OBJECT_REQUEST: {e}")
561
+
562
+ case MessageTopic.OBJECT_RESPONSE:
563
+ try:
564
+ object_response = ObjectResponse.from_bytes(envelope.message.body)
565
+ if object_response.hash not in self.object_request_queue:
566
+ continue
567
+
568
+ match object_response.type:
569
+ case ObjectResponseType.OBJECT_FOUND:
570
+ if object_response.hash != blake3.blake3(object_response.data).digest():
571
+ continue
572
+ self.object_request_queue.remove(object_response.hash)
573
+ self._local_object_put(object_response.hash, object_response.data)
574
+
575
+ case ObjectResponseType.OBJECT_PROVIDER:
576
+ _provider_public_key, provider_address = format.decode(object_response.data)
577
+ provider_ip, provider_port = decode_ip_address(provider_address)
578
+ object_request_message = Message(topic=MessageTopic.OBJECT_REQUEST, body=object_hash)
579
+ object_request_envelope = Envelope(message=object_request_message, sender=self.relay_public_key)
580
+ self.outgoing_queue.put((object_request_envelope.to_bytes(), (provider_ip, provider_port)))
581
+
582
+ case ObjectResponseType.OBJECT_NEAREST_PEER:
583
+ # -- decode the peer info sent back
584
+ nearest_peer_public_key_bytes, nearest_peer_address = (
585
+ format.decode(object_response.data)
586
+ )
587
+ nearest_peer_public_key = X25519PublicKey.from_public_bytes(
588
+ nearest_peer_public_key_bytes
589
+ )
590
+
591
+ # -- XOR-distance between the object hash and the candidate peer
592
+ peer_bytes = nearest_peer_public_key.public_bytes(
593
+ encoding=serialization.Encoding.Raw,
594
+ format=serialization.PublicFormat.Raw,
595
+ )
596
+ object_response_xor = sum(
597
+ a ^ b for a, b in zip(object_response.hash, peer_bytes)
598
+ )
599
+
600
+ # -- forward only if that peer is strictly nearer than any local peer
601
+ if self._is_closer_than_local_peers(
602
+ object_response.hash, nearest_peer_public_key
603
+ ):
604
+ nearest_peer_ip, nearest_peer_port = decode_ip_address(
605
+ nearest_peer_address
606
+ )
607
+ object_request_message = Message(
608
+ topic=MessageTopic.OBJECT_REQUEST,
609
+ body=object_response.hash,
610
+ )
611
+ object_request_envelope = Envelope(
612
+ message=object_request_message,
613
+ sender=self.relay_public_key,
614
+ )
615
+ self.outgoing_queue.put(
616
+ (
617
+ object_request_envelope.to_bytes(),
618
+ (nearest_peer_ip, nearest_peer_port),
619
+ )
620
+ )
621
+
622
+
623
+ except Exception as e:
624
+ print(f"Error processing OBJECT_RESPONSE: {e}")
625
+
626
+ except Exception as e:
627
+ print(f"Error processing message: {e}")
628
+
629
+ def _relay_outgoing_queue_processor(self):
630
+ while True:
631
+ try:
632
+ data, addr = self.outgoing_queue.get()
633
+ self.outgoing_socket.sendto(data, addr)
634
+ except Exception as e:
635
+ print(f"Error sending message: {e}")
636
+
637
+ def _relay_peer_manager(self):
638
+ while True:
639
+ try:
640
+ time.sleep(60)
641
+ for peer in self.peers.values():
642
+ if (datetime.now(timezone.utc) - peer.timestamp).total_seconds() > 900:
643
+ del self.peers[peer.sender]
644
+ self.peer_route.remove_peer(peer.sender)
645
+ if peer.sender in self.validation_route.buckets:
646
+ self.validation_route.remove_peer(peer.sender)
647
+ except Exception as e:
648
+ print(f"Error in _peer_manager_thread: {e}")
649
+
650
+ def _send_ping(self, addr: Tuple[str, int]):
651
+ is_validator_flag = format.encode([1] if self.validation_secret_key else [0])
652
+ ping_message = Message(topic=MessageTopic.PING, body=is_validator_flag)
653
+ ping_envelope = Envelope(message=ping_message, sender=self.relay_public_key)
654
+ self.outgoing_queue.put((ping_envelope.to_bytes(), addr))
655
+
656
+ def _get_closest_local_peer(self, hash: bytes) -> Optional[(X25519PublicKey, Peer)]:
657
+ # Find the globally closest peer using XOR distance
658
+ closest_peer = None
659
+ closest_distance = None
660
+
661
+ # Check all peers
662
+ for peer_key, peer in self.peers.items():
663
+ # Calculate XOR distance between hash and peer's public key
664
+ peer_bytes = peer_key.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)
665
+ # XOR each byte and sum them to get a total distance
666
+ distance = sum(a ^ b for a, b in zip(hash, peer_bytes))
667
+ # Update the closest peer if the distance is smaller
668
+ if closest_distance is None or distance < closest_distance:
669
+ closest_distance = distance
670
+ closest_peer = (peer_key, peer)
671
+
672
+ return closest_peer
673
+
674
+ def _is_closer_than_local_peers(self, hash: bytes, foreign_peer_public_key: X25519PublicKey) -> bool:
675
+
676
+ # Get the closest local peer
677
+ closest_local_peer = self._get_closest_local_peer(hash)
678
+
679
+ # If we have no local peers, the foreign peer is closer by default
680
+ if closest_local_peer is None:
681
+ return True
682
+
683
+ # Calculate XOR distance for the foreign peer
684
+ foreign_peer_bytes = foreign_peer_public_key.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)
685
+ foreign_distance = sum(a ^ b for a, b in zip(hash, foreign_peer_bytes))
686
+
687
+ # Get the closest local peer key and calculate its distance
688
+ closest_peer_key, _ = closest_local_peer
689
+ closest_peer_bytes = closest_peer_key.public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)
690
+ local_distance = sum(a ^ b for a, b in zip(hash, closest_peer_bytes))
691
+
692
+ # Return True if the foreign peer is closer (has smaller XOR distance)
693
+ return foreign_distance < local_distance
694
+
695
+ # MACHINE
696
+ def _machine_setup(self):
697
+ self.sessions: Dict[uuid.UUID, Env] = {}
698
+ self.lock = threading.Lock()
699
+
700
+ def machine_session_create(self) -> uuid.UUID:
701
+ session_id = uuid.uuid4()
702
+ with self.lock:
703
+ self.sessions[session_id] = Env()
704
+ return session_id
705
+
706
+ def machine_session_delete(self, session_id: str) -> bool:
707
+ with self.lock:
708
+ if session_id in self.sessions:
709
+ del self.sessions[session_id]
710
+ return True
711
+ else:
712
+ return False
713
+
714
+ def machine_expr_get(self, session_id: uuid.UUID, name: str) -> Optional[Expr]:
715
+ with self.lock:
716
+ env = self.sessions.get(session_id)
717
+ if env is None:
718
+ return None
719
+ return env.get(name)
720
+
721
+ def machine_expr_put(self, session_id: uuid.UUID, name: str, expr: Expr):
722
+ with self.lock:
723
+ env = self.sessions.get(session_id)
724
+ if env is None:
725
+ return False
726
+ env.put(name, expr)
727
+ return True
728
+
729
+ def machine_expr_eval(self, env: Env, expr: Expr) -> Expr:
730
+ if isinstance(expr, Expr.Boolean) or isinstance(expr, Expr.Integer) or isinstance(expr, Expr.String) or isinstance(expr, Expr.Error):
731
+ return expr
732
+
733
+ elif isinstance(expr, Expr.Symbol):
734
+ value = env.get(expr.value)
735
+ if value:
736
+ return value
737
+ else:
738
+ return Expr.Error(message=f"unbound symbol '{expr.value}'", origin=expr)
739
+
740
+ elif isinstance(expr, Expr.ListExpr):
741
+ if len(expr.elements) == 0:
742
+ return expr
743
+ if len(expr.elements) == 1:
744
+ return self.machine_expr_eval(expr=expr.elements[0], env=env)
745
+ first = expr.elements[0]
746
+ if isinstance(first, Expr.Symbol):
747
+ first_symbol_value = env.get(first.value)
748
+
749
+ if first_symbol_value and not isinstance(first_symbol_value, Expr.Function):
750
+ evaluated_elements = [self.evaluate_expression(e, env) for e in expr.elements]
751
+ return Expr.ListExpr(evaluated_elements)
752
+
753
+ elif first.value == "def":
754
+ if len(args) != 2:
755
+ return Expr.Error(message=f"'def' expects exactly 2 arguments, got {len(args)}", origin=expr)
756
+ if not isinstance(args[0], Expr.Symbol):
757
+ return Expr.Error(message="first argument to 'def' must be a symbol", origin=args[0])
758
+ result = self.machine_expr_eval(env=env, expr=args[1])
759
+ if isinstance(result, Expr.Error):
760
+ return result
761
+ env.put(name=args[0].value, value=result)
762
+ return result
763
+
764
+ # # List
765
+ # elif first.value == "list.new":
766
+ # return Expr.ListExpr([self.evaluate_expression(arg, env) for arg in expr.elements[1:]])
767
+
768
+ # elif first.value == "list.get":
769
+ # args = expr.elements[1:]
770
+ # if len(args) != 2:
771
+ # return Expr.Error(
772
+ # category="SyntaxError",
773
+ # message="list.get expects exactly two arguments: a list and an index"
774
+ # )
775
+ # list_obj = self.evaluate_expression(args[0], env)
776
+ # index = self.evaluate_expression(args[1], env)
777
+ # return handle_list_get(self, list_obj, index, env)
778
+
779
+ # elif first.value == "list.insert":
780
+ # args = expr.elements[1:]
781
+ # if len(args) != 3:
782
+ # return Expr.ListExpr([
783
+ # Expr.ListExpr([]),
784
+ # Expr.String("list.insert expects exactly three arguments: a list, an index, and a value")
785
+ # ])
786
+
787
+ # return handle_list_insert(
788
+ # list=self.evaluate_expression(args[0], env),
789
+ # index=self.evaluate_expression(args[1], env),
790
+ # value=self.evaluate_expression(args[2], env),
791
+ # )
792
+
793
+ # elif first.value == "list.remove":
794
+ # args = expr.elements[1:]
795
+ # if len(args) != 2:
796
+ # return Expr.ListExpr([
797
+ # Expr.ListExpr([]),
798
+ # Expr.String("list.remove expects exactly two arguments: a list and an index")
799
+ # ])
800
+
801
+ # return handle_list_remove(
802
+ # list=self.evaluate_expression(args[0], env),
803
+ # index=self.evaluate_expression(args[1], env),
804
+ # )
805
+
806
+ # elif first.value == "list.length":
807
+ # args = expr.elements[1:]
808
+ # if len(args) != 1:
809
+ # return Expr.ListExpr([
810
+ # Expr.ListExpr([]),
811
+ # Expr.String("list.length expects exactly one argument: a list")
812
+ # ])
813
+
814
+ # list_obj = self.evaluate_expression(args[0], env)
815
+ # if not isinstance(list_obj, Expr.ListExpr):
816
+ # return Expr.ListExpr([
817
+ # Expr.ListExpr([]),
818
+ # Expr.String("Argument must be a list")
819
+ # ])
820
+
821
+ # return Expr.ListExpr([
822
+ # Expr.Integer(len(list_obj.elements)),
823
+ # Expr.ListExpr([])
824
+ # ])
825
+
826
+ # elif first.value == "list.fold":
827
+ # if len(args) != 3:
828
+ # return Expr.ListExpr([
829
+ # Expr.ListExpr([]),
830
+ # Expr.String("list.fold expects exactly three arguments: a list, an initial value, and a function")
831
+ # ])
832
+
833
+ # return handle_list_fold(
834
+ # machine=self,
835
+ # list=self.evaluate_expression(args[0], env),
836
+ # initial=self.evaluate_expression(args[1], env),
837
+ # func=self.evaluate_expression(args[2], env),
838
+ # env=env,
839
+ # )
840
+
841
+ # elif first.value == "list.map":
842
+ # if len(args) != 2:
843
+ # return Expr.ListExpr([
844
+ # Expr.ListExpr([]),
845
+ # Expr.String("list.map expects exactly two arguments: a list and a function")
846
+ # ])
847
+
848
+ # return handle_list_map(
849
+ # machine=self,
850
+ # list=self.evaluate_expression(args[0], env),
851
+ # func=self.evaluate_expression(args[1], env),
852
+ # env=env,
853
+ # )
854
+
855
+ # elif first.value == "list.position":
856
+ # if len(args) != 2:
857
+ # return Expr.ListExpr([
858
+ # Expr.ListExpr([]),
859
+ # Expr.String("list.position expects exactly two arguments: a list and a function")
860
+ # ])
861
+
862
+ # return handle_list_position(
863
+ # machine=self,
864
+ # list=self.evaluate_expression(args[0], env),
865
+ # predicate=self.evaluate_expression(args[1], env),
866
+ # env=env,
867
+ # )
868
+
869
+ # elif first.value == "list.any":
870
+ # if len(args) != 2:
871
+ # return Expr.ListExpr([
872
+ # Expr.ListExpr([]),
873
+ # Expr.String("list.any expects exactly two arguments: a list and a function")
874
+ # ])
875
+
876
+ # return handle_list_any(
877
+ # machine=self,
878
+ # list=self.evaluate_expression(args[0], env),
879
+ # predicate=self.evaluate_expression(args[1], env),
880
+ # env=env,
881
+ # )
882
+
883
+ # elif first.value == "list.all":
884
+ # if len(args) != 2:
885
+ # return Expr.ListExpr([
886
+ # Expr.ListExpr([]),
887
+ # Expr.String("list.all expects exactly two arguments: a list and a function")
888
+ # ])
889
+
890
+ # return handle_list_all(
891
+ # machine=self,
892
+ # list=self.evaluate_expression(args[0], env),
893
+ # predicate=self.evaluate_expression(args[1], env),
894
+ # env=env,
895
+ # )
896
+
897
+ # Integer
898
+ elif first.value == "+":
899
+ args = expr.elements[1:]
900
+ if len(args) == 0:
901
+ return Expr.Error(message="'+' expects at least 1 argument", origin=expr)
902
+ evaluated_args = []
903
+ for arg in args:
904
+ val = self.evaluate_expression(arg, env)
905
+ if isinstance(val, Expr.Error):
906
+ return val
907
+ evaluated_args.append(val)
908
+ if not all(isinstance(val, Expr.Integer) for val in evaluated_args):
909
+ offending = next(val for val in evaluated_args if not isinstance(val, Expr.Integer))
910
+ return Expr.Error(message="'+' only accepts integer operands", origin=offending)
911
+ result = sum(val.value for val in evaluated_args)
912
+ return Expr.Integer(result)
913
+
914
+ # # Subtraction
915
+ # elif first.value == "-":
916
+ # evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
917
+
918
+ # # Check for non-integer arguments
919
+ # if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
920
+ # return Expr.Error(
921
+ # category="TypeError",
922
+ # message="All arguments to - must be integers"
923
+ # )
924
+
925
+ # # With only one argument, negate it
926
+ # if len(evaluated_args) == 1:
927
+ # return Expr.Integer(-evaluated_args[0].value)
928
+
929
+ # # With multiple arguments, subtract all from the first
930
+ # result = evaluated_args[0].value
931
+ # for arg in evaluated_args[1:]:
932
+ # result -= arg.value
933
+
934
+ # return Expr.Integer(result)
935
+
936
+ # # Multiplication
937
+ # elif first.value == "*":
938
+ # evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
939
+
940
+ # # Check for non-integer arguments
941
+ # if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
942
+ # return Expr.Error(
943
+ # category="TypeError",
944
+ # message="All arguments to * must be integers"
945
+ # )
946
+
947
+ # # Multiply all values
948
+ # result = 1
949
+ # for arg in evaluated_args:
950
+ # result *= arg.value
951
+
952
+ # return Expr.Integer(result)
953
+
954
+ # # Division (integer division)
955
+ # elif first.value == "/":
956
+ # evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
957
+
958
+ # # Check for non-integer arguments
959
+ # if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
960
+ # return Expr.Error(
961
+ # category="TypeError",
962
+ # message="All arguments to / must be integers"
963
+ # )
964
+
965
+ # # Need exactly two arguments
966
+ # if len(evaluated_args) != 2:
967
+ # return Expr.Error(
968
+ # category="ArgumentError",
969
+ # message="The / operation requires exactly two arguments"
970
+ # )
971
+
972
+ # dividend = evaluated_args[0].value
973
+ # divisor = evaluated_args[1].value
974
+
975
+ # if divisor == 0:
976
+ # return Expr.Error(
977
+ # category="DivisionError",
978
+ # message="Division by zero"
979
+ # )
980
+
981
+ # return Expr.Integer(dividend // divisor) # Integer division
982
+
983
+ # # Remainder (modulo)
984
+ # elif first.value == "%":
985
+ # evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
986
+
987
+ # # Check for non-integer arguments
988
+ # if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
989
+ # return Expr.Error(
990
+ # category="TypeError",
991
+ # message="All arguments to % must be integers"
992
+ # )
993
+
994
+ # # Need exactly two arguments
995
+ # if len(evaluated_args) != 2:
996
+ # return Expr.Error(
997
+ # category="ArgumentError",
998
+ # message="The % operation requires exactly two arguments"
999
+ # )
1000
+
1001
+ # dividend = evaluated_args[0].value
1002
+ # divisor = evaluated_args[1].value
1003
+
1004
+ # if divisor == 0:
1005
+ # return Expr.Error(
1006
+ # category="DivisionError",
1007
+ # message="Modulo by zero"
1008
+ # )
1009
+
1010
+ # return Expr.Integer(dividend % divisor)
1011
+
1012
+ else:
1013
+ evaluated_elements = [self.evaluate_expression(e, env) for e in expr.elements]
1014
+ return Expr.ListExpr(evaluated_elements)
1015
+
1016
+ elif isinstance(expr, Expr.Function):
1017
+ return expr
1018
+
1019
+ else:
1020
+ raise ValueError(f"Unknown expression type: {type(expr)}")
1021
+