astreum 0.2.29__py3-none-any.whl → 0.2.61__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.
Files changed (58) hide show
  1. astreum/__init__.py +9 -1
  2. astreum/_communication/__init__.py +11 -0
  3. astreum/{models → _communication}/message.py +101 -64
  4. astreum/_communication/peer.py +23 -0
  5. astreum/_communication/ping.py +33 -0
  6. astreum/_communication/route.py +95 -0
  7. astreum/_communication/setup.py +322 -0
  8. astreum/_communication/util.py +42 -0
  9. astreum/_consensus/__init__.py +20 -0
  10. astreum/_consensus/account.py +95 -0
  11. astreum/_consensus/accounts.py +38 -0
  12. astreum/_consensus/block.py +311 -0
  13. astreum/_consensus/chain.py +66 -0
  14. astreum/_consensus/fork.py +100 -0
  15. astreum/_consensus/genesis.py +72 -0
  16. astreum/_consensus/receipt.py +136 -0
  17. astreum/_consensus/setup.py +115 -0
  18. astreum/_consensus/transaction.py +215 -0
  19. astreum/_consensus/workers/__init__.py +9 -0
  20. astreum/_consensus/workers/discovery.py +48 -0
  21. astreum/_consensus/workers/validation.py +125 -0
  22. astreum/_consensus/workers/verify.py +63 -0
  23. astreum/_lispeum/__init__.py +16 -0
  24. astreum/_lispeum/environment.py +13 -0
  25. astreum/_lispeum/expression.py +190 -0
  26. astreum/_lispeum/high_evaluation.py +236 -0
  27. astreum/_lispeum/low_evaluation.py +123 -0
  28. astreum/_lispeum/meter.py +18 -0
  29. astreum/_lispeum/parser.py +51 -0
  30. astreum/_lispeum/tokenizer.py +22 -0
  31. astreum/_node.py +198 -0
  32. astreum/_storage/__init__.py +7 -0
  33. astreum/_storage/atom.py +109 -0
  34. astreum/_storage/patricia.py +478 -0
  35. astreum/_storage/setup.py +35 -0
  36. astreum/models/block.py +48 -39
  37. astreum/node.py +755 -563
  38. astreum/utils/bytes.py +24 -0
  39. astreum/utils/integer.py +25 -0
  40. astreum/utils/logging.py +219 -0
  41. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/METADATA +50 -14
  42. astreum-0.2.61.dist-info/RECORD +57 -0
  43. astreum/lispeum/__init__.py +0 -2
  44. astreum/lispeum/environment.py +0 -40
  45. astreum/lispeum/expression.py +0 -86
  46. astreum/lispeum/parser.py +0 -41
  47. astreum/lispeum/tokenizer.py +0 -52
  48. astreum/models/account.py +0 -91
  49. astreum/models/accounts.py +0 -34
  50. astreum/models/transaction.py +0 -106
  51. astreum/relay/__init__.py +0 -0
  52. astreum/relay/peer.py +0 -9
  53. astreum/relay/route.py +0 -25
  54. astreum/relay/setup.py +0 -58
  55. astreum-0.2.29.dist-info/RECORD +0 -33
  56. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/WHEEL +0 -0
  57. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/licenses/LICENSE +0 -0
  58. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/top_level.txt +0 -0
astreum/__init__.py CHANGED
@@ -1 +1,9 @@
1
- from .node import Node, Expr
1
+ """Lightweight package initializer to avoid circular imports during tests.
2
+
3
+ Exports are intentionally minimal; import submodules directly as needed:
4
+ - Node, Expr, Env, tokenize, parse -> from astreum._node or astreum.lispeum
5
+ - Validation types -> from astreum._validation
6
+ - Storage types -> from astreum._storage
7
+ """
8
+
9
+ __all__: list[str] = []
@@ -0,0 +1,11 @@
1
+ from .message import Message
2
+ from .peer import Peer
3
+ from .route import Route
4
+ from .setup import communication_setup
5
+
6
+ __all__ = [
7
+ "Message",
8
+ "Peer",
9
+ "Route",
10
+ "communication_setup",
11
+ ]
@@ -1,64 +1,101 @@
1
- from enum import IntEnum
2
- from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
3
-
4
- class MessageTopic(IntEnum):
5
- PING = 0
6
- OBJECT_REQUEST = 1
7
- OBJECT_RESPONSE = 2
8
- ROUTE_REQUEST = 3
9
- ROUTE_RESPONSE = 4
10
-
11
- class Message:
12
- handshake: bool
13
- sender: X25519PublicKey
14
-
15
- topic: MessageTopic
16
- content: bytes
17
-
18
- def to_bytes(self):
19
- if self.handshake:
20
- # handshake byte (1) + raw public key bytes
21
- return bytes([1]) + self.sender.public_bytes(
22
- encoding=serialization.Encoding.Raw,
23
- format=serialization.PublicFormat.Raw
24
- )
25
- else:
26
- # normal message: 0 + topic + content
27
- return bytes([0, self.topic.value]) + self.content
28
-
29
- @classmethod
30
- def from_bytes(cls, data: bytes) -> "Message":
31
- if len(data) < 1:
32
- raise ValueError("Cannot parse Message: no data")
33
- flag = data[0]
34
- # create empty instance
35
- msg = cls.__new__(cls)
36
-
37
- if flag == 1:
38
- # handshake message: the rest is the peer’s public key
39
- key_bytes = data[1:]
40
- try:
41
- sender = X25519PublicKey.from_public_bytes(key_bytes)
42
- except ValueError:
43
- raise ValueError("Invalid public key bytes")
44
- msg.handshake = True
45
- msg.sender = sender
46
- msg.topic = None
47
- msg.content = b''
48
- elif flag == 0:
49
- # normal message: next byte is topic, rest is content
50
- if len(data) < 2:
51
- raise ValueError("Cannot parse Message: missing topic byte")
52
- topic_val = data[1]
53
- try:
54
- topic = MessageTopic(topic_val)
55
- except ValueError:
56
- raise ValueError(f"Unknown MessageTopic: {topic_val}")
57
- msg.handshake = False
58
- msg.sender = None
59
- msg.topic = topic
60
- msg.content = data[2:]
61
- else:
62
- raise ValueError(f"Invalid handshake flag: {flag}")
63
-
64
- return msg
1
+ from enum import IntEnum
2
+ from typing import Optional
3
+ from cryptography.hazmat.primitives import serialization
4
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
5
+
6
+ class MessageTopic(IntEnum):
7
+ PING = 0
8
+ OBJECT_REQUEST = 1
9
+ OBJECT_RESPONSE = 2
10
+ ROUTE_REQUEST = 3
11
+ ROUTE_RESPONSE = 4
12
+ TRANSACTION = 5
13
+ STORAGE_REQUEST = 6
14
+
15
+
16
+ class Message:
17
+ handshake: bool
18
+ sender: Optional[X25519PublicKey]
19
+
20
+ topic: Optional[MessageTopic]
21
+ content: bytes
22
+
23
+ def __init__(
24
+ self,
25
+ *,
26
+ handshake: bool = False,
27
+ sender: Optional[X25519PublicKey] = None,
28
+ topic: Optional[MessageTopic] = None,
29
+ content: bytes = b"",
30
+ body: Optional[bytes] = None,
31
+ ) -> None:
32
+ if body is not None:
33
+ if content and content != b"":
34
+ raise ValueError("specify only one of 'content' or 'body'")
35
+ content = body
36
+
37
+ self.handshake = handshake
38
+ self.sender = sender
39
+ self.topic = topic
40
+ self.content = content or b""
41
+
42
+ if self.handshake:
43
+ if self.sender is None:
44
+ raise ValueError("handshake Message requires a sender public key")
45
+ self.topic = None
46
+ self.content = b""
47
+ else:
48
+ if self.topic is None:
49
+ raise ValueError("non-handshake Message requires a topic")
50
+
51
+ def to_bytes(self):
52
+ if self.handshake:
53
+ # handshake byte (1) + raw public key bytes
54
+ return bytes([1]) + self.sender.public_bytes(
55
+ encoding=serialization.Encoding.Raw,
56
+ format=serialization.PublicFormat.Raw
57
+ )
58
+ else:
59
+ # normal message: 0 + topic + content
60
+ return bytes([0, self.topic.value]) + self.content
61
+
62
+ @classmethod
63
+ def from_bytes(cls, data: bytes) -> "Message":
64
+ if len(data) < 1:
65
+ raise ValueError("Cannot parse Message: no data")
66
+ flag = data[0]
67
+ # create empty instance
68
+ msg = cls.__new__(cls)
69
+
70
+ if flag == 1:
71
+ # handshake message: the rest is the peer’s public key
72
+ key_bytes = data[1:]
73
+ if not key_bytes:
74
+ raise ValueError("Handshake message missing sender public key bytes")
75
+ try:
76
+ sender = X25519PublicKey.from_public_bytes(key_bytes)
77
+ except ValueError:
78
+ raise ValueError("Invalid public key bytes")
79
+ if sender is None:
80
+ raise ValueError("Handshake message missing sender public key")
81
+ msg.handshake = True
82
+ msg.sender = sender
83
+ msg.topic = None
84
+ msg.content = b''
85
+ elif flag == 0:
86
+ # normal message: next byte is topic, rest is content
87
+ if len(data) < 2:
88
+ raise ValueError("Cannot parse Message: missing topic byte")
89
+ topic_val = data[1]
90
+ try:
91
+ topic = MessageTopic(topic_val)
92
+ except ValueError:
93
+ raise ValueError(f"Unknown MessageTopic: {topic_val}")
94
+ msg.handshake = False
95
+ msg.sender = None
96
+ msg.topic = topic
97
+ msg.content = data[2:]
98
+ else:
99
+ raise ValueError(f"Invalid handshake flag: {flag}")
100
+
101
+ return msg
@@ -0,0 +1,23 @@
1
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
2
+ from cryptography.hazmat.primitives import serialization
3
+ from datetime import datetime, timezone
4
+ from typing import Optional, Tuple
5
+
6
+ class Peer:
7
+ shared_key: bytes
8
+ timestamp: datetime
9
+ latest_block: bytes
10
+ address: Optional[Tuple[str, int]]
11
+ public_key: X25519PublicKey
12
+ public_key_bytes: bytes
13
+
14
+ def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
15
+ self.shared_key = my_sec_key.exchange(peer_pub_key)
16
+ self.timestamp = datetime.now(timezone.utc)
17
+ self.latest_block = b""
18
+ self.address = None
19
+ self.public_key = peer_pub_key
20
+ self.public_key_bytes = peer_pub_key.public_bytes(
21
+ encoding=serialization.Encoding.Raw,
22
+ format=serialization.PublicFormat.Raw,
23
+ )
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ class PingFormatError(ValueError):
7
+ """Raised when ping payload bytes are invalid."""
8
+
9
+
10
+ @dataclass
11
+ class Ping:
12
+ is_validator: bool
13
+ latest_block: bytes
14
+
15
+ PAYLOAD_SIZE = 33
16
+
17
+ def __post_init__(self) -> None:
18
+ lb = bytes(self.latest_block or b"")
19
+ if len(lb) != 32:
20
+ raise ValueError("latest_block must be exactly 32 bytes")
21
+ self.latest_block = lb
22
+
23
+ def to_bytes(self) -> bytes:
24
+ return (b"\x01" if self.is_validator else b"\x00") + self.latest_block
25
+
26
+ @classmethod
27
+ def from_bytes(cls, data: bytes) -> "Ping":
28
+ if len(data) != cls.PAYLOAD_SIZE:
29
+ raise PingFormatError("ping payload must be exactly 33 bytes")
30
+ flag = data[0]
31
+ if flag not in (0, 1):
32
+ raise PingFormatError("ping validator flag must be 0 or 1")
33
+ return cls(is_validator=bool(flag), latest_block=data[1:])
@@ -0,0 +1,95 @@
1
+ from typing import Dict, List, Optional, Union
2
+ from cryptography.hazmat.primitives import serialization
3
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
4
+ from .peer import Peer
5
+
6
+ PeerKey = Union[X25519PublicKey, bytes, bytearray]
7
+
8
+
9
+ class Route:
10
+ def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
11
+ self.relay_public_key_bytes = relay_public_key.public_bytes(
12
+ encoding=serialization.Encoding.Raw,
13
+ format=serialization.PublicFormat.Raw,
14
+ )
15
+ self.bucket_size = bucket_size
16
+ self.buckets: Dict[int, List[bytes]] = {
17
+ i: [] for i in range(len(self.relay_public_key_bytes) * 8)
18
+ }
19
+ self.peers: Dict[bytes, Peer] = {}
20
+
21
+ @staticmethod
22
+ def _matching_leading_bits(a: bytes, b: bytes) -> int:
23
+ for byte_index, (ba, bb) in enumerate(zip(a, b)):
24
+ diff = ba ^ bb
25
+ if diff:
26
+ return byte_index * 8 + (8 - diff.bit_length())
27
+ return len(a) * 8
28
+
29
+ def _normalize_peer_key(self, peer_public_key: PeerKey) -> bytes:
30
+ if isinstance(peer_public_key, X25519PublicKey):
31
+ return peer_public_key.public_bytes(
32
+ encoding=serialization.Encoding.Raw,
33
+ format=serialization.PublicFormat.Raw,
34
+ )
35
+ if isinstance(peer_public_key, (bytes, bytearray)):
36
+ key_bytes = bytes(peer_public_key)
37
+ if len(key_bytes) != len(self.relay_public_key_bytes):
38
+ raise ValueError("peer key must be raw 32-byte public key")
39
+ return key_bytes
40
+ raise TypeError("peer_public_key must be raw bytes or X25519PublicKey")
41
+
42
+ @staticmethod
43
+ def _xor_distance(a: bytes, b: bytes) -> int:
44
+ if len(a) != len(b):
45
+ raise ValueError("xor distance requires equal-length operands")
46
+ return int.from_bytes(bytes(x ^ y for x, y in zip(a, b)), "big", signed=False)
47
+
48
+ def add_peer(self, peer_public_key: PeerKey, peer: Optional[Peer] = None):
49
+ peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
50
+ bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
51
+ if len(self.buckets[bucket_idx]) < self.bucket_size:
52
+ bucket = self.buckets[bucket_idx]
53
+ if peer_public_key_bytes not in bucket:
54
+ bucket.append(peer_public_key_bytes)
55
+ if peer is not None:
56
+ self.peers[peer_public_key_bytes] = peer
57
+
58
+ def remove_peer(self, peer_public_key: PeerKey):
59
+ peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
60
+ bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
61
+ bucket = self.buckets.get(bucket_idx)
62
+ if not bucket:
63
+ return
64
+ try:
65
+ bucket.remove(peer_public_key_bytes)
66
+ except ValueError:
67
+ pass
68
+ self.peers.pop(peer_public_key_bytes, None)
69
+
70
+ def closest_peer_for_hash(self, target_hash: bytes) -> Optional[Peer]:
71
+ """Return the peer with the minimal XOR distance to ``target_hash``."""
72
+ if not isinstance(target_hash, (bytes, bytearray)):
73
+ raise TypeError("target_hash must be bytes-like")
74
+
75
+ target = bytes(target_hash)
76
+ if len(target) != len(self.relay_public_key_bytes):
77
+ raise ValueError("target_hash must match peer key length (32 bytes)")
78
+
79
+ closest_key: Optional[bytes] = None
80
+ closest_distance: Optional[int] = None
81
+
82
+ for bucket in self.buckets.values():
83
+ for peer_key in bucket:
84
+ try:
85
+ distance = self._xor_distance(target, peer_key)
86
+ except ValueError:
87
+ continue
88
+ if closest_distance is None or distance < closest_distance:
89
+ closest_distance = distance
90
+ closest_key = peer_key
91
+
92
+ if closest_key is None:
93
+ return None
94
+ peer = self.peers.get(closest_key)
95
+ return peer
@@ -0,0 +1,322 @@
1
+ import socket, threading
2
+ from datetime import datetime, timezone
3
+ from queue import Queue
4
+ from typing import Tuple, Optional
5
+ from cryptography.hazmat.primitives.asymmetric import ed25519
6
+ from cryptography.hazmat.primitives import serialization
7
+ from cryptography.hazmat.primitives.asymmetric.x25519 import (
8
+ X25519PrivateKey,
9
+ X25519PublicKey,
10
+ )
11
+
12
+ from typing import TYPE_CHECKING
13
+ if TYPE_CHECKING:
14
+ from .. import Node
15
+
16
+ from . import Route, Message
17
+ from .message import MessageTopic
18
+ from .peer import Peer
19
+ from .ping import Ping
20
+ from .util import address_str_to_host_and_port
21
+
22
+ def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
23
+ """DH key for relaying (always X25519)."""
24
+ return
25
+
26
+ def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
27
+ """Signing key for validation (Ed25519), or None if absent."""
28
+ return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
29
+ if hex_key else None
30
+
31
+ def make_routes(
32
+ relay_pk: X25519PublicKey,
33
+ val_sk: Optional[ed25519.Ed25519PrivateKey]
34
+ ) -> Tuple[Route, Optional[Route]]:
35
+ """Peer route (DH pubkey) + optional validation route (ed pubkey)."""
36
+ peer_rt = Route(relay_pk)
37
+ val_rt = Route(val_sk.public_key()) if val_sk else None
38
+ return peer_rt, val_rt
39
+
40
+ def setup_outgoing(
41
+ use_ipv6: bool
42
+ ) -> Tuple[socket.socket, Queue, threading.Thread]:
43
+ fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
44
+ sock = socket.socket(fam, socket.SOCK_DGRAM)
45
+ q = Queue()
46
+ thr = threading.Thread(target=lambda: None, daemon=True)
47
+ thr.start()
48
+ return sock, q, thr
49
+
50
+ def make_maps():
51
+ """Empty lookup maps: peers and addresses."""
52
+ return
53
+
54
+
55
+ def process_incoming_messages(node: "Node") -> None:
56
+ """Process incoming messages (placeholder)."""
57
+ while True:
58
+ try:
59
+ data, addr = node.incoming_queue.get()
60
+ except Exception as exc:
61
+ print(f"Error taking from incoming queue: {exc}")
62
+ continue
63
+
64
+ try:
65
+ message = Message.from_bytes(data)
66
+ except Exception as exc:
67
+ print(f"Error decoding message: {exc}")
68
+ continue
69
+
70
+ if message.handshake:
71
+ sender_key = message.sender
72
+
73
+ try:
74
+ sender_public_key_bytes = sender_key.public_bytes(
75
+ encoding=serialization.Encoding.Raw,
76
+ format=serialization.PublicFormat.Raw,
77
+ )
78
+ except Exception as exc:
79
+ print(f"Error extracting sender key bytes: {exc}")
80
+ continue
81
+
82
+ # Normalize remote address (IPv6 tuples may be 4 elements)
83
+ try:
84
+ host, port = addr[0], int(addr[1])
85
+ except Exception:
86
+ continue
87
+ address_key = (host, port)
88
+
89
+ old_key_bytes = node.addresses.get(address_key)
90
+ node.addresses[address_key] = sender_public_key_bytes
91
+
92
+ if old_key_bytes is None:
93
+ # brand-new address -> brand-new peer
94
+ try:
95
+ peer = Peer(node.relay_secret_key, sender_key)
96
+ except Exception:
97
+ continue
98
+ peer.address = address_key
99
+
100
+ node.peers[sender_public_key_bytes] = peer
101
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
102
+
103
+ response = Message(handshake=True, sender=node.relay_public_key)
104
+ node.outgoing_queue.put((response.to_bytes(), address_key))
105
+ continue
106
+
107
+ elif old_key_bytes == sender_public_key_bytes:
108
+ # existing mapping with same key -> nothing to change
109
+ peer = node.peers.get(sender_public_key_bytes)
110
+ if peer is not None:
111
+ peer.address = address_key
112
+
113
+ else:
114
+ # address reused with a different key -> replace peer
115
+ node.peers.pop(old_key_bytes, None)
116
+ try:
117
+ node.peer_route.remove_peer(old_key_bytes)
118
+ except Exception:
119
+ pass
120
+ try:
121
+ peer = Peer(node.relay_secret_key, sender_key)
122
+ except Exception:
123
+ continue
124
+ peer.address = address_key
125
+
126
+ node.peers[sender_public_key_bytes] = peer
127
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
128
+
129
+ match message.topic:
130
+ case MessageTopic.PING:
131
+ try:
132
+ host, port = addr[0], int(addr[1])
133
+ except Exception:
134
+ continue
135
+ address_key = (host, port)
136
+ sender_public_key_bytes = node.addresses.get(address_key)
137
+ if sender_public_key_bytes is None:
138
+ continue
139
+ peer = node.peers.get(sender_public_key_bytes)
140
+ if peer is None:
141
+ continue
142
+ try:
143
+ ping = Ping.from_bytes(message.content)
144
+ except Exception as exc:
145
+ print(f"Error decoding ping: {exc}")
146
+ continue
147
+
148
+ peer.timestamp = datetime.now(timezone.utc)
149
+ peer.latest_block = ping.latest_block
150
+
151
+ validation_route = node.validation_route
152
+ if validation_route is None:
153
+ continue
154
+ if ping.is_validator:
155
+ try:
156
+ validation_route.add_peer(sender_public_key_bytes)
157
+ except Exception:
158
+ pass
159
+ else:
160
+ try:
161
+ validation_route.remove_peer(sender_public_key_bytes)
162
+ except Exception:
163
+ pass
164
+ case MessageTopic.OBJECT_REQUEST:
165
+ pass
166
+ case MessageTopic.OBJECT_RESPONSE:
167
+ pass
168
+ case MessageTopic.ROUTE_REQUEST:
169
+ pass
170
+ case MessageTopic.ROUTE_RESPONSE:
171
+ pass
172
+ case MessageTopic.TRANSACTION:
173
+ if node.validation_secret_key is None:
174
+ continue
175
+ node._validation_transaction_queue.put(message.content)
176
+
177
+ case MessageTopic.STORAGE_REQUEST:
178
+ payload = message.content
179
+ if len(payload) < 32:
180
+ continue
181
+
182
+ atom_id = payload[:32]
183
+ provider_bytes = payload[32:]
184
+ if not provider_bytes:
185
+ continue
186
+
187
+ try:
188
+ provider_str = provider_bytes.decode("utf-8")
189
+ except UnicodeDecodeError:
190
+ continue
191
+
192
+ try:
193
+ host, port = addr[0], int(addr[1])
194
+ except Exception:
195
+ continue
196
+ address_key = (host, port)
197
+ sender_key_bytes = node.addresses.get(address_key)
198
+ if sender_key_bytes is None:
199
+ continue
200
+
201
+ try:
202
+ local_key_bytes = node.relay_public_key.public_bytes(
203
+ encoding=serialization.Encoding.Raw,
204
+ format=serialization.PublicFormat.Raw,
205
+ )
206
+ except Exception:
207
+ continue
208
+
209
+ def xor_distance(target: bytes, key: bytes) -> int:
210
+ return int.from_bytes(
211
+ bytes(a ^ b for a, b in zip(target, key)),
212
+ byteorder="big",
213
+ signed=False,
214
+ )
215
+
216
+ self_distance = xor_distance(atom_id, local_key_bytes)
217
+
218
+ try:
219
+ closest_peer = node.peer_route.closest_peer_for_hash(atom_id)
220
+ except Exception:
221
+ closest_peer = None
222
+
223
+ if (
224
+ closest_peer is not None
225
+ and closest_peer.public_key_bytes != sender_key_bytes
226
+ ):
227
+ closest_distance = xor_distance(atom_id, closest_peer.public_key_bytes)
228
+ if closest_distance < self_distance:
229
+ target_addr = closest_peer.address
230
+ if target_addr is not None and target_addr != addr:
231
+ node.outgoing_queue.put((message.to_bytes(), target_addr))
232
+ continue
233
+
234
+ node.storage_index[atom_id] = provider_str.strip()
235
+
236
+ case _:
237
+ continue
238
+
239
+
240
+ def populate_incoming_messages(node: "Node") -> None:
241
+ """Receive UDP packets and feed the incoming queue (placeholder)."""
242
+ while True:
243
+ try:
244
+ data, addr = node.incoming_socket.recvfrom(4096)
245
+ node.incoming_queue.put((data, addr))
246
+ except Exception as exc:
247
+ print(f"Error populating incoming queue: {exc}")
248
+
249
+ def communication_setup(node: "Node", config: dict):
250
+ node.use_ipv6 = config.get('use_ipv6', False)
251
+
252
+ # key loading
253
+ node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
254
+ node.validation_secret_key = load_ed25519(config.get('validation_secret_key'))
255
+
256
+ # derive pubs + routes
257
+ node.relay_public_key = node.relay_secret_key.public_key()
258
+ node.validation_public_key = (
259
+ node.validation_secret_key.public_key().public_bytes(
260
+ encoding=serialization.Encoding.Raw,
261
+ format=serialization.PublicFormat.Raw,
262
+ )
263
+ if node.validation_secret_key
264
+ else None
265
+ )
266
+ node.peer_route, node.validation_route = make_routes(
267
+ node.relay_public_key,
268
+ node.validation_secret_key
269
+ )
270
+
271
+ # sockets + queues + threads
272
+ incoming_port = config.get('incoming_port', 7373)
273
+ fam = socket.AF_INET6 if node.use_ipv6 else socket.AF_INET
274
+ node.incoming_socket = socket.socket(fam, socket.SOCK_DGRAM)
275
+ if node.use_ipv6:
276
+ node.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
277
+ node.incoming_socket.bind(("::" if node.use_ipv6 else "0.0.0.0", incoming_port or 0))
278
+ node.incoming_port = node.incoming_socket.getsockname()[1]
279
+ node.incoming_queue = Queue()
280
+ node.incoming_populate_thread = threading.Thread(
281
+ target=populate_incoming_messages,
282
+ args=(node,),
283
+ daemon=True,
284
+ )
285
+ node.incoming_process_thread = threading.Thread(
286
+ target=process_incoming_messages,
287
+ args=(node,),
288
+ daemon=True,
289
+ )
290
+ node.incoming_populate_thread.start()
291
+ node.incoming_process_thread.start()
292
+
293
+ (node.outgoing_socket,
294
+ node.outgoing_queue,
295
+ node.outgoing_thread
296
+ ) = setup_outgoing(node.use_ipv6)
297
+
298
+ # other workers & maps
299
+ node.object_request_queue = Queue()
300
+ node.peer_manager_thread = threading.Thread(
301
+ target=node._relay_peer_manager,
302
+ daemon=True
303
+ )
304
+ node.peer_manager_thread.start()
305
+
306
+ node.peers, node.addresses = {}, {} # peers: Dict[bytes,Peer], addresses: Dict[(str,int),bytes]
307
+ latest_hash = getattr(node, "latest_block_hash", None)
308
+ if not isinstance(latest_hash, (bytes, bytearray)) or len(latest_hash) != 32:
309
+ node.latest_block_hash = bytes(32)
310
+ else:
311
+ node.latest_block_hash = bytes(latest_hash)
312
+
313
+ # bootstrap pings
314
+ for addr in config.get('bootstrap', []):
315
+ try:
316
+ host, port = address_str_to_host_and_port(addr) # type: ignore[arg-type]
317
+ except Exception:
318
+ continue
319
+
320
+ handshake_message = Message(handshake=True, sender=node.relay_public_key)
321
+
322
+ node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))