astreum 0.2.41__py3-none-any.whl → 0.3.1__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.
- astreum/__init__.py +16 -7
- astreum/{_communication → communication}/__init__.py +3 -3
- astreum/communication/handlers/handshake.py +83 -0
- astreum/communication/handlers/ping.py +48 -0
- astreum/communication/handlers/storage_request.py +81 -0
- astreum/communication/models/__init__.py +0 -0
- astreum/{_communication → communication/models}/message.py +1 -0
- astreum/communication/models/peer.py +23 -0
- astreum/{_communication → communication/models}/route.py +45 -8
- astreum/{_communication → communication}/setup.py +46 -95
- astreum/communication/start.py +38 -0
- astreum/consensus/__init__.py +20 -0
- astreum/consensus/genesis.py +66 -0
- astreum/consensus/models/__init__.py +0 -0
- astreum/consensus/models/account.py +84 -0
- astreum/consensus/models/accounts.py +72 -0
- astreum/consensus/models/block.py +364 -0
- astreum/{_consensus → consensus/models}/chain.py +7 -7
- astreum/{_consensus → consensus/models}/fork.py +8 -8
- astreum/consensus/models/receipt.py +98 -0
- astreum/consensus/models/transaction.py +213 -0
- astreum/{_consensus → consensus}/setup.py +26 -11
- astreum/consensus/start.py +68 -0
- astreum/consensus/validator.py +95 -0
- astreum/{_consensus → consensus}/workers/discovery.py +20 -1
- astreum/consensus/workers/validation.py +291 -0
- astreum/{_consensus → consensus}/workers/verify.py +32 -3
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/machine/evaluations/high_evaluation.py +237 -0
- astreum/machine/evaluations/low_evaluation.py +281 -0
- astreum/machine/evaluations/script_evaluation.py +27 -0
- astreum/machine/models/__init__.py +0 -0
- astreum/machine/models/environment.py +31 -0
- astreum/machine/models/expression.py +218 -0
- astreum/{_lispeum → machine}/parser.py +26 -31
- astreum/machine/tokenizer.py +90 -0
- astreum/node.py +73 -781
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +69 -0
- astreum/storage/actions/set.py +132 -0
- astreum/storage/models/atom.py +107 -0
- astreum/{_storage/patricia.py → storage/models/trie.py} +236 -177
- astreum/storage/setup.py +44 -15
- astreum/utils/bytes.py +24 -0
- astreum/utils/integer.py +25 -0
- astreum/utils/logging.py +219 -0
- astreum-0.3.1.dist-info/METADATA +160 -0
- astreum-0.3.1.dist-info/RECORD +62 -0
- astreum/_communication/peer.py +0 -11
- astreum/_consensus/__init__.py +0 -20
- astreum/_consensus/account.py +0 -170
- astreum/_consensus/accounts.py +0 -67
- astreum/_consensus/block.py +0 -328
- astreum/_consensus/genesis.py +0 -141
- astreum/_consensus/receipt.py +0 -177
- astreum/_consensus/transaction.py +0 -192
- astreum/_consensus/workers/validation.py +0 -122
- astreum/_lispeum/__init__.py +0 -16
- astreum/_lispeum/environment.py +0 -13
- astreum/_lispeum/expression.py +0 -37
- astreum/_lispeum/high_evaluation.py +0 -177
- astreum/_lispeum/low_evaluation.py +0 -123
- astreum/_lispeum/tokenizer.py +0 -22
- astreum/_node.py +0 -58
- astreum/_storage/__init__.py +0 -5
- astreum/_storage/atom.py +0 -117
- astreum/format.py +0 -75
- astreum/models/block.py +0 -441
- astreum/models/merkle.py +0 -205
- astreum/models/patricia.py +0 -393
- astreum/storage/object.py +0 -68
- astreum-0.2.41.dist-info/METADATA +0 -146
- astreum-0.2.41.dist-info/RECORD +0 -53
- /astreum/{models → communication/handlers}/__init__.py +0 -0
- /astreum/{_communication → communication/models}/ping.py +0 -0
- /astreum/{_communication → communication}/util.py +0 -0
- /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
- /astreum/{_lispeum → machine/models}/meter.py +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from .atom import Atom, 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
|
-
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .._node import Node
|
|
7
|
+
|
|
8
|
+
class TrieNode:
|
|
10
9
|
"""
|
|
11
|
-
A node in a compressed-key
|
|
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,100 +30,115 @@ 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
|
|
43
|
-
|
|
44
|
-
def to_atoms(self) -> Tuple[bytes, List[Atom]]:
|
|
33
|
+
self._hash: Optional[bytes] = None
|
|
34
|
+
|
|
35
|
+
def hash(self) -> bytes:
|
|
45
36
|
"""
|
|
46
|
-
|
|
47
|
-
traversal-friendly order: key length prefix (single byte) + key bytes,
|
|
48
|
-
child_0 hash, child_1 hash, and value bytes. Returns the head atom hash
|
|
49
|
-
and the atom list.
|
|
37
|
+
Compute and cache the canonical hash for this node (its type-atom id).
|
|
50
38
|
"""
|
|
51
|
-
if self.
|
|
52
|
-
|
|
39
|
+
if self._hash is None:
|
|
40
|
+
head_hash, _ = self._render_atoms()
|
|
41
|
+
self._hash = head_hash
|
|
42
|
+
return self._hash
|
|
53
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
|
|
54
|
+
|
|
55
|
+
def _render_atoms(self) -> Tuple[bytes, List[Atom]]:
|
|
56
|
+
"""
|
|
57
|
+
Materialise this node with the canonical atom layout used by the
|
|
58
|
+
storage layer: a leading SYMBOL atom with payload ``b"trie"`` whose
|
|
59
|
+
``next`` pointer links to four BYTES atoms containing, in order:
|
|
60
|
+
key (len byte + key payload), child_0 hash, child_1 hash, value bytes.
|
|
61
|
+
Returns the top atom hash and the emitted atoms.
|
|
62
|
+
"""
|
|
54
63
|
entries: List[bytes] = [
|
|
55
|
-
|
|
64
|
+
self.key_len.to_bytes(2, "big", signed=False) + self.key,
|
|
56
65
|
self.child_0 or ZERO32,
|
|
57
66
|
self.child_1 or ZERO32,
|
|
58
67
|
self.value or b"",
|
|
59
68
|
]
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
data_atoms: List[Atom] = []
|
|
62
71
|
next_hash = ZERO32
|
|
63
72
|
for payload in reversed(entries):
|
|
64
|
-
atom = Atom
|
|
65
|
-
|
|
73
|
+
atom = Atom(data=payload, next_id=next_hash, kind=AtomKind.BYTES)
|
|
74
|
+
data_atoms.append(atom)
|
|
66
75
|
next_hash = atom.object_id()
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
atoms.reverse()
|
|
70
|
-
return head, atoms
|
|
71
|
-
|
|
72
|
-
@classmethod
|
|
73
|
-
def from_atoms(
|
|
74
|
-
cls,
|
|
75
|
-
node: "Node",
|
|
76
|
-
head_hash: bytes,
|
|
77
|
-
) -> "PatriciaNode":
|
|
78
|
-
"""
|
|
79
|
-
Reconstruct a node from the atom chain rooted at `head_hash`, using the
|
|
80
|
-
supplied `node` instance to resolve atom object ids.
|
|
81
|
-
"""
|
|
82
|
-
if head_hash == ZERO32:
|
|
83
|
-
raise ValueError("empty atom chain for Patricia node")
|
|
84
|
-
|
|
85
|
-
entries: List[bytes] = []
|
|
86
|
-
current = head_hash
|
|
87
|
-
hops = 0
|
|
77
|
+
data_atoms.reverse()
|
|
88
78
|
|
|
89
|
-
|
|
90
|
-
atom = node._local_get(current)
|
|
91
|
-
if atom is None:
|
|
92
|
-
raise ValueError("missing atom while decoding Patricia node")
|
|
93
|
-
entries.append(atom.data)
|
|
94
|
-
current = atom.next
|
|
95
|
-
hops += 1
|
|
79
|
+
type_atom = Atom(data=b"trie", next_id=next_hash, kind=AtomKind.SYMBOL)
|
|
96
80
|
|
|
97
|
-
|
|
98
|
-
|
|
81
|
+
atoms = data_atoms + [type_atom]
|
|
82
|
+
return type_atom.object_id(), atoms
|
|
99
83
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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:
|
|
105
119
|
raise ValueError("missing key entry while decoding Patricia node")
|
|
106
|
-
key_len = key_entry[
|
|
107
|
-
key = key_entry[
|
|
108
|
-
child_0 =
|
|
109
|
-
child_1 =
|
|
110
|
-
value =
|
|
111
|
-
|
|
112
|
-
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)
|
|
113
127
|
|
|
114
|
-
class
|
|
128
|
+
class Trie:
|
|
115
129
|
"""
|
|
116
|
-
A compressed-key
|
|
130
|
+
A compressed-key Binary Radix Tree supporting get and put.
|
|
117
131
|
"""
|
|
118
132
|
|
|
119
|
-
def __init__(
|
|
120
|
-
self,
|
|
121
|
-
root_hash: Optional[bytes] = None,
|
|
122
|
-
) -> None:
|
|
123
|
-
"""
|
|
124
|
-
:param root_hash: optional hash of existing root node
|
|
125
|
-
"""
|
|
126
|
-
self.nodes: Dict[bytes,
|
|
127
|
-
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
|
|
128
142
|
|
|
129
143
|
@staticmethod
|
|
130
144
|
def _bit(buf: bytes, idx: int) -> bool:
|
|
@@ -154,67 +168,113 @@ class PatriciaTrie:
|
|
|
154
168
|
return False
|
|
155
169
|
return True
|
|
156
170
|
|
|
157
|
-
def _fetch(self, storage_node: "Node", h: bytes) -> Optional[
|
|
158
|
-
"""
|
|
159
|
-
Fetch a node by hash, consulting the in-memory cache first and falling
|
|
160
|
-
back to the atom storage provided by `storage_node`.
|
|
161
|
-
"""
|
|
162
|
-
cached = self.nodes.get(h)
|
|
163
|
-
if cached is not None:
|
|
164
|
-
return cached
|
|
165
|
-
|
|
166
|
-
if storage_node.
|
|
167
|
-
return None
|
|
168
|
-
|
|
169
|
-
pat_node =
|
|
170
|
-
self.nodes[h] = pat_node
|
|
171
|
-
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
|
|
172
186
|
|
|
173
187
|
def get(self, storage_node: "Node", key: bytes) -> Optional[bytes]:
|
|
174
188
|
"""
|
|
175
189
|
Return the stored value for `key`, or None if absent.
|
|
176
190
|
"""
|
|
177
|
-
# Empty trie?
|
|
178
|
-
if self.root_hash is None:
|
|
179
|
-
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
|
|
180
228
|
|
|
181
|
-
|
|
182
|
-
if current is None:
|
|
183
|
-
return None
|
|
229
|
+
return None
|
|
184
230
|
|
|
185
|
-
|
|
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 {}
|
|
186
237
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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]
|
|
192
243
|
|
|
193
|
-
|
|
194
|
-
if
|
|
195
|
-
|
|
196
|
-
|
|
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")
|
|
197
251
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
except IndexError:
|
|
202
|
-
return None
|
|
252
|
+
results: Dict[bytes, bytes] = {}
|
|
253
|
+
stack: List[Tuple[bytes, str]] = [(self.root_hash, "")]
|
|
254
|
+
visited: Set[bytes] = set()
|
|
203
255
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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)
|
|
207
261
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return None # dangling pointer
|
|
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
|
|
212
265
|
|
|
213
|
-
|
|
266
|
+
if pat_node.value is not None:
|
|
267
|
+
key_bytes = _bits_to_bytes(combined_bits)
|
|
268
|
+
results[key_bytes] = pat_node.value
|
|
214
269
|
|
|
215
|
-
|
|
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"))
|
|
274
|
+
|
|
275
|
+
return results
|
|
216
276
|
|
|
217
|
-
def put(self, storage_node: "Node", key: bytes, value: bytes) -> None:
|
|
277
|
+
def put(self, storage_node: "Node", key: bytes, value: bytes) -> None:
|
|
218
278
|
"""
|
|
219
279
|
Insert or update `key` with `value` in-place.
|
|
220
280
|
"""
|
|
@@ -227,64 +287,64 @@ class PatriciaTrie:
|
|
|
227
287
|
return
|
|
228
288
|
|
|
229
289
|
# S2 – traversal bookkeeping
|
|
230
|
-
stack: List[Tuple[
|
|
231
|
-
current = self._fetch(storage_node, self.root_hash)
|
|
232
|
-
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
|
|
233
293
|
key_pos = 0
|
|
234
294
|
|
|
235
295
|
# S4 – main descent loop
|
|
236
296
|
while True:
|
|
237
297
|
# 4.1 – prefix mismatch? → split
|
|
238
|
-
if not self._match_prefix(current.key, current.key_len, key, key_pos):
|
|
239
|
-
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)
|
|
240
300
|
return
|
|
241
301
|
|
|
242
302
|
# 4.2 – consume this prefix
|
|
243
|
-
key_pos += current.key_len
|
|
303
|
+
key_pos += current.key_len
|
|
244
304
|
|
|
245
305
|
# 4.3 – matched entire key → update value
|
|
246
306
|
if key_pos == total_bits:
|
|
247
|
-
old_hash = current.hash()
|
|
248
|
-
current.value = value
|
|
249
|
-
self._invalidate_hash(current)
|
|
250
|
-
new_hash = current.hash()
|
|
251
|
-
if new_hash != old_hash:
|
|
252
|
-
self.nodes.pop(old_hash, None)
|
|
253
|
-
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
|
|
254
314
|
self._bubble(stack, new_hash)
|
|
255
315
|
return
|
|
256
316
|
|
|
257
317
|
# 4.4 – routing bit
|
|
258
318
|
next_bit = self._bit(key, key_pos)
|
|
259
|
-
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
|
|
260
320
|
|
|
261
321
|
# 4.6 – no child → easy append leaf
|
|
262
322
|
if child_hash is None:
|
|
263
|
-
self._append_leaf(current, next_bit, key, key_pos, value, stack)
|
|
323
|
+
self._append_leaf(current, next_bit, key, key_pos, value, stack)
|
|
264
324
|
return
|
|
265
325
|
|
|
266
326
|
# 4.7 – push current node onto stack
|
|
267
|
-
stack.append((current, current.hash(), int(next_bit)))
|
|
327
|
+
stack.append((current, current.hash(), int(next_bit)))
|
|
268
328
|
|
|
269
329
|
# 4.8 – fetch child and continue
|
|
270
|
-
child = self._fetch(storage_node, child_hash)
|
|
271
|
-
if child is None:
|
|
272
|
-
# Dangling pointer: treat as missing child
|
|
273
|
-
parent, _, _ = stack[-1]
|
|
274
|
-
self._append_leaf(parent, next_bit, key, key_pos, value, stack[:-1])
|
|
275
|
-
return
|
|
276
|
-
|
|
277
|
-
current = child
|
|
278
|
-
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
|
|
279
339
|
|
|
280
340
|
def _append_leaf(
|
|
281
341
|
self,
|
|
282
|
-
parent:
|
|
342
|
+
parent: TrieNode,
|
|
283
343
|
dir_bit: bool,
|
|
284
344
|
key: bytes,
|
|
285
345
|
key_pos: int,
|
|
286
346
|
value: bytes,
|
|
287
|
-
stack: List[Tuple[
|
|
347
|
+
stack: List[Tuple[TrieNode, bytes, int]],
|
|
288
348
|
) -> None:
|
|
289
349
|
tail_len = len(key) * 8 - (key_pos + 1)
|
|
290
350
|
tail_bits, tail_len = self._bit_slice(key, key_pos + 1, tail_len)
|
|
@@ -307,8 +367,8 @@ class PatriciaTrie:
|
|
|
307
367
|
|
|
308
368
|
def _split_and_insert(
|
|
309
369
|
self,
|
|
310
|
-
node:
|
|
311
|
-
stack: List[Tuple[
|
|
370
|
+
node: TrieNode,
|
|
371
|
+
stack: List[Tuple[TrieNode, bytes, int]],
|
|
312
372
|
key: bytes,
|
|
313
373
|
key_pos: int,
|
|
314
374
|
value: bytes,
|
|
@@ -381,46 +441,45 @@ class PatriciaTrie:
|
|
|
381
441
|
value: Optional[bytes],
|
|
382
442
|
child0: Optional[bytes],
|
|
383
443
|
child1: Optional[bytes],
|
|
384
|
-
) ->
|
|
385
|
-
node =
|
|
444
|
+
) -> TrieNode:
|
|
445
|
+
node = TrieNode(prefix_len, prefix_bits, value, child0, child1)
|
|
386
446
|
self.nodes[node.hash()] = node
|
|
387
447
|
return node
|
|
388
448
|
|
|
389
|
-
def _invalidate_hash(self, node:
|
|
449
|
+
def _invalidate_hash(self, node: TrieNode) -> None:
|
|
390
450
|
"""Clear cached hash so next .hash() recomputes."""
|
|
391
451
|
node._hash = None # type: ignore
|
|
392
452
|
|
|
393
453
|
def _bubble(
|
|
394
454
|
self,
|
|
395
|
-
stack: List[Tuple[
|
|
455
|
+
stack: List[Tuple[TrieNode, bytes, int]],
|
|
396
456
|
new_hash: bytes
|
|
397
457
|
) -> None:
|
|
398
458
|
"""
|
|
399
459
|
Propagate updated child-hash `new_hash` up the ancestor stack,
|
|
400
460
|
rebasing each parent's pointer, invalidating and re-hashing.
|
|
401
461
|
"""
|
|
402
|
-
while stack:
|
|
403
|
-
parent, old_hash, dir_bit = stack.pop()
|
|
404
|
-
|
|
405
|
-
if dir_bit == 0:
|
|
406
|
-
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
|
|
407
467
|
else:
|
|
408
468
|
parent.child_1 = new_hash
|
|
409
469
|
|
|
410
470
|
self._invalidate_hash(parent)
|
|
411
471
|
new_hash = parent.hash()
|
|
412
472
|
if new_hash != old_hash:
|
|
413
|
-
self.nodes.pop(old_hash, None)
|
|
414
|
-
self.nodes[new_hash] = parent
|
|
415
|
-
|
|
416
|
-
self.root_hash = new_hash
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
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
|
|
424
483
|
) -> tuple[bytes, int]:
|
|
425
484
|
"""
|
|
426
485
|
Extract `length` bits from `buf` starting at `start_bit` (MSB-first),
|
astreum/storage/setup.py
CHANGED
|
@@ -1,15 +1,44 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
storage_index
|
|
15
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def storage_setup(node: Any, config: dict) -> None:
|
|
8
|
+
"""Initialize hot/cold storage helpers on the node."""
|
|
9
|
+
|
|
10
|
+
node.logger.info("Setting up node storage")
|
|
11
|
+
|
|
12
|
+
node.hot_storage = {}
|
|
13
|
+
node.hot_storage_hits = {}
|
|
14
|
+
node.storage_index = {}
|
|
15
|
+
node.hot_storage_size = 0
|
|
16
|
+
hot_storage_default_limit = 1 << 30 # 1 GiB
|
|
17
|
+
hot_storage_limit_value = config.get("hot_storage_limit", hot_storage_default_limit)
|
|
18
|
+
try:
|
|
19
|
+
node.hot_storage_limit = int(hot_storage_limit_value)
|
|
20
|
+
except (TypeError, ValueError):
|
|
21
|
+
node.hot_storage_limit = hot_storage_default_limit
|
|
22
|
+
|
|
23
|
+
node.cold_storage_size = 0
|
|
24
|
+
cold_storage_default_limit = 10 << 30 # 10 GiB
|
|
25
|
+
cold_storage_limit_value = config.get("cold_storage_limit", cold_storage_default_limit)
|
|
26
|
+
try:
|
|
27
|
+
node.cold_storage_limit = int(cold_storage_limit_value)
|
|
28
|
+
except (TypeError, ValueError):
|
|
29
|
+
node.cold_storage_limit = cold_storage_default_limit
|
|
30
|
+
|
|
31
|
+
cold_storage_path = config.get("cold_storage_path")
|
|
32
|
+
if cold_storage_path:
|
|
33
|
+
try:
|
|
34
|
+
Path(cold_storage_path).mkdir(parents=True, exist_ok=True)
|
|
35
|
+
except OSError:
|
|
36
|
+
cold_storage_path = None
|
|
37
|
+
node.cold_storage_path = cold_storage_path
|
|
38
|
+
|
|
39
|
+
node.logger.info(
|
|
40
|
+
"Storage ready (hot_limit=%s bytes, cold_limit=%s bytes, cold_path=%s)",
|
|
41
|
+
node.hot_storage_limit,
|
|
42
|
+
node.cold_storage_limit,
|
|
43
|
+
node.cold_storage_path or "disabled",
|
|
44
|
+
)
|
astreum/utils/bytes.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def hex_to_bytes(value: str, *, expected_length: Optional[int] = None) -> bytes:
|
|
5
|
+
"""Convert a 0x-prefixed hex string into raw bytes."""
|
|
6
|
+
if not isinstance(value, str):
|
|
7
|
+
raise TypeError("hex value must be provided as a string")
|
|
8
|
+
|
|
9
|
+
if not value.startswith(("0x", "0X")):
|
|
10
|
+
raise ValueError("hex value must start with '0x'")
|
|
11
|
+
|
|
12
|
+
hex_digits = value[2:]
|
|
13
|
+
if len(hex_digits) % 2:
|
|
14
|
+
raise ValueError("hex value must have an even number of digits")
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
result = bytes.fromhex(hex_digits)
|
|
18
|
+
except ValueError as exc:
|
|
19
|
+
raise ValueError("hex value contains non-hexadecimal characters") from exc
|
|
20
|
+
|
|
21
|
+
if expected_length is not None and len(result) != expected_length:
|
|
22
|
+
raise ValueError(f"hex value must decode to exactly {expected_length} bytes")
|
|
23
|
+
|
|
24
|
+
return result
|
astreum/utils/integer.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Union
|
|
4
|
+
|
|
5
|
+
ByteLike = Union[bytes, bytearray, memoryview]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def int_to_bytes(value: Optional[int]) -> bytes:
|
|
9
|
+
"""Convert an integer to a little-endian byte string with minimal length."""
|
|
10
|
+
if value is None:
|
|
11
|
+
return b""
|
|
12
|
+
value = int(value)
|
|
13
|
+
if value == 0:
|
|
14
|
+
return b"\x00"
|
|
15
|
+
length = (value.bit_length() + 7) // 8
|
|
16
|
+
return value.to_bytes(length, "little", signed=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def bytes_to_int(data: Optional[ByteLike]) -> int:
|
|
20
|
+
"""Convert a little-endian byte string to an integer."""
|
|
21
|
+
if not data:
|
|
22
|
+
return 0
|
|
23
|
+
if isinstance(data, memoryview):
|
|
24
|
+
data = data.tobytes()
|
|
25
|
+
return int.from_bytes(bytes(data), "little", signed=False)
|