astreum 0.2.41__py3-none-any.whl → 0.3.1__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 (82) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +83 -0
  4. astreum/communication/handlers/ping.py +48 -0
  5. astreum/communication/handlers/storage_request.py +81 -0
  6. astreum/communication/models/__init__.py +0 -0
  7. astreum/{_communication → communication/models}/message.py +1 -0
  8. astreum/communication/models/peer.py +23 -0
  9. astreum/{_communication → communication/models}/route.py +45 -8
  10. astreum/{_communication → communication}/setup.py +46 -95
  11. astreum/communication/start.py +38 -0
  12. astreum/consensus/__init__.py +20 -0
  13. astreum/consensus/genesis.py +66 -0
  14. astreum/consensus/models/__init__.py +0 -0
  15. astreum/consensus/models/account.py +84 -0
  16. astreum/consensus/models/accounts.py +72 -0
  17. astreum/consensus/models/block.py +364 -0
  18. astreum/{_consensus → consensus/models}/chain.py +7 -7
  19. astreum/{_consensus → consensus/models}/fork.py +8 -8
  20. astreum/consensus/models/receipt.py +98 -0
  21. astreum/consensus/models/transaction.py +213 -0
  22. astreum/{_consensus → consensus}/setup.py +26 -11
  23. astreum/consensus/start.py +68 -0
  24. astreum/consensus/validator.py +95 -0
  25. astreum/{_consensus → consensus}/workers/discovery.py +20 -1
  26. astreum/consensus/workers/validation.py +291 -0
  27. astreum/{_consensus → consensus}/workers/verify.py +32 -3
  28. astreum/machine/__init__.py +20 -0
  29. astreum/machine/evaluations/__init__.py +0 -0
  30. astreum/machine/evaluations/high_evaluation.py +237 -0
  31. astreum/machine/evaluations/low_evaluation.py +281 -0
  32. astreum/machine/evaluations/script_evaluation.py +27 -0
  33. astreum/machine/models/__init__.py +0 -0
  34. astreum/machine/models/environment.py +31 -0
  35. astreum/machine/models/expression.py +218 -0
  36. astreum/{_lispeum → machine}/parser.py +26 -31
  37. astreum/machine/tokenizer.py +90 -0
  38. astreum/node.py +73 -781
  39. astreum/storage/__init__.py +7 -0
  40. astreum/storage/actions/get.py +69 -0
  41. astreum/storage/actions/set.py +132 -0
  42. astreum/storage/models/atom.py +107 -0
  43. astreum/{_storage/patricia.py → storage/models/trie.py} +236 -177
  44. astreum/storage/setup.py +44 -15
  45. astreum/utils/bytes.py +24 -0
  46. astreum/utils/integer.py +25 -0
  47. astreum/utils/logging.py +219 -0
  48. astreum-0.3.1.dist-info/METADATA +160 -0
  49. astreum-0.3.1.dist-info/RECORD +62 -0
  50. astreum/_communication/peer.py +0 -11
  51. astreum/_consensus/__init__.py +0 -20
  52. astreum/_consensus/account.py +0 -170
  53. astreum/_consensus/accounts.py +0 -67
  54. astreum/_consensus/block.py +0 -328
  55. astreum/_consensus/genesis.py +0 -141
  56. astreum/_consensus/receipt.py +0 -177
  57. astreum/_consensus/transaction.py +0 -192
  58. astreum/_consensus/workers/validation.py +0 -122
  59. astreum/_lispeum/__init__.py +0 -16
  60. astreum/_lispeum/environment.py +0 -13
  61. astreum/_lispeum/expression.py +0 -37
  62. astreum/_lispeum/high_evaluation.py +0 -177
  63. astreum/_lispeum/low_evaluation.py +0 -123
  64. astreum/_lispeum/tokenizer.py +0 -22
  65. astreum/_node.py +0 -58
  66. astreum/_storage/__init__.py +0 -5
  67. astreum/_storage/atom.py +0 -117
  68. astreum/format.py +0 -75
  69. astreum/models/block.py +0 -441
  70. astreum/models/merkle.py +0 -205
  71. astreum/models/patricia.py +0 -393
  72. astreum/storage/object.py +0 -68
  73. astreum-0.2.41.dist-info/METADATA +0 -146
  74. astreum-0.2.41.dist-info/RECORD +0 -53
  75. /astreum/{models → communication/handlers}/__init__.py +0 -0
  76. /astreum/{_communication → communication/models}/ping.py +0 -0
  77. /astreum/{_communication → communication}/util.py +0 -0
  78. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  79. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  80. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
  81. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
  82. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,13 @@
1
- import blake3
2
- from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
3
-
4
- from .atom import Atom, ZERO32
5
-
6
- if TYPE_CHECKING:
7
- from .._node import Node
1
+ from typing import Dict, List, Optional, Set, Tuple, TYPE_CHECKING
2
+
3
+ from .atom import Atom, AtomKind, ZERO32
8
4
 
9
- class PatriciaNode:
5
+ if TYPE_CHECKING:
6
+ from .._node import Node
7
+
8
+ class TrieNode:
10
9
  """
11
- A node in a compressed-key Patricia trie.
10
+ A node in a compressed-key Binary Radix Tree.
12
11
 
13
12
  Attributes:
14
13
  key_len (int): Number of bits in the `key` prefix that are meaningful.
@@ -31,100 +30,115 @@ class PatriciaNode:
31
30
  self.value = value
32
31
  self.child_0 = child_0
33
32
  self.child_1 = child_1
34
- self._hash: Optional[bytes] = None
35
-
36
- def hash(self) -> bytes:
37
- """
38
- Compute and cache the BLAKE3 hash of this node's serialized form.
39
- """
40
- if self._hash is None:
41
- self._hash = blake3.blake3(self.to_bytes()).digest()
42
- return self._hash
43
-
44
- def to_atoms(self) -> Tuple[bytes, List[Atom]]:
33
+ self._hash: Optional[bytes] = None
34
+
35
+ def hash(self) -> bytes:
45
36
  """
46
- Materialise this node as a flat atom chain containing the fields in a
47
- traversal-friendly order: key length prefix (single byte) + key bytes,
48
- child_0 hash, child_1 hash, and value bytes. Returns the head atom hash
49
- and the atom list.
37
+ Compute and cache the canonical hash for this node (its type-atom id).
50
38
  """
51
- if self.key_len > 255:
52
- raise ValueError("Patricia key length > 255 bits cannot be encoded in a single atom field")
39
+ if self._hash is None:
40
+ head_hash, _ = self._render_atoms()
41
+ self._hash = head_hash
42
+ return self._hash
53
43
 
44
+ def to_bytes(self) -> bytes:
45
+ """
46
+ Serialize for hashing: key_len (u16 big-endian) + key payload +
47
+ child_0 (or ZERO32) + child_1 (or ZERO32) + value.
48
+ """
49
+ key_len_bytes = self.key_len.to_bytes(2, "big", signed=False)
50
+ child0 = self.child_0 or ZERO32
51
+ child1 = self.child_1 or ZERO32
52
+ value = self.value or b""
53
+ return key_len_bytes + self.key + child0 + child1 + value
54
+
55
+ def _render_atoms(self) -> Tuple[bytes, List[Atom]]:
56
+ """
57
+ Materialise this node with the canonical atom layout used by the
58
+ storage layer: a leading SYMBOL atom with payload ``b"trie"`` whose
59
+ ``next`` pointer links to four BYTES atoms containing, in order:
60
+ key (len byte + key payload), child_0 hash, child_1 hash, value bytes.
61
+ Returns the top atom hash and the emitted atoms.
62
+ """
54
63
  entries: List[bytes] = [
55
- bytes([self.key_len]) + self.key,
64
+ self.key_len.to_bytes(2, "big", signed=False) + self.key,
56
65
  self.child_0 or ZERO32,
57
66
  self.child_1 or ZERO32,
58
67
  self.value or b"",
59
68
  ]
60
69
 
61
- atoms: List[Atom] = []
70
+ data_atoms: List[Atom] = []
62
71
  next_hash = ZERO32
63
72
  for payload in reversed(entries):
64
- atom = Atom.from_data(data=payload, next_hash=next_hash)
65
- atoms.append(atom)
73
+ atom = Atom(data=payload, next_id=next_hash, kind=AtomKind.BYTES)
74
+ data_atoms.append(atom)
66
75
  next_hash = atom.object_id()
67
76
 
68
- head = next_hash
69
- atoms.reverse()
70
- return head, atoms
71
-
72
- @classmethod
73
- def from_atoms(
74
- cls,
75
- node: "Node",
76
- head_hash: bytes,
77
- ) -> "PatriciaNode":
78
- """
79
- Reconstruct a node from the atom chain rooted at `head_hash`, using the
80
- supplied `node` instance to resolve atom object ids.
81
- """
82
- if head_hash == ZERO32:
83
- raise ValueError("empty atom chain for Patricia node")
84
-
85
- entries: List[bytes] = []
86
- current = head_hash
87
- hops = 0
77
+ data_atoms.reverse()
88
78
 
89
- while current != ZERO32 and hops < 4:
90
- atom = node._local_get(current)
91
- if atom is None:
92
- raise ValueError("missing atom while decoding Patricia node")
93
- entries.append(atom.data)
94
- current = atom.next
95
- hops += 1
79
+ type_atom = Atom(data=b"trie", next_id=next_hash, kind=AtomKind.SYMBOL)
96
80
 
97
- if current != ZERO32:
98
- raise ValueError("too many fields while decoding Patricia node")
81
+ atoms = data_atoms + [type_atom]
82
+ return type_atom.object_id(), atoms
99
83
 
100
- if len(entries) != 4:
101
- raise ValueError("incomplete atom sequence for Patricia node")
102
-
103
- key_entry = entries[0]
104
- if not key_entry:
84
+ def to_atoms(self) -> Tuple[bytes, List[Atom]]:
85
+ head_hash, atoms = self._render_atoms()
86
+ self._hash = head_hash
87
+ return head_hash, atoms
88
+
89
+ @classmethod
90
+ def from_atoms(
91
+ cls,
92
+ node: "Node",
93
+ head_hash: bytes,
94
+ ) -> "TrieNode":
95
+ """
96
+ Reconstruct a node from the atom chain rooted at `head_hash`, using the
97
+ supplied `node` instance to resolve atom object ids.
98
+ """
99
+ if head_hash == ZERO32:
100
+ raise ValueError("empty atom chain for Patricia node")
101
+
102
+ atom_chain = node.get_atom_list_from_storage(head_hash)
103
+ if atom_chain is None or len(atom_chain) != 5:
104
+ raise ValueError("malformed Patricia atom chain")
105
+
106
+ type_atom, key_atom, child0_atom, child1_atom, value_atom = atom_chain
107
+
108
+ if type_atom.kind is not AtomKind.SYMBOL:
109
+ raise ValueError("malformed Patricia node (type atom kind)")
110
+ if type_atom.data != b"trie":
111
+ raise ValueError("not a Patricia node (type mismatch)")
112
+
113
+ for detail in (key_atom, child0_atom, child1_atom, value_atom):
114
+ if detail.kind is not AtomKind.BYTES:
115
+ raise ValueError("Patricia node detail atoms must be bytes")
116
+
117
+ key_entry = key_atom.data
118
+ if len(key_entry) < 2:
105
119
  raise ValueError("missing key entry while decoding Patricia node")
106
- key_len = key_entry[0]
107
- key = key_entry[1:]
108
- child_0 = entries[1] if entries[1] != ZERO32 else None
109
- child_1 = entries[2] if entries[2] != ZERO32 else None
110
- value = entries[3]
111
-
112
- return cls(key_len=key_len, key=key, value=value, child_0=child_0, child_1=child_1)
120
+ key_len = int.from_bytes(key_entry[:2], "big", signed=False)
121
+ key = key_entry[2:]
122
+ child_0 = child0_atom.data if child0_atom.data != ZERO32 else None
123
+ child_1 = child1_atom.data if child1_atom.data != ZERO32 else None
124
+ value = value_atom.data
125
+
126
+ return cls(key_len=key_len, key=key, value=value, child_0=child_0, child_1=child_1)
113
127
 
114
- class PatriciaTrie:
128
+ class Trie:
115
129
  """
116
- A compressed-key Patricia trie supporting get and put.
130
+ A compressed-key Binary Radix Tree supporting get and put.
117
131
  """
118
132
 
119
- def __init__(
120
- self,
121
- root_hash: Optional[bytes] = None,
122
- ) -> None:
123
- """
124
- :param root_hash: optional hash of existing root node
125
- """
126
- self.nodes: Dict[bytes, PatriciaNode] = {}
127
- self.root_hash = root_hash
133
+ def __init__(
134
+ self,
135
+ root_hash: Optional[bytes] = None,
136
+ ) -> None:
137
+ """
138
+ :param root_hash: optional hash of existing root node
139
+ """
140
+ self.nodes: Dict[bytes, TrieNode] = {}
141
+ self.root_hash = root_hash
128
142
 
129
143
  @staticmethod
130
144
  def _bit(buf: bytes, idx: int) -> bool:
@@ -154,67 +168,113 @@ class PatriciaTrie:
154
168
  return False
155
169
  return True
156
170
 
157
- def _fetch(self, storage_node: "Node", h: bytes) -> Optional[PatriciaNode]:
158
- """
159
- Fetch a node by hash, consulting the in-memory cache first and falling
160
- back to the atom storage provided by `storage_node`.
161
- """
162
- cached = self.nodes.get(h)
163
- if cached is not None:
164
- return cached
165
-
166
- if storage_node._local_get(h) is None:
167
- return None
168
-
169
- pat_node = PatriciaNode.from_atoms(storage_node, h)
170
- self.nodes[h] = pat_node
171
- return pat_node
171
+ def _fetch(self, storage_node: "Node", h: bytes) -> Optional[TrieNode]:
172
+ """
173
+ Fetch a node by hash, consulting the in-memory cache first and falling
174
+ back to the atom storage provided by `storage_node`.
175
+ """
176
+ cached = self.nodes.get(h)
177
+ if cached is not None:
178
+ return cached
179
+
180
+ if storage_node.storage_get(h) is None:
181
+ return None
182
+
183
+ pat_node = TrieNode.from_atoms(storage_node, h)
184
+ self.nodes[h] = pat_node
185
+ return pat_node
172
186
 
173
187
  def get(self, storage_node: "Node", key: bytes) -> Optional[bytes]:
174
188
  """
175
189
  Return the stored value for `key`, or None if absent.
176
190
  """
177
- # Empty trie?
178
- if self.root_hash is None:
179
- return None
191
+ # Empty trie?
192
+ if self.root_hash is None:
193
+ return None
194
+
195
+ current = self._fetch(storage_node, self.root_hash)
196
+ if current is None:
197
+ return None
198
+
199
+ key_pos = 0 # bit offset into key
200
+
201
+ while current is not None:
202
+ # 1) Check that this node's prefix matches the key here
203
+ if not self._match_prefix(current.key, current.key_len, key, key_pos):
204
+ return None
205
+ key_pos += current.key_len
206
+
207
+ # 2) If we've consumed all bits of the search key:
208
+ if key_pos == len(key) * 8:
209
+ # Return value only if this node actually stores one
210
+ return current.value
211
+
212
+ # 3) Decide which branch to follow via next bit
213
+ try:
214
+ next_bit = self._bit(key, key_pos)
215
+ except IndexError:
216
+ return None
217
+
218
+ child_hash = current.child_1 if next_bit else current.child_0
219
+ if child_hash is None:
220
+ return None # dead end
221
+
222
+ # 4) Fetch child and continue descent
223
+ current = self._fetch(storage_node, child_hash)
224
+ if current is None:
225
+ return None # dangling pointer
226
+
227
+ key_pos += 1 # consumed routing bit
180
228
 
181
- current = self._fetch(storage_node, self.root_hash)
182
- if current is None:
183
- return None
229
+ return None
184
230
 
185
- key_pos = 0 # bit offset into key
231
+ def get_all(self, storage_node: "Node") -> Dict[bytes, bytes]:
232
+ """
233
+ Return a mapping of every key/value pair stored in the trie.
234
+ """
235
+ if self.root_hash is None or self.root_hash == ZERO32:
236
+ return {}
186
237
 
187
- while current is not None:
188
- # 1) Check that this node's prefix matches the key here
189
- if not self._match_prefix(current.key, current.key_len, key, key_pos):
190
- return None
191
- key_pos += current.key_len
238
+ def _bits_from_payload(payload: bytes, bit_length: int) -> str:
239
+ if bit_length <= 0 or not payload:
240
+ return ""
241
+ bit_stream = "".join(f"{byte:08b}" for byte in payload)
242
+ return bit_stream[:bit_length]
192
243
 
193
- # 2) If we've consumed all bits of the search key:
194
- if key_pos == len(key) * 8:
195
- # Return value only if this node actually stores one
196
- return current.value
244
+ def _bits_to_bytes(bit_string: str) -> bytes:
245
+ if not bit_string:
246
+ return b""
247
+ pad = (8 - (len(bit_string) % 8)) % 8
248
+ bit_string = bit_string + ("0" * pad)
249
+ byte_len = len(bit_string) // 8
250
+ return int(bit_string, 2).to_bytes(byte_len, "big")
197
251
 
198
- # 3) Decide which branch to follow via next bit
199
- try:
200
- next_bit = self._bit(key, key_pos)
201
- except IndexError:
202
- return None
252
+ results: Dict[bytes, bytes] = {}
253
+ stack: List[Tuple[bytes, str]] = [(self.root_hash, "")]
254
+ visited: Set[bytes] = set()
203
255
 
204
- child_hash = current.child_1 if next_bit else current.child_0
205
- if child_hash is None:
206
- return None # dead end
256
+ while stack:
257
+ node_hash, prefix_bits = stack.pop()
258
+ if not node_hash or node_hash == ZERO32 or node_hash in visited:
259
+ continue
260
+ visited.add(node_hash)
207
261
 
208
- # 4) Fetch child and continue descent
209
- current = self._fetch(storage_node, child_hash)
210
- if current is None:
211
- return None # dangling pointer
262
+ pat_node = TrieNode.from_atoms(storage_node, node_hash)
263
+ node_bits = _bits_from_payload(pat_node.key, pat_node.key_len)
264
+ combined_bits = prefix_bits + node_bits
212
265
 
213
- key_pos += 1 # consumed routing bit
266
+ if pat_node.value is not None:
267
+ key_bytes = _bits_to_bytes(combined_bits)
268
+ results[key_bytes] = pat_node.value
214
269
 
215
- return None
270
+ if pat_node.child_0:
271
+ stack.append((pat_node.child_0, combined_bits + "0"))
272
+ if pat_node.child_1:
273
+ stack.append((pat_node.child_1, combined_bits + "1"))
274
+
275
+ return results
216
276
 
217
- def put(self, storage_node: "Node", key: bytes, value: bytes) -> None:
277
+ def put(self, storage_node: "Node", key: bytes, value: bytes) -> None:
218
278
  """
219
279
  Insert or update `key` with `value` in-place.
220
280
  """
@@ -227,64 +287,64 @@ class PatriciaTrie:
227
287
  return
228
288
 
229
289
  # S2 – traversal bookkeeping
230
- stack: List[Tuple[PatriciaNode, bytes, int]] = [] # (parent, parent_hash, dir_bit)
231
- current = self._fetch(storage_node, self.root_hash)
232
- assert current is not None
290
+ stack: List[Tuple[TrieNode, bytes, int]] = [] # (parent, parent_hash, dir_bit)
291
+ current = self._fetch(storage_node, self.root_hash)
292
+ assert current is not None
233
293
  key_pos = 0
234
294
 
235
295
  # S4 – main descent loop
236
296
  while True:
237
297
  # 4.1 – prefix mismatch? → split
238
- if not self._match_prefix(current.key, current.key_len, key, key_pos):
239
- self._split_and_insert(current, stack, key, key_pos, value)
298
+ if not self._match_prefix(current.key, current.key_len, key, key_pos):
299
+ self._split_and_insert(current, stack, key, key_pos, value)
240
300
  return
241
301
 
242
302
  # 4.2 – consume this prefix
243
- key_pos += current.key_len
303
+ key_pos += current.key_len
244
304
 
245
305
  # 4.3 – matched entire key → update value
246
306
  if key_pos == total_bits:
247
- old_hash = current.hash()
248
- current.value = value
249
- self._invalidate_hash(current)
250
- new_hash = current.hash()
251
- if new_hash != old_hash:
252
- self.nodes.pop(old_hash, None)
253
- self.nodes[new_hash] = current
307
+ old_hash = current.hash()
308
+ current.value = value
309
+ self._invalidate_hash(current)
310
+ new_hash = current.hash()
311
+ if new_hash != old_hash:
312
+ self.nodes.pop(old_hash, None)
313
+ self.nodes[new_hash] = current
254
314
  self._bubble(stack, new_hash)
255
315
  return
256
316
 
257
317
  # 4.4 – routing bit
258
318
  next_bit = self._bit(key, key_pos)
259
- child_hash = current.child_1 if next_bit else current.child_0
319
+ child_hash = current.child_1 if next_bit else current.child_0
260
320
 
261
321
  # 4.6 – no child → easy append leaf
262
322
  if child_hash is None:
263
- self._append_leaf(current, next_bit, key, key_pos, value, stack)
323
+ self._append_leaf(current, next_bit, key, key_pos, value, stack)
264
324
  return
265
325
 
266
326
  # 4.7 – push current node onto stack
267
- stack.append((current, current.hash(), int(next_bit)))
327
+ stack.append((current, current.hash(), int(next_bit)))
268
328
 
269
329
  # 4.8 – fetch child and continue
270
- child = self._fetch(storage_node, child_hash)
271
- if child is None:
272
- # Dangling pointer: treat as missing child
273
- parent, _, _ = stack[-1]
274
- self._append_leaf(parent, next_bit, key, key_pos, value, stack[:-1])
275
- return
276
-
277
- current = child
278
- key_pos += 1 # consumed routing bit
330
+ child = self._fetch(storage_node, child_hash)
331
+ if child is None:
332
+ # Dangling pointer: treat as missing child
333
+ parent, _, _ = stack[-1]
334
+ self._append_leaf(parent, next_bit, key, key_pos, value, stack[:-1])
335
+ return
336
+
337
+ current = child
338
+ key_pos += 1 # consumed routing bit
279
339
 
280
340
  def _append_leaf(
281
341
  self,
282
- parent: PatriciaNode,
342
+ parent: TrieNode,
283
343
  dir_bit: bool,
284
344
  key: bytes,
285
345
  key_pos: int,
286
346
  value: bytes,
287
- stack: List[Tuple[PatriciaNode, bytes, int]],
347
+ stack: List[Tuple[TrieNode, bytes, int]],
288
348
  ) -> None:
289
349
  tail_len = len(key) * 8 - (key_pos + 1)
290
350
  tail_bits, tail_len = self._bit_slice(key, key_pos + 1, tail_len)
@@ -307,8 +367,8 @@ class PatriciaTrie:
307
367
 
308
368
  def _split_and_insert(
309
369
  self,
310
- node: PatriciaNode,
311
- stack: List[Tuple[PatriciaNode, bytes, int]],
370
+ node: TrieNode,
371
+ stack: List[Tuple[TrieNode, bytes, int]],
312
372
  key: bytes,
313
373
  key_pos: int,
314
374
  value: bytes,
@@ -381,46 +441,45 @@ class PatriciaTrie:
381
441
  value: Optional[bytes],
382
442
  child0: Optional[bytes],
383
443
  child1: Optional[bytes],
384
- ) -> PatriciaNode:
385
- node = PatriciaNode(prefix_len, prefix_bits, value, child0, child1)
444
+ ) -> TrieNode:
445
+ node = TrieNode(prefix_len, prefix_bits, value, child0, child1)
386
446
  self.nodes[node.hash()] = node
387
447
  return node
388
448
 
389
- def _invalidate_hash(self, node: PatriciaNode) -> None:
449
+ def _invalidate_hash(self, node: TrieNode) -> None:
390
450
  """Clear cached hash so next .hash() recomputes."""
391
451
  node._hash = None # type: ignore
392
452
 
393
453
  def _bubble(
394
454
  self,
395
- stack: List[Tuple[PatriciaNode, bytes, int]],
455
+ stack: List[Tuple[TrieNode, bytes, int]],
396
456
  new_hash: bytes
397
457
  ) -> None:
398
458
  """
399
459
  Propagate updated child-hash `new_hash` up the ancestor stack,
400
460
  rebasing each parent's pointer, invalidating and re-hashing.
401
461
  """
402
- while stack:
403
- parent, old_hash, dir_bit = stack.pop()
404
-
405
- if dir_bit == 0:
406
- parent.child_0 = new_hash
462
+ while stack:
463
+ parent, old_hash, dir_bit = stack.pop()
464
+
465
+ if dir_bit == 0:
466
+ parent.child_0 = new_hash
407
467
  else:
408
468
  parent.child_1 = new_hash
409
469
 
410
470
  self._invalidate_hash(parent)
411
471
  new_hash = parent.hash()
412
472
  if new_hash != old_hash:
413
- self.nodes.pop(old_hash, None)
414
- self.nodes[new_hash] = parent
415
-
416
- self.root_hash = new_hash
417
-
418
-
419
- def _bit_slice(
420
- self,
421
- buf: bytes,
422
- start_bit: int,
423
- length: int
473
+ self.nodes.pop(old_hash, None)
474
+ self.nodes[new_hash] = parent
475
+
476
+ self.root_hash = new_hash
477
+
478
+ def _bit_slice(
479
+ self,
480
+ buf: bytes,
481
+ start_bit: int,
482
+ length: int
424
483
  ) -> tuple[bytes, int]:
425
484
  """
426
485
  Extract `length` bits from `buf` starting at `start_bit` (MSB-first),
astreum/storage/setup.py CHANGED
@@ -1,15 +1,44 @@
1
- from pathlib import Path
2
- from typing import Optional, Dict, Tuple, Any
3
-
4
- def storage_setup(config: dict) -> Tuple[Optional[Path], Dict[bytes, Any], int, Dict[bytes, bytes]]:
5
- storage_path_str = config.get('storage_path')
6
- if storage_path_str is None:
7
- storage_path, memory_storage = None, {}
8
- else:
9
- storage_path = Path(storage_path_str)
10
- storage_path.mkdir(parents=True, exist_ok=True)
11
- memory_storage = None
12
-
13
- timeout = config.get('storage_get_relay_timeout', 5)
14
- storage_index: Dict[bytes, bytes] = {}
15
- return storage_path, memory_storage, timeout, storage_index
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+
7
+ def storage_setup(node: Any, config: dict) -> None:
8
+ """Initialize hot/cold storage helpers on the node."""
9
+
10
+ node.logger.info("Setting up node storage")
11
+
12
+ node.hot_storage = {}
13
+ node.hot_storage_hits = {}
14
+ node.storage_index = {}
15
+ node.hot_storage_size = 0
16
+ hot_storage_default_limit = 1 << 30 # 1 GiB
17
+ hot_storage_limit_value = config.get("hot_storage_limit", hot_storage_default_limit)
18
+ try:
19
+ node.hot_storage_limit = int(hot_storage_limit_value)
20
+ except (TypeError, ValueError):
21
+ node.hot_storage_limit = hot_storage_default_limit
22
+
23
+ node.cold_storage_size = 0
24
+ cold_storage_default_limit = 10 << 30 # 10 GiB
25
+ cold_storage_limit_value = config.get("cold_storage_limit", cold_storage_default_limit)
26
+ try:
27
+ node.cold_storage_limit = int(cold_storage_limit_value)
28
+ except (TypeError, ValueError):
29
+ node.cold_storage_limit = cold_storage_default_limit
30
+
31
+ cold_storage_path = config.get("cold_storage_path")
32
+ if cold_storage_path:
33
+ try:
34
+ Path(cold_storage_path).mkdir(parents=True, exist_ok=True)
35
+ except OSError:
36
+ cold_storage_path = None
37
+ node.cold_storage_path = cold_storage_path
38
+
39
+ node.logger.info(
40
+ "Storage ready (hot_limit=%s bytes, cold_limit=%s bytes, cold_path=%s)",
41
+ node.hot_storage_limit,
42
+ node.cold_storage_limit,
43
+ node.cold_storage_path or "disabled",
44
+ )
astreum/utils/bytes.py ADDED
@@ -0,0 +1,24 @@
1
+ from typing import Optional
2
+
3
+
4
+ def hex_to_bytes(value: str, *, expected_length: Optional[int] = None) -> bytes:
5
+ """Convert a 0x-prefixed hex string into raw bytes."""
6
+ if not isinstance(value, str):
7
+ raise TypeError("hex value must be provided as a string")
8
+
9
+ if not value.startswith(("0x", "0X")):
10
+ raise ValueError("hex value must start with '0x'")
11
+
12
+ hex_digits = value[2:]
13
+ if len(hex_digits) % 2:
14
+ raise ValueError("hex value must have an even number of digits")
15
+
16
+ try:
17
+ result = bytes.fromhex(hex_digits)
18
+ except ValueError as exc:
19
+ raise ValueError("hex value contains non-hexadecimal characters") from exc
20
+
21
+ if expected_length is not None and len(result) != expected_length:
22
+ raise ValueError(f"hex value must decode to exactly {expected_length} bytes")
23
+
24
+ return result
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Union
4
+
5
+ ByteLike = Union[bytes, bytearray, memoryview]
6
+
7
+
8
+ def int_to_bytes(value: Optional[int]) -> bytes:
9
+ """Convert an integer to a little-endian byte string with minimal length."""
10
+ if value is None:
11
+ return b""
12
+ value = int(value)
13
+ if value == 0:
14
+ return b"\x00"
15
+ length = (value.bit_length() + 7) // 8
16
+ return value.to_bytes(length, "little", signed=False)
17
+
18
+
19
+ def bytes_to_int(data: Optional[ByteLike]) -> int:
20
+ """Convert a little-endian byte string to an integer."""
21
+ if not data:
22
+ return 0
23
+ if isinstance(data, memoryview):
24
+ data = data.tobytes()
25
+ return int.from_bytes(bytes(data), "little", signed=False)