astreum 0.1.16__py3-none-any.whl → 0.1.18__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/node/__init__.py +417 -422
- astreum/node/relay/__init__.py +3 -0
- astreum/node/storage/merkle.py +224 -734
- astreum/node/storage/patricia.py +289 -0
- astreum/node/validation/account.py +99 -874
- astreum/node/validation/block.py +30 -0
- astreum/node/validation/transaction.py +57 -1
- {astreum-0.1.16.dist-info → astreum-0.1.18.dist-info}/METADATA +1 -1
- {astreum-0.1.16.dist-info → astreum-0.1.18.dist-info}/RECORD +16 -15
- {astreum-0.1.16.dist-info → astreum-0.1.18.dist-info}/WHEEL +1 -1
- astreum/node/storage/trie.py +0 -146
- /astreum/node/validation/{block → _block}/__init__.py +0 -0
- /astreum/node/validation/{block → _block}/create.py +0 -0
- /astreum/node/validation/{block → _block}/model.py +0 -0
- /astreum/node/validation/{block → _block}/validate.py +0 -0
- {astreum-0.1.16.dist-info → astreum-0.1.18.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.1.16.dist-info → astreum-0.1.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import blake3
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from .storage import Storage
|
|
4
|
+
import astreum.utils.bytes_format as bytes_format bytes_format.decode, bytes_format.encode
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def common_prefix_length(a: bytes, b: bytes) -> int:
|
|
8
|
+
"""Return the number of common prefix bytes between a and b."""
|
|
9
|
+
i = 0
|
|
10
|
+
while i < len(a) and i < len(b) and a[i] == b[i]:
|
|
11
|
+
i += 1
|
|
12
|
+
return i
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PatriciaNode:
|
|
16
|
+
def __init__(self, key: bytes, value: Optional[bytes], children: Optional[List[bytes]] = None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize a Patricia node.
|
|
19
|
+
|
|
20
|
+
:param key: A compressed part of the key.
|
|
21
|
+
:param value: The stored value (if this node represents a complete key) or None.
|
|
22
|
+
:param children: A list of child node hashes (bytes). The children are ordered by the first
|
|
23
|
+
byte of the child's key.
|
|
24
|
+
"""
|
|
25
|
+
self.key = key
|
|
26
|
+
self.value = value
|
|
27
|
+
self.children = children if children is not None else []
|
|
28
|
+
self._hash: Optional[bytes] = None
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_bytes(cls, data: bytes) -> 'PatriciaNode':
|
|
32
|
+
"""
|
|
33
|
+
Deserialize a PatriciaNode from its byte representation.
|
|
34
|
+
|
|
35
|
+
Expected format: [key, value, children]
|
|
36
|
+
where children is a list of child node hashes (bytes).
|
|
37
|
+
"""
|
|
38
|
+
decoded = bytes_format.decode(data)
|
|
39
|
+
key, value, children = decoded
|
|
40
|
+
return cls(key, value, children)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_storage(cls, storage: Storage, hash_value: bytes) -> Optional['PatriciaNode']:
|
|
44
|
+
"""
|
|
45
|
+
Retrieve and deserialize a PatriciaNode from storage using its hash.
|
|
46
|
+
|
|
47
|
+
:param storage: The Storage instance used to retrieve the node.
|
|
48
|
+
:param hash_value: The hash key under which the node is stored.
|
|
49
|
+
:return: A PatriciaNode instance if found, otherwise None.
|
|
50
|
+
"""
|
|
51
|
+
node_bytes = storage.get(hash_value)
|
|
52
|
+
if node_bytes is None:
|
|
53
|
+
return None
|
|
54
|
+
return cls.from_bytes(node_bytes)
|
|
55
|
+
|
|
56
|
+
def to_bytes(self) -> bytes:
|
|
57
|
+
"""
|
|
58
|
+
Serialize the PatriciaNode into bytes using the Astreum format.
|
|
59
|
+
|
|
60
|
+
Structure: [key, value, children]
|
|
61
|
+
"""
|
|
62
|
+
return bytes_format.encode([self.key, self.value, self.children])
|
|
63
|
+
|
|
64
|
+
def hash(self) -> bytes:
|
|
65
|
+
"""
|
|
66
|
+
Compute (or retrieve a cached) Blake3 hash over the node's serialized bytes.
|
|
67
|
+
"""
|
|
68
|
+
if self._hash is None:
|
|
69
|
+
self._hash = blake3.blake3(self.to_bytes()).digest()
|
|
70
|
+
return self._hash
|
|
71
|
+
|
|
72
|
+
def invalidate_hash(self) -> None:
|
|
73
|
+
"""Clear the cached hash so that it is recomputed on next use."""
|
|
74
|
+
self._hash = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PatriciaTrie:
|
|
78
|
+
def __init__(self, storage: Storage, root_hash: Optional[bytes] = None):
|
|
79
|
+
"""
|
|
80
|
+
Initialize a Patricia Trie.
|
|
81
|
+
|
|
82
|
+
:param storage: A Storage instance for persisting nodes.
|
|
83
|
+
:param root_hash: Optionally, an existing root hash. If None, the trie is empty.
|
|
84
|
+
"""
|
|
85
|
+
self.storage = storage
|
|
86
|
+
self.root_hash = root_hash
|
|
87
|
+
|
|
88
|
+
def get(self, key: bytes) -> Optional[bytes]:
|
|
89
|
+
"""
|
|
90
|
+
Retrieve the value associated with the given key.
|
|
91
|
+
|
|
92
|
+
:param key: The key (as bytes) to search for.
|
|
93
|
+
:return: The stored value if found, otherwise None.
|
|
94
|
+
"""
|
|
95
|
+
if self.root_hash is None:
|
|
96
|
+
return None
|
|
97
|
+
return self._get(self.root_hash, key)
|
|
98
|
+
|
|
99
|
+
def _get(self, node_hash: bytes, key: bytes) -> Optional[bytes]:
|
|
100
|
+
node = PatriciaNode.from_storage(self.storage, node_hash)
|
|
101
|
+
if node is None:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
cp_len = common_prefix_length(key, node.key)
|
|
105
|
+
# If node.key completely matches the beginning of key...
|
|
106
|
+
if cp_len == len(node.key):
|
|
107
|
+
if cp_len == len(key):
|
|
108
|
+
return node.value
|
|
109
|
+
remainder = key[cp_len:]
|
|
110
|
+
branch = remainder[0]
|
|
111
|
+
child_hash = self._find_child(node.children, branch)
|
|
112
|
+
if child_hash is None:
|
|
113
|
+
return None
|
|
114
|
+
return self._get(child_hash, remainder)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def put(self, key: bytes, value: bytes) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Insert or update the key with the provided value.
|
|
120
|
+
|
|
121
|
+
:param key: The key (as bytes) to insert.
|
|
122
|
+
:param value: The value (as bytes) to associate with the key.
|
|
123
|
+
"""
|
|
124
|
+
if self.root_hash is None:
|
|
125
|
+
new_node = PatriciaNode(key, value, [])
|
|
126
|
+
new_hash = new_node.hash()
|
|
127
|
+
self.storage.put(new_hash, new_node.to_bytes())
|
|
128
|
+
self.root_hash = new_hash
|
|
129
|
+
else:
|
|
130
|
+
self.root_hash = self._put(self.root_hash, key, value)
|
|
131
|
+
|
|
132
|
+
def _put(self, node_hash: bytes, key: bytes, value: bytes) -> bytes:
|
|
133
|
+
"""
|
|
134
|
+
Recursive helper for inserting or updating a key.
|
|
135
|
+
|
|
136
|
+
Returns the new hash for the node that replaces the current node.
|
|
137
|
+
"""
|
|
138
|
+
node = PatriciaNode.from_storage(self.storage, node_hash)
|
|
139
|
+
if node is None:
|
|
140
|
+
# Node missing: create a new leaf.
|
|
141
|
+
new_node = PatriciaNode(key, value, [])
|
|
142
|
+
new_hash = new_node.hash()
|
|
143
|
+
self.storage.put(new_hash, new_node.to_bytes())
|
|
144
|
+
return new_hash
|
|
145
|
+
|
|
146
|
+
cp_len = common_prefix_length(key, node.key)
|
|
147
|
+
len_node_key = len(node.key)
|
|
148
|
+
len_key = len(key)
|
|
149
|
+
|
|
150
|
+
# Case 1: Exact match: update the value.
|
|
151
|
+
if cp_len == len_node_key and cp_len == len_key:
|
|
152
|
+
node.value = value
|
|
153
|
+
node.invalidate_hash()
|
|
154
|
+
new_hash = node.hash()
|
|
155
|
+
self.storage.put(new_hash, node.to_bytes())
|
|
156
|
+
self.storage.delete(node_hash)
|
|
157
|
+
return new_hash
|
|
158
|
+
|
|
159
|
+
# Case 2: Node key is a prefix of key (descend to child).
|
|
160
|
+
if cp_len == len_node_key:
|
|
161
|
+
remainder = key[cp_len:]
|
|
162
|
+
branch = remainder[0]
|
|
163
|
+
child_hash = self._find_child(node.children, branch)
|
|
164
|
+
if child_hash is not None:
|
|
165
|
+
new_child_hash = self._put(child_hash, remainder, value)
|
|
166
|
+
# Update the child pointer in the list.
|
|
167
|
+
idx = self._find_child_index(node.children, branch)
|
|
168
|
+
if idx is None:
|
|
169
|
+
raise Exception("Child not found during update.")
|
|
170
|
+
node.children[idx] = new_child_hash
|
|
171
|
+
else:
|
|
172
|
+
# Create a new leaf for the remainder.
|
|
173
|
+
new_leaf = PatriciaNode(remainder, value, [])
|
|
174
|
+
new_leaf_hash = new_leaf.hash()
|
|
175
|
+
self.storage.put(new_leaf_hash, new_leaf.to_bytes())
|
|
176
|
+
self._insert_child(node.children, new_leaf_hash)
|
|
177
|
+
node.invalidate_hash()
|
|
178
|
+
new_hash = node.hash()
|
|
179
|
+
self.storage.put(new_hash, node.to_bytes())
|
|
180
|
+
self.storage.delete(node_hash)
|
|
181
|
+
return new_hash
|
|
182
|
+
|
|
183
|
+
# Case 3: Key is a prefix of node.key (split node).
|
|
184
|
+
if cp_len == len_key and cp_len < len_node_key:
|
|
185
|
+
old_suffix = node.key[cp_len:]
|
|
186
|
+
node.key = old_suffix # update node to hold only the suffix
|
|
187
|
+
node.invalidate_hash()
|
|
188
|
+
|
|
189
|
+
# Create a new branch node with the key as prefix and value.
|
|
190
|
+
branch_node = PatriciaNode(key, value, [])
|
|
191
|
+
# The existing node becomes a child of the branch.
|
|
192
|
+
self._insert_child(branch_node.children, node.hash())
|
|
193
|
+
branch_hash = branch_node.hash()
|
|
194
|
+
self.storage.put(branch_hash, branch_node.to_bytes())
|
|
195
|
+
self.storage.put(node.hash(), node.to_bytes())
|
|
196
|
+
self.storage.delete(node_hash)
|
|
197
|
+
return branch_hash
|
|
198
|
+
|
|
199
|
+
# Case 4: Partial common prefix (split into a branch with two children).
|
|
200
|
+
if cp_len < len_node_key and cp_len < len_key:
|
|
201
|
+
common_prefix = key[:cp_len]
|
|
202
|
+
old_suffix = node.key[cp_len:]
|
|
203
|
+
new_suffix = key[cp_len:]
|
|
204
|
+
branch_node = PatriciaNode(common_prefix, None, [])
|
|
205
|
+
|
|
206
|
+
# Adjust the existing node.
|
|
207
|
+
node.key = old_suffix
|
|
208
|
+
node.invalidate_hash()
|
|
209
|
+
self._insert_child(branch_node.children, node.hash())
|
|
210
|
+
|
|
211
|
+
# Create a new leaf for the new key’s remaining portion.
|
|
212
|
+
new_leaf = PatriciaNode(new_suffix, value, [])
|
|
213
|
+
new_leaf_hash = new_leaf.hash()
|
|
214
|
+
self.storage.put(new_leaf_hash, new_leaf.to_bytes())
|
|
215
|
+
self._insert_child(branch_node.children, new_leaf_hash)
|
|
216
|
+
|
|
217
|
+
branch_hash = branch_node.hash()
|
|
218
|
+
self.storage.put(branch_hash, branch_node.to_bytes())
|
|
219
|
+
self.storage.put(node.hash(), node.to_bytes())
|
|
220
|
+
self.storage.delete(node_hash)
|
|
221
|
+
return branch_hash
|
|
222
|
+
|
|
223
|
+
raise Exception("Unhandled case in PatriciaTrie.put")
|
|
224
|
+
|
|
225
|
+
def _find_child(self, children: List[bytes], branch: int) -> Optional[bytes]:
|
|
226
|
+
"""
|
|
227
|
+
Perform a binary search over the ordered children list to find the child hash whose
|
|
228
|
+
branch (first byte of its key) equals the target branch.
|
|
229
|
+
"""
|
|
230
|
+
lo = 0
|
|
231
|
+
hi = len(children)
|
|
232
|
+
while lo < hi:
|
|
233
|
+
mid = (lo + hi) // 2
|
|
234
|
+
child_hash = children[mid]
|
|
235
|
+
child_node = PatriciaNode.from_storage(self.storage, child_hash)
|
|
236
|
+
if child_node is None or not child_node.key:
|
|
237
|
+
raise Exception("Child node missing or has empty key.")
|
|
238
|
+
child_branch = child_node.key[0]
|
|
239
|
+
if child_branch == branch:
|
|
240
|
+
return child_hash
|
|
241
|
+
elif child_branch < branch:
|
|
242
|
+
lo = mid + 1
|
|
243
|
+
else:
|
|
244
|
+
hi = mid
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def _find_child_index(self, children: List[bytes], branch: int) -> Optional[int]:
|
|
248
|
+
"""
|
|
249
|
+
Similar to _find_child but returns the index in the list.
|
|
250
|
+
"""
|
|
251
|
+
lo = 0
|
|
252
|
+
hi = len(children)
|
|
253
|
+
while lo < hi:
|
|
254
|
+
mid = (lo + hi) // 2
|
|
255
|
+
child_hash = children[mid]
|
|
256
|
+
child_node = PatriciaNode.from_storage(self.storage, child_hash)
|
|
257
|
+
if child_node is None or not child_node.key:
|
|
258
|
+
raise Exception("Child node missing or has empty key.")
|
|
259
|
+
child_branch = child_node.key[0]
|
|
260
|
+
if child_branch == branch:
|
|
261
|
+
return mid
|
|
262
|
+
elif child_branch < branch:
|
|
263
|
+
lo = mid + 1
|
|
264
|
+
else:
|
|
265
|
+
hi = mid
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
def _insert_child(self, children: List[bytes], new_child_hash: bytes) -> None:
|
|
269
|
+
"""
|
|
270
|
+
Insert a new child hash into the ordered children list.
|
|
271
|
+
"""
|
|
272
|
+
new_child_node = PatriciaNode.from_storage(self.storage, new_child_hash)
|
|
273
|
+
if new_child_node is None or not new_child_node.key:
|
|
274
|
+
raise Exception("New child node missing or has empty key.")
|
|
275
|
+
new_branch = new_child_node.key[0]
|
|
276
|
+
lo = 0
|
|
277
|
+
hi = len(children)
|
|
278
|
+
while lo < hi:
|
|
279
|
+
mid = (lo + hi) // 2
|
|
280
|
+
child_hash = children[mid]
|
|
281
|
+
child_node = PatriciaNode.from_storage(self.storage, child_hash)
|
|
282
|
+
if child_node is None or not child_node.key:
|
|
283
|
+
raise Exception("Child node missing or has empty key.")
|
|
284
|
+
child_branch = child_node.key[0]
|
|
285
|
+
if child_branch < new_branch:
|
|
286
|
+
lo = mid + 1
|
|
287
|
+
else:
|
|
288
|
+
hi = mid
|
|
289
|
+
children.insert(lo, new_child_hash)
|