astreum 0.2.15__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 (28) hide show
  1. {astreum-0.2.15/src/astreum.egg-info → astreum-0.2.16}/PKG-INFO +1 -1
  2. {astreum-0.2.15 → astreum-0.2.16}/pyproject.toml +1 -1
  3. {astreum-0.2.15 → 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.15 → astreum-0.2.16/src/astreum.egg-info}/PKG-INFO +1 -1
  6. astreum-0.2.15/src/astreum/models/patricia.py +0 -249
  7. {astreum-0.2.15 → astreum-0.2.16}/LICENSE +0 -0
  8. {astreum-0.2.15 → astreum-0.2.16}/README.md +0 -0
  9. {astreum-0.2.15 → astreum-0.2.16}/setup.cfg +0 -0
  10. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/__init__.py +0 -0
  11. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/crypto/__init__.py +0 -0
  12. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/crypto/ed25519.py +0 -0
  13. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/crypto/quadratic_form.py +0 -0
  14. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/crypto/wesolowski.py +0 -0
  15. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/crypto/x25519.py +0 -0
  16. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/format.py +0 -0
  17. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/lispeum/__init__.py +0 -0
  18. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/lispeum/parser.py +0 -0
  19. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/lispeum/tokenizer.py +0 -0
  20. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/models/__init__.py +0 -0
  21. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/models/block.py +0 -0
  22. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/models/transaction.py +0 -0
  23. {astreum-0.2.15 → astreum-0.2.16}/src/astreum/node.py +0 -0
  24. {astreum-0.2.15 → astreum-0.2.16}/src/astreum.egg-info/SOURCES.txt +0 -0
  25. {astreum-0.2.15 → astreum-0.2.16}/src/astreum.egg-info/dependency_links.txt +0 -0
  26. {astreum-0.2.15 → astreum-0.2.16}/src/astreum.egg-info/requires.txt +0 -0
  27. {astreum-0.2.15 → astreum-0.2.16}/src/astreum.egg-info/top_level.txt +0 -0
  28. {astreum-0.2.15 → 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.15
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.15"
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.15
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,249 +0,0 @@
1
- import blake3
2
- from typing import Callable, Dict, List, Optional, Tuple
3
- from astreum import format
4
-
5
- class PatriciaNode:
6
- def __init__(
7
- self,
8
- key_len: int,
9
- key: bytes,
10
- value: Optional[bytes],
11
- child_0: Optional[bytes],
12
- child_1: Optional[bytes]
13
- ):
14
- self.key_len = key_len
15
- self.key = key
16
- self.value = value
17
- self.child_0 = child_0
18
- self.child_1 = child_1
19
- self._hash: bytes | None = None
20
-
21
- def to_bytes(self) -> bytes:
22
- return format.encode([self.key_len, self.key, self.value, self.child_0, self.child_1])
23
-
24
- @classmethod
25
- def from_bytes(cls, blob: bytes) -> "PatriciaNode":
26
- key_len, key, value, child_0, child_1 = format.decode(blob)
27
- return cls(key_len, key, value, child_0, child_1)
28
-
29
- def hash(self) -> bytes:
30
- if self._hash is None:
31
- self._hash = blake3.blake3(self.to_bytes()).digest()
32
- return self._hash
33
-
34
- class PatriciaTrie:
35
- def __init__(
36
- self,
37
- node_get: Callable[[bytes], Optional[bytes]],
38
- root_hash: Optional[bytes] = None,
39
- ) -> None:
40
- self._node_get = node_get
41
- self.nodes: Dict[bytes, PatriciaNode] = {}
42
- self.root_hash: Optional[bytes] = root_hash
43
-
44
- @staticmethod
45
- def _bit(buf: bytes, idx: int) -> bool:
46
- byte_i, offset = divmod(idx, 8)
47
- return ((buf[byte_i] >> (7 - offset)) & 1) == 1
48
-
49
- @classmethod
50
- def _match_prefix(
51
- cls,
52
- prefix: bytes,
53
- prefix_len: int,
54
- key: bytes,
55
- key_bit_offset: int,
56
- ) -> bool:
57
- if key_bit_offset + prefix_len > len(key) * 8:
58
- return False
59
-
60
- for i in range(prefix_len):
61
- if cls._bit(prefix, i) != cls._bit(key, key_bit_offset + i):
62
- return False
63
- return True
64
-
65
- def _fetch(self, h: bytes) -> Optional[PatriciaNode]:
66
- node = self.nodes.get(h)
67
- if node is None:
68
- raw = self._node_get(h)
69
- if raw is None:
70
- return None
71
- node = PatriciaNode.from_bytes(raw)
72
- self.nodes[h] = node
73
- return node
74
-
75
- def get(self, key: bytes) -> Optional["PatriciaNode"]:
76
- """Return the node that stores *key*, or ``None`` if absent."""
77
- if self.root_hash is None:
78
- return None
79
-
80
- node = self._fetch(self.root_hash)
81
- if node is None:
82
- return None
83
-
84
- key_pos = 0
85
-
86
- while node is not None:
87
- # 1️⃣ Verify that this node's (possibly sub‑byte) prefix matches.
88
- if not self._match_prefix(node.key, node.key_len, key, key_pos):
89
- return None
90
- key_pos += node.key_len
91
-
92
- # 2️⃣ If every bit of *key* has been matched, success only if the
93
- # node actually stores a value.
94
- if key_pos == len(key) * 8:
95
- return node if node.value is not None else None
96
-
97
- # 3️⃣ Decide which branch to follow using the next bit of *key*.
98
- try:
99
- next_bit = self._bit(key, key_pos)
100
- except IndexError: # key ended prematurely
101
- return None
102
-
103
- child_hash = node.child_1 if next_bit else node.child_0
104
- if child_hash is None: # dead end – key not present
105
- return None
106
-
107
- # 4️⃣ Fetch the child node via unified helper.
108
- node = self._fetch(child_hash)
109
- if node is None: # dangling pointer
110
- return None
111
-
112
- key_pos += 1 # we just consumed one routing bit
113
-
114
- return None
115
-
116
- def put(self, key: bytes, value: bytes) -> None:
117
- """Insert or update ``key`` with ``value`` in‑place."""
118
- total_bits = len(key) * 8
119
-
120
- # S1 – Empty trie → create root leaf
121
- if self.root_hash is None:
122
- leaf = self._make_node(key, total_bits, value, None, None)
123
- self.root_hash = leaf.hash()
124
- return
125
-
126
- # S2 – traversal bookkeeping
127
- stack: List[Tuple[PatriciaNode, bytes, int]] = [] # (parent, parent_hash, dir_bit)
128
- node = self._fetch(self.root_hash)
129
- assert node is not None # root must exist now
130
- key_pos = 0
131
-
132
- # S4 – main descent loop
133
- while True:
134
- # 4.1 – prefix mismatch? → split
135
- if not self._match_prefix(node.key, node.key_len, key, key_pos):
136
- self._split_and_insert(node, stack, key, key_pos, value)
137
- return
138
-
139
- # 4.2 – consume this prefix
140
- key_pos += node.key_len
141
-
142
- # 4.3 – matched entire key → update value
143
- if key_pos == total_bits:
144
- self._invalidate_hash(node)
145
- node.value = value
146
- new_hash = node.hash()
147
- self._bubble(stack, new_hash)
148
- return
149
-
150
- # 4.4 – routing bit
151
- next_bit = self._bit(key, key_pos)
152
- child_hash = node.child_1 if next_bit else node.child_0
153
-
154
- # 4.6 – no child → easy append leaf
155
- if child_hash is None:
156
- self._append_leaf(node, next_bit, key, key_pos, value, stack)
157
- return
158
-
159
- # 4.7 – push current node onto stack
160
- stack.append((node, node.hash(), int(next_bit)))
161
-
162
- # 4.8 – fetch child and continue
163
- node = self._fetch(child_hash)
164
- if node is None:
165
- # Dangling pointer – treat as append missing leaf
166
- self._append_leaf(stack[-1][0], next_bit, key, key_pos, value, stack[:-1])
167
- return
168
- key_pos += 1 # consumed routing bit
169
-
170
- def _append_leaf(
171
- self,
172
- parent: PatriciaNode,
173
- dir_bit: bool,
174
- key: bytes,
175
- key_pos: int,
176
- value: bytes,
177
- stack: List[Tuple[PatriciaNode, bytes, int]],
178
- ) -> None:
179
- # key_pos points to routing bit; leaf stores the *rest* after that bit
180
- tail_len = len(key) * 8 - (key_pos + 1)
181
- tail_bits, tail_len = self._bit_slice(key, key_pos + 1, tail_len)
182
- leaf = self._make_node(tail_bits, tail_len, value, None, None)
183
-
184
- # attach
185
- if dir_bit:
186
- parent.child_1 = leaf.hash()
187
- else:
188
- parent.child_0 = leaf.hash()
189
- self._invalidate_hash(parent)
190
- new_parent_hash = parent.hash()
191
- self._bubble(stack, new_parent_hash)
192
-
193
- def _split_and_insert(
194
- self,
195
- node: PatriciaNode,
196
- stack: List[Tuple[PatriciaNode, bytes, int]],
197
- key: bytes,
198
- key_pos: int,
199
- value: bytes,
200
- ) -> None:
201
- """Split ``node`` at first divergent bit and insert new leaf for *key*."""
202
- # Compute LCP between node.key and remaining key bits
203
- max_lcp = min(node.key_len, len(key) * 8 - key_pos)
204
- lcp = 0
205
- while lcp < max_lcp and self._bit(node.key, lcp) == self._bit(key, key_pos + lcp):
206
- lcp += 1
207
-
208
- # Common prefix bits → new internal node
209
- common_bits, common_len = self._bit_slice(node.key, 0, lcp)
210
- internal = self._make_node(common_bits, common_len, None, None, None)
211
-
212
- # Trim old node prefix
213
- old_suffix_bits, old_suffix_len = self._bit_slice(node.key, lcp, node.key_len - lcp)
214
- node.key = old_suffix_bits
215
- node.key_len = old_suffix_len
216
- self._invalidate_hash(node) # will be re‑hashed when attached
217
- old_div_bit = self._bit(node.key, 0) if old_suffix_len > 0 else False
218
-
219
- # New key leaf
220
- new_key_tail_len = len(key) * 8 - (key_pos + lcp + 1)
221
- new_tail_bits, new_tail_len = self._bit_slice(key, key_pos + lcp + 1, new_key_tail_len)
222
- leaf = self._make_node(new_tail_bits, new_tail_len, value, None, None)
223
- new_div_bit = self._bit(key, key_pos + lcp)
224
-
225
- # Attach children to internal
226
- if old_div_bit:
227
- internal.child_1 = node.hash()
228
- internal.child_0 = leaf.hash() if not new_div_bit else internal.child_0
229
- else:
230
- internal.child_0 = node.hash()
231
- internal.child_1 = leaf.hash() if new_div_bit else internal.child_1
232
- self._invalidate_hash(internal)
233
- internal_hash = internal.hash()
234
-
235
- # Rewire parent link or set as root
236
- if not stack:
237
- self.root_hash = internal_hash
238
- return
239
-
240
- parent, parent_old_hash, dir_bit = stack.pop()
241
- if dir_bit == 0:
242
- parent.child_0 = internal_hash
243
- else:
244
- parent.child_1 = internal_hash
245
- self._invalidate_hash(parent)
246
- parent_new_hash = parent.hash()
247
- self._bubble(stack, parent_new_hash)
248
-
249
-
File without changes
File without changes
File without changes
File without changes
File without changes