astreum 0.2.14__tar.gz → 0.2.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of astreum might be problematic. Click here for more details.

Files changed (32) hide show
  1. {astreum-0.2.14/src/astreum.egg-info → astreum-0.2.16}/PKG-INFO +1 -1
  2. {astreum-0.2.14 → astreum-0.2.16}/pyproject.toml +1 -1
  3. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/models/merkle.py +1 -0
  4. astreum-0.2.16/src/astreum/models/patricia.py +377 -0
  5. {astreum-0.2.14 → astreum-0.2.16/src/astreum.egg-info}/PKG-INFO +1 -1
  6. {astreum-0.2.14 → astreum-0.2.16}/src/astreum.egg-info/SOURCES.txt +0 -4
  7. astreum-0.2.14/src/astreum/_node/__init__.py +0 -447
  8. astreum-0.2.14/src/astreum/_node/storage/merkle.py +0 -224
  9. astreum-0.2.14/src/astreum/_node/storage/patricia.py +0 -289
  10. astreum-0.2.14/src/astreum/models/__init__.py +0 -0
  11. astreum-0.2.14/src/astreum/models/patricia.py +0 -249
  12. {astreum-0.2.14 → astreum-0.2.16}/LICENSE +0 -0
  13. {astreum-0.2.14 → astreum-0.2.16}/README.md +0 -0
  14. {astreum-0.2.14 → astreum-0.2.16}/setup.cfg +0 -0
  15. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/__init__.py +0 -0
  16. {astreum-0.2.14/src/astreum/_node/storage → astreum-0.2.16/src/astreum/crypto}/__init__.py +0 -0
  17. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/crypto/ed25519.py +0 -0
  18. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/crypto/quadratic_form.py +0 -0
  19. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/crypto/wesolowski.py +0 -0
  20. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/crypto/x25519.py +0 -0
  21. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/format.py +0 -0
  22. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/lispeum/__init__.py +0 -0
  23. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/lispeum/parser.py +0 -0
  24. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/lispeum/tokenizer.py +0 -0
  25. {astreum-0.2.14/src/astreum/crypto → astreum-0.2.16/src/astreum/models}/__init__.py +0 -0
  26. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/models/block.py +0 -0
  27. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/models/transaction.py +0 -0
  28. {astreum-0.2.14 → astreum-0.2.16}/src/astreum/node.py +0 -0
  29. {astreum-0.2.14 → astreum-0.2.16}/src/astreum.egg-info/dependency_links.txt +0 -0
  30. {astreum-0.2.14 → astreum-0.2.16}/src/astreum.egg-info/requires.txt +0 -0
  31. {astreum-0.2.14 → astreum-0.2.16}/src/astreum.egg-info/top_level.txt +0 -0
  32. {astreum-0.2.14 → astreum-0.2.16}/tests/test_node_machine.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.14
3
+ Version: 0.2.16
4
4
  Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "astreum"
3
- version = "0.2.14"
3
+ version = "0.2.16"
4
4
  authors = [
5
5
  { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
6
  ]
@@ -217,6 +217,7 @@ class MerkleTree:
217
217
  leaf.value = value
218
218
  self._invalidate(leaf)
219
219
  new_hash = leaf.hash()
220
+ self.nodes[new_hash] = leaf
220
221
 
221
222
  # bubble updated hashes
222
223
  for parent, old_hash, went_right in reversed(stack):
@@ -0,0 +1,377 @@
1
+ import blake3
2
+ from typing import Callable, Dict, List, Optional, Tuple
3
+ from ..format import encode, decode
4
+
5
+ class PatriciaNode:
6
+ """
7
+ A node in a compressed-key Patricia trie.
8
+
9
+ Attributes:
10
+ key_len (int): Number of bits in the `key` prefix that are meaningful.
11
+ key (bytes): The MSB-aligned bit prefix (zero-padded in last byte).
12
+ value (Optional[bytes]): Stored payload (None for internal nodes).
13
+ child_0 (Optional[bytes]): Hash pointer for next-bit == 0.
14
+ child_1 (Optional[bytes]): Hash pointer for next-bit == 1.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ key_len: int,
20
+ key: bytes,
21
+ value: Optional[bytes],
22
+ child_0: Optional[bytes],
23
+ child_1: Optional[bytes]
24
+ ):
25
+ self.key_len = key_len
26
+ self.key = key
27
+ self.value = value
28
+ self.child_0 = child_0
29
+ self.child_1 = child_1
30
+ self._hash: Optional[bytes] = None
31
+
32
+ def to_bytes(self) -> bytes:
33
+ """
34
+ Serialize node fields to bytes using the shared encode format.
35
+ - key_len in a single byte.
36
+ - None pointers/values as empty bytes.
37
+ """
38
+ key_len_b = self.key_len.to_bytes(1, "big")
39
+ val_b = self.value if self.value is not None else b""
40
+ c0_b = self.child_0 if self.child_0 is not None else b""
41
+ c1_b = self.child_1 if self.child_1 is not None else b""
42
+ return encode([key_len_b, self.key, val_b, c0_b, c1_b])
43
+
44
+ @classmethod
45
+ def from_bytes(cls, blob: bytes) -> "PatriciaNode":
46
+ """
47
+ Deserialize a blob produced by to_bytes() back into a PatriciaNode.
48
+ Empty bytes are converted back to None for value/children.
49
+ """
50
+ key_len_b, key, val_b, c0_b, c1_b = decode(blob)
51
+ key_len = key_len_b[0]
52
+ value = val_b if val_b else None
53
+ child_0 = c0_b if c0_b else None
54
+ child_1 = c1_b if c1_b else None
55
+ return cls(key_len, key, value, child_0, child_1)
56
+
57
+ def hash(self) -> bytes:
58
+ """
59
+ Compute and cache the BLAKE3 hash of this node's serialized form.
60
+ """
61
+ if self._hash is None:
62
+ self._hash = blake3.blake3(self.to_bytes()).digest()
63
+ return self._hash
64
+
65
+ class PatriciaTrie:
66
+ """
67
+ A compressed-key Patricia trie supporting get and put.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ node_get: Callable[[bytes], Optional[bytes]],
73
+ root_hash: Optional[bytes] = None,
74
+ ) -> None:
75
+ """
76
+ :param node_get: function mapping node-hash -> serialized node bytes (or None)
77
+ :param root_hash: optional hash of existing root node
78
+ """
79
+ self._node_get = node_get
80
+ self.nodes: Dict[bytes, PatriciaNode] = {}
81
+ self.root_hash = root_hash
82
+
83
+ @staticmethod
84
+ def _bit(buf: bytes, idx: int) -> bool:
85
+ """
86
+ Return the bit at position `idx` (MSB-first) from `buf`.
87
+ """
88
+ byte_i, offset = divmod(idx, 8)
89
+ return ((buf[byte_i] >> (7 - offset)) & 1) == 1
90
+
91
+ @classmethod
92
+ def _match_prefix(
93
+ cls,
94
+ prefix: bytes,
95
+ prefix_len: int,
96
+ key: bytes,
97
+ key_bit_offset: int,
98
+ ) -> bool:
99
+ """
100
+ Check whether the `prefix_len` bits of `prefix` match
101
+ bits in `key` starting at `key_bit_offset`.
102
+ """
103
+ total_bits = len(key) * 8
104
+ if key_bit_offset + prefix_len > total_bits:
105
+ return False
106
+ for i in range(prefix_len):
107
+ if cls._bit(prefix, i) != cls._bit(key, key_bit_offset + i):
108
+ return False
109
+ return True
110
+
111
+ def _fetch(self, h: bytes) -> Optional[PatriciaNode]:
112
+ """
113
+ Fetch a node by hash, using in-memory cache then external node_get.
114
+ """
115
+ node = self.nodes.get(h)
116
+ if node is None:
117
+ raw = self._node_get(h)
118
+ if raw is None:
119
+ return None
120
+ node = PatriciaNode.from_bytes(raw)
121
+ self.nodes[h] = node
122
+ return node
123
+
124
+ def get(self, key: bytes) -> Optional[bytes]:
125
+ """
126
+ Return the stored value for `key`, or None if absent.
127
+ """
128
+ # Empty trie?
129
+ if self.root_hash is None:
130
+ return None
131
+
132
+ node = self._fetch(self.root_hash)
133
+ if node is None:
134
+ return None
135
+
136
+ key_pos = 0 # bit offset into key
137
+
138
+ while node is not None:
139
+ # 1) Check that this node's prefix matches the key here
140
+ if not self._match_prefix(node.key, node.key_len, key, key_pos):
141
+ return None
142
+ key_pos += node.key_len
143
+
144
+ # 2) If we've consumed all bits of the search key:
145
+ if key_pos == len(key) * 8:
146
+ # Return value only if this node actually stores one
147
+ return node.value
148
+
149
+ # 3) Decide which branch to follow via next bit
150
+ try:
151
+ next_bit = self._bit(key, key_pos)
152
+ except IndexError:
153
+ return None
154
+
155
+ child_hash = node.child_1 if next_bit else node.child_0
156
+ if child_hash is None:
157
+ return None # dead end
158
+
159
+ # 4) Fetch child and continue descent
160
+ node = self._fetch(child_hash)
161
+ if node is None:
162
+ return None # dangling pointer
163
+
164
+ key_pos += 1 # consumed routing bit
165
+
166
+ return None
167
+
168
+ def put(self, key: bytes, value: bytes) -> None:
169
+ """
170
+ Insert or update `key` with `value` in-place.
171
+ """
172
+ total_bits = len(key) * 8
173
+
174
+ # S1 – Empty trie → create root leaf
175
+ if self.root_hash is None:
176
+ leaf = self._make_node(key, total_bits, value, None, None)
177
+ self.root_hash = leaf.hash()
178
+ return
179
+
180
+ # S2 – traversal bookkeeping
181
+ stack: List[Tuple[PatriciaNode, bytes, int]] = [] # (parent, parent_hash, dir_bit)
182
+ node = self._fetch(self.root_hash)
183
+ assert node is not None
184
+ key_pos = 0
185
+
186
+ # S4 – main descent loop
187
+ while True:
188
+ # 4.1 – prefix mismatch? → split
189
+ if not self._match_prefix(node.key, node.key_len, key, key_pos):
190
+ self._split_and_insert(node, stack, key, key_pos, value)
191
+ return
192
+
193
+ # 4.2 – consume this prefix
194
+ key_pos += node.key_len
195
+
196
+ # 4.3 – matched entire key → update value
197
+ if key_pos == total_bits:
198
+ self._invalidate_hash(node)
199
+ node.value = value
200
+ new_hash = node.hash()
201
+ self.nodes[new_hash] = node
202
+ self._bubble(stack, new_hash)
203
+ return
204
+
205
+ # 4.4 – routing bit
206
+ next_bit = self._bit(key, key_pos)
207
+ child_hash = node.child_1 if next_bit else node.child_0
208
+
209
+ # 4.6 – no child → easy append leaf
210
+ if child_hash is None:
211
+ self._append_leaf(node, next_bit, key, key_pos, value, stack)
212
+ return
213
+
214
+ # 4.7 – push current node onto stack
215
+ stack.append((node, node.hash(), int(next_bit)))
216
+
217
+ # 4.8 – fetch child and continue
218
+ node = self._fetch(child_hash)
219
+ if node is None:
220
+ # Dangling pointer: treat as missing child
221
+ parent, _, _ = stack[-1]
222
+ self._append_leaf(parent, next_bit, key, key_pos, value, stack[:-1])
223
+ return
224
+
225
+ key_pos += 1 # consumed routing bit
226
+
227
+ def _append_leaf(
228
+ self,
229
+ parent: PatriciaNode,
230
+ dir_bit: bool,
231
+ key: bytes,
232
+ key_pos: int,
233
+ value: bytes,
234
+ stack: List[Tuple[PatriciaNode, bytes, int]],
235
+ ) -> None:
236
+ # key_pos points to routing bit; leaf stores the rest after that bit
237
+ tail_len = len(key) * 8 - (key_pos + 1)
238
+ tail_bits, tail_len = self._bit_slice(key, key_pos + 1, tail_len)
239
+ leaf = self._make_node(tail_bits, tail_len, value, None, None)
240
+
241
+ # attach to parent
242
+ if dir_bit:
243
+ parent.child_1 = leaf.hash()
244
+ else:
245
+ parent.child_0 = leaf.hash()
246
+
247
+ self._invalidate_hash(parent)
248
+ new_parent_hash = parent.hash()
249
+ self.nodes[new_parent_hash] = parent
250
+ self._bubble(stack, new_parent_hash)
251
+
252
+ def _split_and_insert(
253
+ self,
254
+ node: PatriciaNode,
255
+ stack: List[Tuple[PatriciaNode, bytes, int]],
256
+ key: bytes,
257
+ key_pos: int,
258
+ value: bytes,
259
+ ) -> None:
260
+ # ➊—find longest-common-prefix (lcp) as before …
261
+ max_lcp = min(node.key_len, len(key) * 8 - key_pos)
262
+ lcp = 0
263
+ while lcp < max_lcp and self._bit(node.key, lcp) == self._bit(key, key_pos + lcp):
264
+ lcp += 1
265
+
266
+ # divergence bit values (taken **before** we mutate node.key)
267
+ old_div_bit = self._bit(node.key, lcp)
268
+ new_div_bit = self._bit(key, key_pos + lcp)
269
+
270
+ # ➋—internal node that holds the common prefix
271
+ common_bits, common_len = self._bit_slice(node.key, 0, lcp)
272
+ internal = self._make_node(common_bits, common_len, None, None, None)
273
+
274
+ # ➌—trim the *existing* node’s prefix **after** the divergence bit
275
+ old_suffix_bits, old_suffix_len = self._bit_slice(
276
+ node.key,
277
+ lcp + 1, # start *after* divergence bit
278
+ node.key_len - lcp - 1 # may be zero
279
+ )
280
+ node.key = old_suffix_bits
281
+ node.key_len = old_suffix_len
282
+ self._invalidate_hash(node)
283
+ new_node_hash = node.hash()
284
+ self.nodes[new_node_hash] = node
285
+
286
+ # ➍—new leaf for the key being inserted (unchanged)
287
+ new_tail_len = len(key) * 8 - (key_pos + lcp + 1)
288
+ new_tail_bits, _ = self._bit_slice(key, key_pos + lcp + 1, new_tail_len)
289
+ leaf = self._make_node(new_tail_bits, new_tail_len, value, None, None)
290
+
291
+ # ➎—hang the two children off the internal node
292
+ if old_div_bit:
293
+ internal.child_1 = new_node_hash
294
+ internal.child_0 = leaf.hash()
295
+ else:
296
+ internal.child_0 = new_node_hash
297
+ internal.child_1 = leaf.hash()
298
+
299
+ # ➏—rehash up to the root (unchanged)
300
+ self._invalidate_hash(internal)
301
+ internal_hash = internal.hash()
302
+ self.nodes[internal_hash] = internal
303
+
304
+ if not stack:
305
+ self.root_hash = internal_hash
306
+ return
307
+
308
+ parent, _, dir_bit = stack.pop()
309
+ if dir_bit == 0:
310
+ parent.child_0 = internal_hash
311
+ else:
312
+ parent.child_1 = internal_hash
313
+ self._invalidate_hash(parent)
314
+ self._bubble(stack, parent.hash())
315
+
316
+
317
+ def _make_node(
318
+ self,
319
+ prefix_bits: bytes,
320
+ prefix_len: int,
321
+ value: Optional[bytes],
322
+ child0: Optional[bytes],
323
+ child1: Optional[bytes],
324
+ ) -> PatriciaNode:
325
+ node = PatriciaNode(prefix_len, prefix_bits, value, child0, child1)
326
+ self.nodes[node.hash()] = node
327
+ return node
328
+
329
+ def _invalidate_hash(self, node: PatriciaNode) -> None:
330
+ """Clear cached hash so next .hash() recomputes."""
331
+ node._hash = None # type: ignore
332
+
333
+ def _bubble(
334
+ self,
335
+ stack: List[Tuple[PatriciaNode, bytes, int]],
336
+ new_hash: bytes
337
+ ) -> None:
338
+ """
339
+ Propagate updated child-hash `new_hash` up the ancestor stack,
340
+ rebasing each parent's pointer, invalidating and re-hashing.
341
+ """
342
+ while stack:
343
+ parent, old_hash, dir_bit = stack.pop()
344
+ if dir_bit == 0:
345
+ parent.child_0 = new_hash
346
+ else:
347
+ parent.child_1 = new_hash
348
+ self._invalidate_hash(parent)
349
+ new_hash = parent.hash()
350
+ self.nodes[new_hash] = parent
351
+ self.root_hash = new_hash
352
+
353
+ def _bit_slice(
354
+ self,
355
+ buf: bytes,
356
+ start_bit: int,
357
+ length: int
358
+ ) -> tuple[bytes, int]:
359
+ """
360
+ Extract `length` bits from `buf` starting at `start_bit` (MSB-first),
361
+ returning (bytes, bit_len) with zero-padding.
362
+ """
363
+ if length == 0:
364
+ return b"", 0
365
+
366
+ total = int.from_bytes(buf, "big")
367
+ bits_in_buf = len(buf) * 8
368
+
369
+ # shift so slice ends at LSB
370
+ shift = bits_in_buf - (start_bit + length)
371
+ slice_int = (total >> shift) & ((1 << length) - 1)
372
+
373
+ # left-align to MSB of first byte
374
+ pad = (8 - (length % 8)) % 8
375
+ slice_int <<= pad
376
+ byte_len = (length + 7) // 8
377
+ return slice_int.to_bytes(byte_len, "big"), length
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.14
3
+ Version: 0.2.16
4
4
  Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib
@@ -9,10 +9,6 @@ src/astreum.egg-info/SOURCES.txt
9
9
  src/astreum.egg-info/dependency_links.txt
10
10
  src/astreum.egg-info/requires.txt
11
11
  src/astreum.egg-info/top_level.txt
12
- src/astreum/_node/__init__.py
13
- src/astreum/_node/storage/__init__.py
14
- src/astreum/_node/storage/merkle.py
15
- src/astreum/_node/storage/patricia.py
16
12
  src/astreum/crypto/__init__.py
17
13
  src/astreum/crypto/ed25519.py
18
14
  src/astreum/crypto/quadratic_form.py