astreum 0.2.10__py3-none-any.whl → 0.2.12__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.
Potentially problematic release.
This version of astreum might be problematic. Click here for more details.
- astreum/utils/__init__.py +0 -0
- astreum/utils/patricia.py +249 -0
- {astreum-0.2.10.dist-info → astreum-0.2.12.dist-info}/METADATA +1 -1
- {astreum-0.2.10.dist-info → astreum-0.2.12.dist-info}/RECORD +7 -5
- {astreum-0.2.10.dist-info → astreum-0.2.12.dist-info}/WHEEL +0 -0
- {astreum-0.2.10.dist-info → astreum-0.2.12.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.10.dist-info → astreum-0.2.12.dist-info}/top_level.txt +0 -0
|
File without changes
|
|
@@ -0,0 +1,249 @@
|
|
|
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
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
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
|
|
@@ -13,8 +13,10 @@ astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
|
|
|
13
13
|
astreum/lispeum/__init__.py,sha256=K-NDzIjtIsXzC9X7lnYvlvIaVxjFcY7WNsgLIE3DH3U,58
|
|
14
14
|
astreum/lispeum/parser.py,sha256=jQRzZYvBuSg8t_bxsbt1-WcHaR_LPveHNX7Qlxhaw-M,1165
|
|
15
15
|
astreum/lispeum/tokenizer.py,sha256=J-I7MEd0r2ZoVqxvRPlu-Afe2ZdM0tKXXhf1R4SxYTo,1429
|
|
16
|
-
astreum
|
|
17
|
-
astreum
|
|
18
|
-
astreum-0.2.
|
|
19
|
-
astreum-0.2.
|
|
20
|
-
astreum-0.2.
|
|
16
|
+
astreum/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
astreum/utils/patricia.py,sha256=D7UVU4b6Yvn2_McI35VoMEbpqwR8OmZon5LGoUSRADo,8913
|
|
18
|
+
astreum-0.2.12.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
|
|
19
|
+
astreum-0.2.12.dist-info/METADATA,sha256=lpoXNcNED3phD7Rb3PuwLYjy6cpoQFcsR9exdUAs42Q,5454
|
|
20
|
+
astreum-0.2.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
astreum-0.2.12.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
|
|
22
|
+
astreum-0.2.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|