astreum 0.3.16__py3-none-any.whl → 0.3.48__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 (65) hide show
  1. astreum/__init__.py +1 -2
  2. astreum/communication/__init__.py +15 -11
  3. astreum/communication/difficulty.py +39 -0
  4. astreum/communication/disconnect.py +57 -0
  5. astreum/communication/handlers/handshake.py +105 -62
  6. astreum/communication/handlers/object_request.py +226 -138
  7. astreum/communication/handlers/object_response.py +118 -10
  8. astreum/communication/handlers/ping.py +9 -0
  9. astreum/communication/handlers/route_request.py +7 -1
  10. astreum/communication/handlers/route_response.py +7 -1
  11. astreum/communication/incoming_queue.py +96 -0
  12. astreum/communication/message_pow.py +36 -0
  13. astreum/communication/models/peer.py +4 -0
  14. astreum/communication/models/ping.py +27 -6
  15. astreum/communication/models/route.py +4 -0
  16. astreum/communication/{start.py → node.py} +10 -11
  17. astreum/communication/outgoing_queue.py +108 -0
  18. astreum/communication/processors/incoming.py +110 -37
  19. astreum/communication/processors/outgoing.py +35 -2
  20. astreum/communication/processors/peer.py +133 -58
  21. astreum/communication/setup.py +272 -113
  22. astreum/communication/util.py +14 -0
  23. astreum/machine/evaluations/low_evaluation.py +5 -5
  24. astreum/machine/models/expression.py +5 -5
  25. astreum/node.py +96 -87
  26. astreum/storage/actions/get.py +285 -183
  27. astreum/storage/actions/set.py +171 -156
  28. astreum/storage/models/atom.py +0 -14
  29. astreum/storage/models/trie.py +2 -2
  30. astreum/storage/providers.py +24 -0
  31. astreum/storage/requests.py +13 -10
  32. astreum/storage/setup.py +20 -15
  33. astreum/utils/config.py +260 -43
  34. astreum/utils/logging.py +1 -1
  35. astreum/{consensus → validation}/__init__.py +0 -4
  36. astreum/validation/constants.py +2 -0
  37. astreum/{consensus → validation}/genesis.py +4 -6
  38. astreum/{consensus → validation}/models/account.py +1 -1
  39. astreum/validation/models/block.py +544 -0
  40. astreum/validation/models/fork.py +511 -0
  41. astreum/{consensus → validation}/models/receipt.py +18 -5
  42. astreum/{consensus → validation}/models/transaction.py +50 -8
  43. astreum/validation/node.py +190 -0
  44. astreum/{consensus → validation}/validator.py +1 -1
  45. astreum/validation/workers/__init__.py +8 -0
  46. astreum/{consensus → validation}/workers/validation.py +360 -333
  47. astreum/verification/__init__.py +4 -0
  48. astreum/{consensus/workers/discovery.py → verification/discover.py} +1 -1
  49. astreum/verification/node.py +61 -0
  50. astreum/verification/worker.py +183 -0
  51. {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/METADATA +45 -9
  52. astreum-0.3.48.dist-info/RECORD +79 -0
  53. astreum/consensus/models/block.py +0 -364
  54. astreum/consensus/models/chain.py +0 -66
  55. astreum/consensus/models/fork.py +0 -100
  56. astreum/consensus/setup.py +0 -83
  57. astreum/consensus/start.py +0 -67
  58. astreum/consensus/workers/__init__.py +0 -9
  59. astreum/consensus/workers/verify.py +0 -90
  60. astreum-0.3.16.dist-info/RECORD +0 -72
  61. /astreum/{consensus → validation}/models/__init__.py +0 -0
  62. /astreum/{consensus → validation}/models/accounts.py +0 -0
  63. {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/WHEEL +0 -0
  64. {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/licenses/LICENSE +0 -0
  65. {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/top_level.txt +0 -0
@@ -1,183 +1,285 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from typing import Optional
5
-
6
- from ..models.atom import Atom
7
-
8
-
9
- def _hot_storage_get(self, key: bytes) -> Optional[Atom]:
10
- """Retrieve an atom from in-memory cache while tracking hit statistics."""
11
- atom = self.hot_storage.get(key)
12
- if atom is not None:
13
- self.hot_storage_hits[key] = self.hot_storage_hits.get(key, 0) + 1
14
- self.logger.debug("Hot storage hit for %s", key.hex())
15
- else:
16
- self.logger.debug("Hot storage miss for %s", key.hex())
17
- return atom
18
-
19
-
20
- def _network_get(self, key: bytes) -> Optional[Atom]:
21
- """Attempt to fetch an atom from network peers when local storage misses."""
22
- if not getattr(self, "is_connected", False):
23
- self.logger.debug("Network fetch skipped for %s; node not connected", key.hex())
24
- return None
25
- self.logger.debug("Attempting network fetch for %s", key.hex())
26
- try:
27
- from ...communication.handlers.object_request import (
28
- ObjectRequest,
29
- ObjectRequestType,
30
- )
31
- from ...communication.models.message import Message, MessageTopic
32
- except Exception as exc:
33
- self.logger.warning(
34
- "Communication module unavailable; cannot fetch %s: %s",
35
- key.hex(),
36
- exc,
37
- )
38
- return None
39
-
40
- try:
41
- closest_peer = self.peer_route.closest_peer_for_hash(key)
42
- except Exception as exc:
43
- self.logger.warning("Peer lookup failed for %s: %s", key.hex(), exc)
44
- return None
45
-
46
- if closest_peer is None or closest_peer.address is None:
47
- self.logger.debug("No peer available to fetch %s", key.hex())
48
- return None
49
-
50
- obj_req = ObjectRequest(
51
- type=ObjectRequestType.OBJECT_GET,
52
- data=b"",
53
- atom_id=key,
54
- )
55
- try:
56
- message = Message(
57
- topic=MessageTopic.OBJECT_REQUEST,
58
- content=obj_req.to_bytes(),
59
- sender=self.relay_public_key,
60
- )
61
- except Exception as exc:
62
- self.logger.warning("Failed to build object request for %s: %s", key.hex(), exc)
63
- return None
64
-
65
- # encrypt the outbound request for the target peer
66
- message.encrypt(closest_peer.shared_key_bytes)
67
-
68
- try:
69
- self.add_atom_req(key)
70
- except Exception as exc:
71
- self.logger.warning("Failed to track object request for %s: %s", key.hex(), exc)
72
-
73
- try:
74
- self.outgoing_queue.put((message.to_bytes(), closest_peer.address))
75
- self.logger.debug(
76
- "Queued OBJECT_GET for %s to peer %s",
77
- key.hex(),
78
- closest_peer.address,
79
- )
80
- except Exception as exc:
81
- self.logger.warning(
82
- "Failed to queue OBJECT_GET for %s to %s: %s",
83
- key.hex(),
84
- closest_peer.address,
85
- exc,
86
- )
87
- return None
88
-
89
-
90
- def storage_get(self, key: bytes) -> Optional[Atom]:
91
- """Retrieve an Atom by checking local storage first, then the network."""
92
- self.logger.debug("Fetching atom %s", key.hex())
93
- atom = self._hot_storage_get(key)
94
- if atom is not None:
95
- self.logger.debug("Returning atom %s from hot storage", key.hex())
96
- return atom
97
- atom = self._cold_storage_get(key)
98
- if atom is not None:
99
- self.logger.debug("Returning atom %s from cold storage", key.hex())
100
- return atom
101
-
102
- if not self.is_connected:
103
- return None
104
-
105
- provider_payload = self.storage_index.get(key)
106
- if provider_payload is not None:
107
- try:
108
- from ...communication.handlers.object_response import decode_object_provider
109
- from ...communication.handlers.object_request import (
110
- ObjectRequest,
111
- ObjectRequestType,
112
- )
113
- from ...communication.models.message import Message, MessageTopic
114
- from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
115
-
116
- provider_key, provider_address, provider_port = decode_object_provider(provider_payload)
117
- provider_public_key = X25519PublicKey.from_public_bytes(provider_key)
118
- shared_key_bytes = self.relay_secret_key.exchange(provider_public_key)
119
-
120
- obj_req = ObjectRequest(
121
- type=ObjectRequestType.OBJECT_GET,
122
- data=b"",
123
- atom_id=key,
124
- )
125
- message = Message(
126
- topic=MessageTopic.OBJECT_REQUEST,
127
- content=obj_req.to_bytes(),
128
- sender=self.relay_public_key,
129
- )
130
- message.encrypt(shared_key_bytes)
131
- self.add_atom_req(key)
132
- self.outgoing_queue.put((message.to_bytes(), (provider_address, provider_port)))
133
- self.logger.debug(
134
- "Requested atom %s from indexed provider %s:%s",
135
- key.hex(),
136
- provider_address,
137
- provider_port,
138
- )
139
- except Exception as exc:
140
- self.logger.warning("Failed indexed fetch for %s: %s", key.hex(), exc)
141
- return None
142
-
143
- self.logger.debug("Falling back to network fetch for %s", key.hex())
144
- return self._network_get(key)
145
-
146
-
147
- def local_get(self, key: bytes) -> Optional[Atom]:
148
- """Retrieve an Atom by checking only local hot and cold storage."""
149
- self.logger.debug("Fetching atom %s (local only)", key.hex())
150
- atom = self._hot_storage_get(key)
151
- if atom is not None:
152
- self.logger.debug("Returning atom %s from hot storage", key.hex())
153
- return atom
154
- atom = self._cold_storage_get(key)
155
- if atom is not None:
156
- self.logger.debug("Returning atom %s from cold storage", key.hex())
157
- return atom
158
- self.logger.debug("Local storage miss for %s", key.hex())
159
- return None
160
-
161
-
162
- def _cold_storage_get(self, key: bytes) -> Optional[Atom]:
163
- """Read an atom from the cold storage directory if configured."""
164
- if not self.config["cold_storage_path"]:
165
- self.logger.debug("Cold storage disabled; cannot fetch %s", key.hex())
166
- return None
167
- filename = f"{key.hex().upper()}.bin"
168
- file_path = Path(self.config["cold_storage_path"]) / filename
169
- try:
170
- data = file_path.read_bytes()
171
- except FileNotFoundError:
172
- self.logger.debug("Cold storage miss for %s", key.hex())
173
- return None
174
- except OSError as exc:
175
- self.logger.warning("Error reading cold storage file %s: %s", file_path, exc)
176
- return None
177
- try:
178
- atom = Atom.from_bytes(data)
179
- self.logger.debug("Loaded atom %s from cold storage", key.hex())
180
- return atom
181
- except ValueError as exc:
182
- self.logger.warning("Cold storage data corrupted for %s: %s", file_path, exc)
183
- return None
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from time import sleep
5
+ from typing import List, Optional, Union
6
+
7
+ from ..models.atom import Atom, ZERO32
8
+ from ..providers import provider_payload_for_id
9
+
10
+
11
+ def _hot_storage_get(self, key: bytes) -> Optional[Atom]:
12
+ """Retrieve an atom from in-memory cache while tracking hit statistics."""
13
+ atom = self.hot_storage.get(key)
14
+ if atom is not None:
15
+ self.hot_storage_hits[key] = self.hot_storage_hits.get(key, 0) + 1
16
+ self.logger.debug("Hot storage hit for %s", key.hex())
17
+ else:
18
+ self.logger.debug("Hot storage miss for %s", key.hex())
19
+ return atom
20
+
21
+
22
+ def _network_get(self, atom_id: bytes, payload_type: int) -> Optional[Union[Atom, List[Atom]]]:
23
+ """Attempt to fetch an atom from network peers when local storage misses."""
24
+ from ...communication.handlers.object_response import (
25
+ OBJECT_FOUND_ATOM_PAYLOAD,
26
+ OBJECT_FOUND_LIST_PAYLOAD,
27
+ )
28
+
29
+ def _wait_for_atom(atom_id: bytes, interval: float, retries: int) -> Optional[Atom]:
30
+ if interval <= 0 or retries <= 0:
31
+ return self.get_atom_from_local_storage(atom_id=atom_id)
32
+ for _ in range(retries):
33
+ atom = self.get_atom_from_local_storage(atom_id=atom_id)
34
+ if atom is not None:
35
+ return atom
36
+ sleep(interval)
37
+ return self.get_atom_from_local_storage(atom_id=atom_id)
38
+
39
+ def _wait_for_list(root_hash: bytes, interval: float, retries: int) -> Optional[List[Atom]]:
40
+ if interval <= 0 or retries <= 0:
41
+ return self.get_atom_list_from_local_storage(root_hash=root_hash)
42
+ for _ in range(retries):
43
+ atoms = self.get_atom_list_from_local_storage(root_hash=root_hash)
44
+ if atoms is not None:
45
+ return atoms
46
+ sleep(interval)
47
+ return self.get_atom_list_from_local_storage(root_hash=root_hash)
48
+
49
+ def _wait_for_payload() -> Optional[Union[Atom, List[Atom]]]:
50
+ wait_interval = self.config["atom_fetch_interval"]
51
+ wait_retries = self.config["atom_fetch_retries"]
52
+ if payload_type == OBJECT_FOUND_ATOM_PAYLOAD:
53
+ return _wait_for_atom(atom_id, wait_interval, wait_retries)
54
+ if payload_type == OBJECT_FOUND_LIST_PAYLOAD:
55
+ return _wait_for_list(atom_id, wait_interval, wait_retries)
56
+ self.logger.warning(
57
+ "Unknown payload type %s for %s",
58
+ payload_type,
59
+ atom_id.hex(),
60
+ )
61
+ return None
62
+
63
+ if payload_type == OBJECT_FOUND_ATOM_PAYLOAD:
64
+ local_atom = self.get_atom_from_local_storage(atom_id=atom_id)
65
+ if local_atom is not None:
66
+ return local_atom
67
+ elif payload_type == OBJECT_FOUND_LIST_PAYLOAD:
68
+ local_atoms = self.get_atom_list_from_local_storage(root_hash=atom_id)
69
+ if local_atoms is not None:
70
+ return local_atoms
71
+ else:
72
+ self.logger.warning(
73
+ "Unknown payload type %s for %s",
74
+ payload_type,
75
+ atom_id.hex(),
76
+ )
77
+
78
+ if not getattr(self, "is_connected", False):
79
+ self.logger.debug("Network fetch skipped for %s; node not connected", atom_id.hex())
80
+ return None
81
+ self.logger.debug("Attempting network fetch for %s", atom_id.hex())
82
+
83
+ provider_id = self.storage_index.get(atom_id)
84
+ if provider_id is not None:
85
+ provider_payload = provider_payload_for_id(self, provider_id)
86
+ if provider_payload is not None:
87
+ try:
88
+ from ...communication.handlers.object_response import decode_object_provider
89
+ from ...communication.handlers.object_request import (
90
+ ObjectRequest,
91
+ ObjectRequestType,
92
+ )
93
+ from ...communication.models.message import Message, MessageTopic
94
+ from ...communication.outgoing_queue import enqueue_outgoing
95
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
96
+
97
+ provider_key, provider_address, provider_port = decode_object_provider(provider_payload)
98
+ provider_public_key = X25519PublicKey.from_public_bytes(provider_key)
99
+ shared_key_bytes = self.relay_secret_key.exchange(provider_public_key)
100
+
101
+ obj_req = ObjectRequest(
102
+ type=ObjectRequestType.OBJECT_GET,
103
+ data=b"",
104
+ atom_id=atom_id,
105
+ payload_type=payload_type,
106
+ )
107
+ message = Message(
108
+ topic=MessageTopic.OBJECT_REQUEST,
109
+ content=obj_req.to_bytes(),
110
+ sender=self.relay_public_key,
111
+ )
112
+ message.encrypt(shared_key_bytes)
113
+ self.add_atom_req(atom_id, payload_type)
114
+ queued = enqueue_outgoing(
115
+ self,
116
+ (provider_address, provider_port),
117
+ message=message,
118
+ difficulty=1,
119
+ )
120
+ if queued:
121
+ self.logger.debug(
122
+ "Requested atom %s from indexed provider %s:%s",
123
+ atom_id.hex(),
124
+ provider_address,
125
+ provider_port,
126
+ )
127
+ else:
128
+ self.logger.debug(
129
+ "Dropped request for atom %s to indexed provider %s:%s",
130
+ atom_id.hex(),
131
+ provider_address,
132
+ provider_port,
133
+ )
134
+ except Exception as exc:
135
+ self.logger.warning("Failed indexed fetch for %s: %s", atom_id.hex(), exc)
136
+ return _wait_for_payload()
137
+ self.logger.warning("Unknown provider id %s for %s", provider_id, atom_id.hex())
138
+
139
+ self.logger.debug("Falling back to network fetch for %s", atom_id.hex())
140
+
141
+ from ...communication.handlers.object_request import (
142
+ ObjectRequest,
143
+ ObjectRequestType,
144
+ )
145
+ from ...communication.models.message import Message, MessageTopic
146
+ from ...communication.outgoing_queue import enqueue_outgoing
147
+
148
+ try:
149
+ closest_peer = self.peer_route.closest_peer_for_hash(atom_id)
150
+ except Exception as exc:
151
+ self.logger.warning("Peer lookup failed for %s: %s", atom_id.hex(), exc)
152
+ return _wait_for_payload()
153
+
154
+ if closest_peer is None or closest_peer.address is None:
155
+ self.logger.debug("No peer available to fetch %s", atom_id.hex())
156
+ return None
157
+
158
+ obj_req = ObjectRequest(
159
+ type=ObjectRequestType.OBJECT_GET,
160
+ data=b"",
161
+ atom_id=atom_id,
162
+ payload_type=payload_type,
163
+ )
164
+ try:
165
+ message = Message(
166
+ topic=MessageTopic.OBJECT_REQUEST,
167
+ content=obj_req.to_bytes(),
168
+ sender=self.relay_public_key,
169
+ )
170
+ except Exception as exc:
171
+ self.logger.warning("Failed to build object request for %s: %s", atom_id.hex(), exc)
172
+ return None
173
+
174
+ # encrypt the outbound request for the target peer
175
+ message.encrypt(closest_peer.shared_key_bytes)
176
+
177
+ try:
178
+ self.add_atom_req(atom_id, payload_type)
179
+ except Exception as exc:
180
+ self.logger.warning("Failed to track object request for %s: %s", atom_id.hex(), exc)
181
+
182
+ try:
183
+ queued = enqueue_outgoing(
184
+ self,
185
+ closest_peer.address,
186
+ message=message,
187
+ difficulty=closest_peer.difficulty,
188
+ )
189
+ if queued:
190
+ self.logger.debug(
191
+ "Queued OBJECT_GET for %s to peer %s",
192
+ atom_id.hex(),
193
+ closest_peer.address,
194
+ )
195
+ else:
196
+ self.logger.debug(
197
+ "Dropped OBJECT_GET for %s to peer %s",
198
+ atom_id.hex(),
199
+ closest_peer.address,
200
+ )
201
+ except Exception as exc:
202
+ self.logger.warning(
203
+ "Failed to queue OBJECT_GET for %s to %s: %s",
204
+ atom_id.hex(),
205
+ closest_peer.address,
206
+ exc,
207
+ )
208
+ return _wait_for_payload()
209
+
210
+ def get_atom_from_local_storage(self, atom_id: bytes) -> Optional[Atom]:
211
+ """Retrieve an Atom by checking only local hot and cold storage."""
212
+ self.logger.debug("Fetching atom %s (local only)", atom_id.hex())
213
+ atom = self._hot_storage_get(atom_id)
214
+ if atom is not None:
215
+ self.logger.debug("Returning atom %s from hot storage", atom_id.hex())
216
+ return atom
217
+ atom = self._cold_storage_get(atom_id)
218
+ if atom is not None:
219
+ self.logger.debug("Returning atom %s from cold storage", atom_id.hex())
220
+ return atom
221
+ self.logger.debug("Local storage miss for %s", atom_id.hex())
222
+ return None
223
+
224
+
225
+ def get_atom(self, atom_id: bytes) -> Optional[Atom]:
226
+ """Retrieve an atom locally first, then request it from the network."""
227
+ atom = self.get_atom_from_local_storage(atom_id=atom_id)
228
+ if atom is not None:
229
+ return atom
230
+ from ...communication.handlers.object_response import OBJECT_FOUND_ATOM_PAYLOAD
231
+
232
+ result = self._network_get(atom_id, OBJECT_FOUND_ATOM_PAYLOAD)
233
+ if isinstance(result, Atom):
234
+ return result
235
+ return None
236
+
237
+
238
+ def get_atom_list_from_local_storage(self, root_hash: bytes) -> Optional[List[Atom]]:
239
+ """Follow a local-only atom list chain, returning atoms or None on gaps."""
240
+ next_id = root_hash
241
+ atoms: List[Atom] = []
242
+ while next_id != ZERO32:
243
+ atom = self.get_atom_from_local_storage(atom_id=next_id)
244
+ if atom is None:
245
+ return None
246
+ atoms.append(atom)
247
+ next_id = atom.next_id
248
+ return atoms
249
+
250
+
251
+ def get_atom_list(self, root_hash: bytes) -> Optional[List[Atom]]:
252
+ """Retrieve an atom list locally first, then request it from the network."""
253
+ atoms = self.get_atom_list_from_local_storage(root_hash=root_hash)
254
+ if atoms is not None:
255
+ return atoms
256
+ from ...communication.handlers.object_response import OBJECT_FOUND_LIST_PAYLOAD
257
+
258
+ result = self._network_get(root_hash, OBJECT_FOUND_LIST_PAYLOAD)
259
+ if isinstance(result, list):
260
+ return result
261
+ return None
262
+
263
+
264
+ def _cold_storage_get(self, key: bytes) -> Optional[Atom]:
265
+ """Read an atom from the cold storage directory if configured."""
266
+ if not self.config["cold_storage_path"]:
267
+ self.logger.debug("Cold storage disabled; cannot fetch %s", key.hex())
268
+ return None
269
+ filename = f"{key.hex().upper()}.bin"
270
+ file_path = Path(self.config["cold_storage_path"]) / filename
271
+ try:
272
+ data = file_path.read_bytes()
273
+ except FileNotFoundError:
274
+ self.logger.debug("Cold storage miss for %s", key.hex())
275
+ return None
276
+ except OSError as exc:
277
+ self.logger.warning("Error reading cold storage file %s: %s", file_path, exc)
278
+ return None
279
+ try:
280
+ atom = Atom.from_bytes(data)
281
+ self.logger.debug("Loaded atom %s from cold storage", key.hex())
282
+ return atom
283
+ except ValueError as exc:
284
+ self.logger.warning("Cold storage data corrupted for %s: %s", file_path, exc)
285
+ return None