astreum 0.2.61__py3-none-any.whl → 0.3.9__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 (86) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +89 -0
  4. astreum/communication/handlers/object_request.py +176 -0
  5. astreum/communication/handlers/object_response.py +115 -0
  6. astreum/communication/handlers/ping.py +34 -0
  7. astreum/communication/handlers/route_request.py +76 -0
  8. astreum/communication/handlers/route_response.py +53 -0
  9. astreum/communication/models/__init__.py +0 -0
  10. astreum/communication/models/message.py +124 -0
  11. astreum/communication/models/peer.py +51 -0
  12. astreum/{_communication → communication/models}/route.py +7 -12
  13. astreum/communication/processors/__init__.py +0 -0
  14. astreum/communication/processors/incoming.py +98 -0
  15. astreum/communication/processors/outgoing.py +20 -0
  16. astreum/communication/setup.py +166 -0
  17. astreum/communication/start.py +37 -0
  18. astreum/{_communication → communication}/util.py +7 -0
  19. astreum/consensus/__init__.py +20 -0
  20. astreum/consensus/genesis.py +66 -0
  21. astreum/consensus/models/__init__.py +0 -0
  22. astreum/consensus/models/account.py +84 -0
  23. astreum/consensus/models/accounts.py +72 -0
  24. astreum/consensus/models/block.py +364 -0
  25. astreum/{_consensus → consensus/models}/chain.py +7 -7
  26. astreum/{_consensus → consensus/models}/fork.py +8 -8
  27. astreum/consensus/models/receipt.py +98 -0
  28. astreum/{_consensus → consensus/models}/transaction.py +76 -78
  29. astreum/{_consensus → consensus}/setup.py +18 -50
  30. astreum/consensus/start.py +67 -0
  31. astreum/consensus/validator.py +95 -0
  32. astreum/{_consensus → consensus}/workers/discovery.py +19 -1
  33. astreum/consensus/workers/validation.py +307 -0
  34. astreum/{_consensus → consensus}/workers/verify.py +29 -2
  35. astreum/crypto/chacha20poly1305.py +74 -0
  36. astreum/machine/__init__.py +20 -0
  37. astreum/machine/evaluations/__init__.py +0 -0
  38. astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
  39. astreum/machine/evaluations/low_evaluation.py +281 -0
  40. astreum/machine/evaluations/script_evaluation.py +27 -0
  41. astreum/machine/models/__init__.py +0 -0
  42. astreum/machine/models/environment.py +31 -0
  43. astreum/{_lispeum → machine/models}/expression.py +36 -8
  44. astreum/machine/tokenizer.py +90 -0
  45. astreum/node.py +78 -767
  46. astreum/storage/__init__.py +7 -0
  47. astreum/storage/actions/get.py +183 -0
  48. astreum/storage/actions/set.py +178 -0
  49. astreum/{_storage → storage/models}/atom.py +55 -57
  50. astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
  51. astreum/storage/requests.py +28 -0
  52. astreum/storage/setup.py +22 -15
  53. astreum/utils/config.py +48 -0
  54. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
  55. astreum-0.3.9.dist-info/RECORD +71 -0
  56. astreum/_communication/message.py +0 -101
  57. astreum/_communication/peer.py +0 -23
  58. astreum/_communication/setup.py +0 -322
  59. astreum/_consensus/__init__.py +0 -20
  60. astreum/_consensus/account.py +0 -95
  61. astreum/_consensus/accounts.py +0 -38
  62. astreum/_consensus/block.py +0 -311
  63. astreum/_consensus/genesis.py +0 -72
  64. astreum/_consensus/receipt.py +0 -136
  65. astreum/_consensus/workers/validation.py +0 -125
  66. astreum/_lispeum/__init__.py +0 -16
  67. astreum/_lispeum/environment.py +0 -13
  68. astreum/_lispeum/low_evaluation.py +0 -123
  69. astreum/_lispeum/tokenizer.py +0 -22
  70. astreum/_node.py +0 -198
  71. astreum/_storage/__init__.py +0 -7
  72. astreum/_storage/setup.py +0 -35
  73. astreum/format.py +0 -75
  74. astreum/models/block.py +0 -441
  75. astreum/models/merkle.py +0 -205
  76. astreum/models/patricia.py +0 -393
  77. astreum/storage/object.py +0 -68
  78. astreum-0.2.61.dist-info/RECORD +0 -57
  79. /astreum/{models → communication/handlers}/__init__.py +0 -0
  80. /astreum/{_communication → communication/models}/ping.py +0 -0
  81. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  82. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  83. /astreum/{_lispeum → machine}/parser.py +0 -0
  84. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
  85. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
  86. {astreum-0.2.61.dist-info → astreum-0.3.9.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, AtomKind, 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,29 +30,38 @@ 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
33
+ self._hash: Optional[bytes] = None
34
+
35
+ def hash(self) -> bytes:
36
+ """
37
+ Compute and cache the canonical hash for this node (its type-atom id).
38
+ """
39
+ if self._hash is None:
40
+ head_hash, _ = self._render_atoms()
41
+ self._hash = head_hash
42
+ return self._hash
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
43
54
 
44
- def to_atoms(self) -> Tuple[bytes, List[Atom]]:
55
+ def _render_atoms(self) -> Tuple[bytes, List[Atom]]:
45
56
  """
46
57
  Materialise this node with the canonical atom layout used by the
47
- storage layer: a leading SYMBOL atom with payload ``b"radix"`` whose
58
+ storage layer: a leading SYMBOL atom with payload ``b"trie"`` whose
48
59
  ``next`` pointer links to four BYTES atoms containing, in order:
49
60
  key (len byte + key payload), child_0 hash, child_1 hash, value bytes.
50
61
  Returns the top atom hash and the emitted atoms.
51
62
  """
52
- if self.key_len > 255:
53
- raise ValueError("Patricia key length > 255 bits cannot be encoded in a single atom field")
54
-
55
63
  entries: List[bytes] = [
56
- bytes([self.key_len]) + self.key,
64
+ self.key_len.to_bytes(2, "big", signed=False) + self.key,
57
65
  self.child_0 or ZERO32,
58
66
  self.child_1 or ZERO32,
59
67
  self.value or b"",
@@ -62,104 +70,75 @@ class PatriciaNode:
62
70
  data_atoms: List[Atom] = []
63
71
  next_hash = ZERO32
64
72
  for payload in reversed(entries):
65
- atom = Atom.from_data(data=payload, next_hash=next_hash, kind=AtomKind.BYTES)
73
+ atom = Atom(data=payload, next_id=next_hash, kind=AtomKind.BYTES)
66
74
  data_atoms.append(atom)
67
75
  next_hash = atom.object_id()
68
76
 
69
77
  data_atoms.reverse()
70
78
 
71
- type_atom = Atom.from_data(
72
- data=b"radix",
73
- next_hash=next_hash,
74
- kind=AtomKind.SYMBOL,
75
- )
79
+ type_atom = Atom(data=b"trie", next_id=next_hash, kind=AtomKind.SYMBOL)
76
80
 
77
81
  atoms = data_atoms + [type_atom]
78
82
  return type_atom.object_id(), atoms
79
83
 
80
- @classmethod
81
- def from_atoms(
82
- cls,
83
- node: "Node",
84
- head_hash: bytes,
85
- ) -> "PatriciaNode":
86
- """
87
- Reconstruct a node from the atom chain rooted at `head_hash`, using the
88
- supplied `node` instance to resolve atom object ids.
89
- """
90
- if head_hash == ZERO32:
91
- raise ValueError("empty atom chain for Patricia node")
92
-
93
- def _atom_kind(atom: Optional[Atom]) -> Optional[AtomKind]:
94
- kind_value = getattr(atom, "kind", None)
95
- if isinstance(kind_value, AtomKind):
96
- return kind_value
97
- if isinstance(kind_value, int):
98
- try:
99
- return AtomKind(kind_value)
100
- except ValueError:
101
- return None
102
- return None
103
-
104
- def _require_atom(atom_hash: Optional[bytes], context: str) -> Atom:
105
- if not atom_hash or atom_hash == ZERO32:
106
- raise ValueError(f"missing {context}")
107
- atom = node.storage_get(atom_hash)
108
- if atom is None:
109
- raise ValueError(f"missing {context}")
110
- return atom
111
-
112
- type_atom = _require_atom(head_hash, "Patricia type atom")
113
- if _atom_kind(type_atom) is not AtomKind.SYMBOL:
114
- raise ValueError("malformed Patricia node (type atom kind)")
115
- if type_atom.data != b"radix":
116
- raise ValueError("not a Patricia node (type mismatch)")
117
-
118
- entries: List[bytes] = []
119
- current = type_atom.next
120
- hops = 0
121
-
122
- while current and current != ZERO32 and hops < 4:
123
- atom = node.storage_get(current)
124
- if atom is None:
125
- raise ValueError("missing atom while decoding Patricia node")
126
- if _atom_kind(atom) is not AtomKind.BYTES:
127
- raise ValueError("Patricia node detail atoms must be bytes")
128
- entries.append(atom.data)
129
- current = atom.next
130
- hops += 1
131
-
132
- if current and current != ZERO32:
133
- raise ValueError("too many fields while decoding Patricia node")
134
-
135
- if len(entries) != 4:
136
- raise ValueError("incomplete atom sequence for Patricia node")
137
-
138
- key_entry = entries[0]
139
- 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:
140
119
  raise ValueError("missing key entry while decoding Patricia node")
141
- key_len = key_entry[0]
142
- key = key_entry[1:]
143
- child_0 = entries[1] if entries[1] != ZERO32 else None
144
- child_1 = entries[2] if entries[2] != ZERO32 else None
145
- value = entries[3]
146
-
147
- 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)
148
127
 
149
- class PatriciaTrie:
128
+ class Trie:
150
129
  """
151
- A compressed-key Patricia trie supporting get and put.
130
+ A compressed-key Binary Radix Tree supporting get and put.
152
131
  """
153
132
 
154
- def __init__(
155
- self,
156
- root_hash: Optional[bytes] = None,
157
- ) -> None:
158
- """
159
- :param root_hash: optional hash of existing root node
160
- """
161
- self.nodes: Dict[bytes, PatriciaNode] = {}
162
- 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
163
142
 
164
143
  @staticmethod
165
144
  def _bit(buf: bytes, idx: int) -> bool:
@@ -189,67 +168,113 @@ class PatriciaTrie:
189
168
  return False
190
169
  return True
191
170
 
192
- def _fetch(self, storage_node: "Node", h: bytes) -> Optional[PatriciaNode]:
193
- """
194
- Fetch a node by hash, consulting the in-memory cache first and falling
195
- back to the atom storage provided by `storage_node`.
196
- """
197
- cached = self.nodes.get(h)
198
- if cached is not None:
199
- return cached
200
-
201
- if storage_node.storage_get(h) is None:
202
- return None
203
-
204
- pat_node = PatriciaNode.from_atoms(storage_node, h)
205
- self.nodes[h] = pat_node
206
- 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
207
186
 
208
187
  def get(self, storage_node: "Node", key: bytes) -> Optional[bytes]:
209
188
  """
210
189
  Return the stored value for `key`, or None if absent.
211
190
  """
212
- # Empty trie?
213
- if self.root_hash is None:
214
- 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
228
+
229
+ return None
215
230
 
216
- current = self._fetch(storage_node, self.root_hash)
217
- if current is None:
218
- return None
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 {}
219
237
 
220
- key_pos = 0 # bit offset into key
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]
221
243
 
222
- while current is not None:
223
- # 1) Check that this node's prefix matches the key here
224
- if not self._match_prefix(current.key, current.key_len, key, key_pos):
225
- return None
226
- key_pos += current.key_len
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")
227
251
 
228
- # 2) If we've consumed all bits of the search key:
229
- if key_pos == len(key) * 8:
230
- # Return value only if this node actually stores one
231
- return current.value
252
+ results: Dict[bytes, bytes] = {}
253
+ stack: List[Tuple[bytes, str]] = [(self.root_hash, "")]
254
+ visited: Set[bytes] = set()
232
255
 
233
- # 3) Decide which branch to follow via next bit
234
- try:
235
- next_bit = self._bit(key, key_pos)
236
- except IndexError:
237
- return None
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)
238
261
 
239
- child_hash = current.child_1 if next_bit else current.child_0
240
- if child_hash is None:
241
- return None # dead end
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
242
265
 
243
- # 4) Fetch child and continue descent
244
- current = self._fetch(storage_node, child_hash)
245
- if current is None:
246
- return None # dangling pointer
266
+ if pat_node.value is not None:
267
+ key_bytes = _bits_to_bytes(combined_bits)
268
+ results[key_bytes] = pat_node.value
247
269
 
248
- key_pos += 1 # consumed routing bit
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"))
249
274
 
250
- return None
275
+ return results
251
276
 
252
- def put(self, storage_node: "Node", key: bytes, value: bytes) -> None:
277
+ def put(self, storage_node: "Node", key: bytes, value: bytes) -> None:
253
278
  """
254
279
  Insert or update `key` with `value` in-place.
255
280
  """
@@ -262,64 +287,64 @@ class PatriciaTrie:
262
287
  return
263
288
 
264
289
  # S2 – traversal bookkeeping
265
- stack: List[Tuple[PatriciaNode, bytes, int]] = [] # (parent, parent_hash, dir_bit)
266
- current = self._fetch(storage_node, self.root_hash)
267
- 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
268
293
  key_pos = 0
269
294
 
270
295
  # S4 – main descent loop
271
296
  while True:
272
297
  # 4.1 – prefix mismatch? → split
273
- if not self._match_prefix(current.key, current.key_len, key, key_pos):
274
- 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)
275
300
  return
276
301
 
277
302
  # 4.2 – consume this prefix
278
- key_pos += current.key_len
303
+ key_pos += current.key_len
279
304
 
280
305
  # 4.3 – matched entire key → update value
281
306
  if key_pos == total_bits:
282
- old_hash = current.hash()
283
- current.value = value
284
- self._invalidate_hash(current)
285
- new_hash = current.hash()
286
- if new_hash != old_hash:
287
- self.nodes.pop(old_hash, None)
288
- 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
289
314
  self._bubble(stack, new_hash)
290
315
  return
291
316
 
292
317
  # 4.4 – routing bit
293
318
  next_bit = self._bit(key, key_pos)
294
- 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
295
320
 
296
321
  # 4.6 – no child → easy append leaf
297
322
  if child_hash is None:
298
- self._append_leaf(current, next_bit, key, key_pos, value, stack)
323
+ self._append_leaf(current, next_bit, key, key_pos, value, stack)
299
324
  return
300
325
 
301
326
  # 4.7 – push current node onto stack
302
- stack.append((current, current.hash(), int(next_bit)))
327
+ stack.append((current, current.hash(), int(next_bit)))
303
328
 
304
329
  # 4.8 – fetch child and continue
305
- child = self._fetch(storage_node, child_hash)
306
- if child is None:
307
- # Dangling pointer: treat as missing child
308
- parent, _, _ = stack[-1]
309
- self._append_leaf(parent, next_bit, key, key_pos, value, stack[:-1])
310
- return
311
-
312
- current = child
313
- 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
314
339
 
315
340
  def _append_leaf(
316
341
  self,
317
- parent: PatriciaNode,
342
+ parent: TrieNode,
318
343
  dir_bit: bool,
319
344
  key: bytes,
320
345
  key_pos: int,
321
346
  value: bytes,
322
- stack: List[Tuple[PatriciaNode, bytes, int]],
347
+ stack: List[Tuple[TrieNode, bytes, int]],
323
348
  ) -> None:
324
349
  tail_len = len(key) * 8 - (key_pos + 1)
325
350
  tail_bits, tail_len = self._bit_slice(key, key_pos + 1, tail_len)
@@ -342,8 +367,8 @@ class PatriciaTrie:
342
367
 
343
368
  def _split_and_insert(
344
369
  self,
345
- node: PatriciaNode,
346
- stack: List[Tuple[PatriciaNode, bytes, int]],
370
+ node: TrieNode,
371
+ stack: List[Tuple[TrieNode, bytes, int]],
347
372
  key: bytes,
348
373
  key_pos: int,
349
374
  value: bytes,
@@ -416,46 +441,45 @@ class PatriciaTrie:
416
441
  value: Optional[bytes],
417
442
  child0: Optional[bytes],
418
443
  child1: Optional[bytes],
419
- ) -> PatriciaNode:
420
- node = PatriciaNode(prefix_len, prefix_bits, value, child0, child1)
444
+ ) -> TrieNode:
445
+ node = TrieNode(prefix_len, prefix_bits, value, child0, child1)
421
446
  self.nodes[node.hash()] = node
422
447
  return node
423
448
 
424
- def _invalidate_hash(self, node: PatriciaNode) -> None:
449
+ def _invalidate_hash(self, node: TrieNode) -> None:
425
450
  """Clear cached hash so next .hash() recomputes."""
426
451
  node._hash = None # type: ignore
427
452
 
428
453
  def _bubble(
429
454
  self,
430
- stack: List[Tuple[PatriciaNode, bytes, int]],
455
+ stack: List[Tuple[TrieNode, bytes, int]],
431
456
  new_hash: bytes
432
457
  ) -> None:
433
458
  """
434
459
  Propagate updated child-hash `new_hash` up the ancestor stack,
435
460
  rebasing each parent's pointer, invalidating and re-hashing.
436
461
  """
437
- while stack:
438
- parent, old_hash, dir_bit = stack.pop()
439
-
440
- if dir_bit == 0:
441
- 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
442
467
  else:
443
468
  parent.child_1 = new_hash
444
469
 
445
470
  self._invalidate_hash(parent)
446
471
  new_hash = parent.hash()
447
472
  if new_hash != old_hash:
448
- self.nodes.pop(old_hash, None)
449
- self.nodes[new_hash] = parent
450
-
451
- self.root_hash = new_hash
452
-
453
-
454
- def _bit_slice(
455
- self,
456
- buf: bytes,
457
- start_bit: int,
458
- 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
459
483
  ) -> tuple[bytes, int]:
460
484
  """
461
485
  Extract `length` bits from `buf` starting at `start_bit` (MSB-first),
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from threading import RLock
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .. import Node
8
+
9
+
10
+ def add_atom_req(node: "Node", atom_id: bytes) -> None:
11
+ """Mark an atom request as pending."""
12
+ with node.atom_requests_lock:
13
+ node.atom_requests.add(atom_id)
14
+
15
+
16
+ def has_atom_req(node: "Node", atom_id: bytes) -> bool:
17
+ """Return True if the atom request is currently tracked."""
18
+ with node.atom_requests_lock:
19
+ return atom_id in node.atom_requests
20
+
21
+
22
+ def pop_atom_req(node: "Node", atom_id: bytes) -> bool:
23
+ """Remove the pending request if present. Returns True when removed."""
24
+ with node.atom_requests_lock:
25
+ if atom_id in node.atom_requests:
26
+ node.atom_requests.remove(atom_id)
27
+ return True
28
+ return False
astreum/storage/setup.py CHANGED
@@ -1,15 +1,22 @@
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 typing import Any
4
+
5
+
6
+ def storage_setup(node: Any, config: dict) -> None:
7
+ """Initialize hot/cold storage helpers on the node."""
8
+
9
+ node.logger.info("Setting up node storage")
10
+
11
+ node.hot_storage = {}
12
+ node.hot_storage_hits = {}
13
+ node.storage_index = {}
14
+ node.hot_storage_size = 0
15
+ node.cold_storage_size = 0
16
+
17
+ node.logger.info(
18
+ "Storage ready (hot_limit=%s bytes, cold_limit=%s bytes, cold_path=%s)",
19
+ config["hot_storage_default_limit"],
20
+ config["cold_storage_limit"],
21
+ config["cold_storage_path"] or "disabled",
22
+ )