astreum 0.2.61__py3-none-any.whl → 0.3.9__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 +89 -0
- astreum/communication/handlers/object_request.py +176 -0
- astreum/communication/handlers/object_response.py +115 -0
- astreum/communication/handlers/ping.py +34 -0
- astreum/communication/handlers/route_request.py +76 -0
- astreum/communication/handlers/route_response.py +53 -0
- astreum/communication/models/__init__.py +0 -0
- astreum/communication/models/message.py +124 -0
- astreum/communication/models/peer.py +51 -0
- astreum/{_communication → communication/models}/route.py +7 -12
- astreum/communication/processors/__init__.py +0 -0
- astreum/communication/processors/incoming.py +98 -0
- astreum/communication/processors/outgoing.py +20 -0
- astreum/communication/setup.py +166 -0
- astreum/communication/start.py +37 -0
- astreum/{_communication → communication}/util.py +7 -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 → consensus/models}/transaction.py +76 -78
- astreum/{_consensus → consensus}/setup.py +18 -50
- astreum/consensus/start.py +67 -0
- astreum/consensus/validator.py +95 -0
- astreum/{_consensus → consensus}/workers/discovery.py +19 -1
- astreum/consensus/workers/validation.py +307 -0
- astreum/{_consensus → consensus}/workers/verify.py +29 -2
- astreum/crypto/chacha20poly1305.py +74 -0
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
- 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/{_lispeum → machine/models}/expression.py +36 -8
- astreum/machine/tokenizer.py +90 -0
- astreum/node.py +78 -767
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +183 -0
- astreum/storage/actions/set.py +178 -0
- astreum/{_storage → storage/models}/atom.py +55 -57
- astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
- astreum/storage/requests.py +28 -0
- astreum/storage/setup.py +22 -15
- astreum/utils/config.py +48 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
- astreum-0.3.9.dist-info/RECORD +71 -0
- astreum/_communication/message.py +0 -101
- astreum/_communication/peer.py +0 -23
- astreum/_communication/setup.py +0 -322
- astreum/_consensus/__init__.py +0 -20
- astreum/_consensus/account.py +0 -95
- astreum/_consensus/accounts.py +0 -38
- astreum/_consensus/block.py +0 -311
- astreum/_consensus/genesis.py +0 -72
- astreum/_consensus/receipt.py +0 -136
- astreum/_consensus/workers/validation.py +0 -125
- astreum/_lispeum/__init__.py +0 -16
- astreum/_lispeum/environment.py +0 -13
- astreum/_lispeum/low_evaluation.py +0 -123
- astreum/_lispeum/tokenizer.py +0 -22
- astreum/_node.py +0 -198
- astreum/_storage/__init__.py +0 -7
- astreum/_storage/setup.py +0 -35
- 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.61.dist-info/RECORD +0 -57
- /astreum/{models → communication/handlers}/__init__.py +0 -0
- /astreum/{_communication → communication/models}/ping.py +0 -0
- /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
- /astreum/{_lispeum → machine/models}/meter.py +0 -0
- /astreum/{_lispeum → machine}/parser.py +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from .atom import Atom, AtomKind, 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,29 +30,38 @@ 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
|
|
39
|
-
"""
|
|
40
|
-
if self._hash is None:
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
self._hash: Optional[bytes] = None
|
|
34
|
+
|
|
35
|
+
def hash(self) -> bytes:
|
|
36
|
+
"""
|
|
37
|
+
Compute and cache the canonical hash for this node (its type-atom id).
|
|
38
|
+
"""
|
|
39
|
+
if self._hash is None:
|
|
40
|
+
head_hash, _ = self._render_atoms()
|
|
41
|
+
self._hash = head_hash
|
|
42
|
+
return self._hash
|
|
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
|
|
43
54
|
|
|
44
|
-
def
|
|
55
|
+
def _render_atoms(self) -> Tuple[bytes, List[Atom]]:
|
|
45
56
|
"""
|
|
46
57
|
Materialise this node with the canonical atom layout used by the
|
|
47
|
-
storage layer: a leading SYMBOL atom with payload ``b"
|
|
58
|
+
storage layer: a leading SYMBOL atom with payload ``b"trie"`` whose
|
|
48
59
|
``next`` pointer links to four BYTES atoms containing, in order:
|
|
49
60
|
key (len byte + key payload), child_0 hash, child_1 hash, value bytes.
|
|
50
61
|
Returns the top atom hash and the emitted atoms.
|
|
51
62
|
"""
|
|
52
|
-
if self.key_len > 255:
|
|
53
|
-
raise ValueError("Patricia key length > 255 bits cannot be encoded in a single atom field")
|
|
54
|
-
|
|
55
63
|
entries: List[bytes] = [
|
|
56
|
-
|
|
64
|
+
self.key_len.to_bytes(2, "big", signed=False) + self.key,
|
|
57
65
|
self.child_0 or ZERO32,
|
|
58
66
|
self.child_1 or ZERO32,
|
|
59
67
|
self.value or b"",
|
|
@@ -62,104 +70,75 @@ class PatriciaNode:
|
|
|
62
70
|
data_atoms: List[Atom] = []
|
|
63
71
|
next_hash = ZERO32
|
|
64
72
|
for payload in reversed(entries):
|
|
65
|
-
atom = Atom
|
|
73
|
+
atom = Atom(data=payload, next_id=next_hash, kind=AtomKind.BYTES)
|
|
66
74
|
data_atoms.append(atom)
|
|
67
75
|
next_hash = atom.object_id()
|
|
68
76
|
|
|
69
77
|
data_atoms.reverse()
|
|
70
78
|
|
|
71
|
-
type_atom = Atom.
|
|
72
|
-
data=b"radix",
|
|
73
|
-
next_hash=next_hash,
|
|
74
|
-
kind=AtomKind.SYMBOL,
|
|
75
|
-
)
|
|
79
|
+
type_atom = Atom(data=b"trie", next_id=next_hash, kind=AtomKind.SYMBOL)
|
|
76
80
|
|
|
77
81
|
atoms = data_atoms + [type_atom]
|
|
78
82
|
return type_atom.object_id(), atoms
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if type_atom.data != b"radix":
|
|
116
|
-
raise ValueError("not a Patricia node (type mismatch)")
|
|
117
|
-
|
|
118
|
-
entries: List[bytes] = []
|
|
119
|
-
current = type_atom.next
|
|
120
|
-
hops = 0
|
|
121
|
-
|
|
122
|
-
while current and current != ZERO32 and hops < 4:
|
|
123
|
-
atom = node.storage_get(current)
|
|
124
|
-
if atom is None:
|
|
125
|
-
raise ValueError("missing atom while decoding Patricia node")
|
|
126
|
-
if _atom_kind(atom) is not AtomKind.BYTES:
|
|
127
|
-
raise ValueError("Patricia node detail atoms must be bytes")
|
|
128
|
-
entries.append(atom.data)
|
|
129
|
-
current = atom.next
|
|
130
|
-
hops += 1
|
|
131
|
-
|
|
132
|
-
if current and current != ZERO32:
|
|
133
|
-
raise ValueError("too many fields while decoding Patricia node")
|
|
134
|
-
|
|
135
|
-
if len(entries) != 4:
|
|
136
|
-
raise ValueError("incomplete atom sequence for Patricia node")
|
|
137
|
-
|
|
138
|
-
key_entry = entries[0]
|
|
139
|
-
if not key_entry:
|
|
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:
|
|
140
119
|
raise ValueError("missing key entry while decoding Patricia node")
|
|
141
|
-
key_len = key_entry[
|
|
142
|
-
key = key_entry[
|
|
143
|
-
child_0 =
|
|
144
|
-
child_1 =
|
|
145
|
-
value =
|
|
146
|
-
|
|
147
|
-
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)
|
|
148
127
|
|
|
149
|
-
class
|
|
128
|
+
class Trie:
|
|
150
129
|
"""
|
|
151
|
-
A compressed-key
|
|
130
|
+
A compressed-key Binary Radix Tree supporting get and put.
|
|
152
131
|
"""
|
|
153
132
|
|
|
154
|
-
def __init__(
|
|
155
|
-
self,
|
|
156
|
-
root_hash: Optional[bytes] = None,
|
|
157
|
-
) -> None:
|
|
158
|
-
"""
|
|
159
|
-
:param root_hash: optional hash of existing root node
|
|
160
|
-
"""
|
|
161
|
-
self.nodes: Dict[bytes,
|
|
162
|
-
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
|
|
163
142
|
|
|
164
143
|
@staticmethod
|
|
165
144
|
def _bit(buf: bytes, idx: int) -> bool:
|
|
@@ -189,67 +168,113 @@ class PatriciaTrie:
|
|
|
189
168
|
return False
|
|
190
169
|
return True
|
|
191
170
|
|
|
192
|
-
def _fetch(self, storage_node: "Node", h: bytes) -> Optional[
|
|
193
|
-
"""
|
|
194
|
-
Fetch a node by hash, consulting the in-memory cache first and falling
|
|
195
|
-
back to the atom storage provided by `storage_node`.
|
|
196
|
-
"""
|
|
197
|
-
cached = self.nodes.get(h)
|
|
198
|
-
if cached is not None:
|
|
199
|
-
return cached
|
|
200
|
-
|
|
201
|
-
if storage_node.storage_get(h) is None:
|
|
202
|
-
return None
|
|
203
|
-
|
|
204
|
-
pat_node =
|
|
205
|
-
self.nodes[h] = pat_node
|
|
206
|
-
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
|
|
207
186
|
|
|
208
187
|
def get(self, storage_node: "Node", key: bytes) -> Optional[bytes]:
|
|
209
188
|
"""
|
|
210
189
|
Return the stored value for `key`, or None if absent.
|
|
211
190
|
"""
|
|
212
|
-
# Empty trie?
|
|
213
|
-
if self.root_hash is None:
|
|
214
|
-
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
|
|
228
|
+
|
|
229
|
+
return None
|
|
215
230
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 {}
|
|
219
237
|
|
|
220
|
-
|
|
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]
|
|
221
243
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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")
|
|
227
251
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return current.value
|
|
252
|
+
results: Dict[bytes, bytes] = {}
|
|
253
|
+
stack: List[Tuple[bytes, str]] = [(self.root_hash, "")]
|
|
254
|
+
visited: Set[bytes] = set()
|
|
232
255
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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)
|
|
238
261
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
242
265
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return None # dangling pointer
|
|
266
|
+
if pat_node.value is not None:
|
|
267
|
+
key_bytes = _bits_to_bytes(combined_bits)
|
|
268
|
+
results[key_bytes] = pat_node.value
|
|
247
269
|
|
|
248
|
-
|
|
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"))
|
|
249
274
|
|
|
250
|
-
return
|
|
275
|
+
return results
|
|
251
276
|
|
|
252
|
-
def put(self, storage_node: "Node", key: bytes, value: bytes) -> None:
|
|
277
|
+
def put(self, storage_node: "Node", key: bytes, value: bytes) -> None:
|
|
253
278
|
"""
|
|
254
279
|
Insert or update `key` with `value` in-place.
|
|
255
280
|
"""
|
|
@@ -262,64 +287,64 @@ class PatriciaTrie:
|
|
|
262
287
|
return
|
|
263
288
|
|
|
264
289
|
# S2 – traversal bookkeeping
|
|
265
|
-
stack: List[Tuple[
|
|
266
|
-
current = self._fetch(storage_node, self.root_hash)
|
|
267
|
-
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
|
|
268
293
|
key_pos = 0
|
|
269
294
|
|
|
270
295
|
# S4 – main descent loop
|
|
271
296
|
while True:
|
|
272
297
|
# 4.1 – prefix mismatch? → split
|
|
273
|
-
if not self._match_prefix(current.key, current.key_len, key, key_pos):
|
|
274
|
-
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)
|
|
275
300
|
return
|
|
276
301
|
|
|
277
302
|
# 4.2 – consume this prefix
|
|
278
|
-
key_pos += current.key_len
|
|
303
|
+
key_pos += current.key_len
|
|
279
304
|
|
|
280
305
|
# 4.3 – matched entire key → update value
|
|
281
306
|
if key_pos == total_bits:
|
|
282
|
-
old_hash = current.hash()
|
|
283
|
-
current.value = value
|
|
284
|
-
self._invalidate_hash(current)
|
|
285
|
-
new_hash = current.hash()
|
|
286
|
-
if new_hash != old_hash:
|
|
287
|
-
self.nodes.pop(old_hash, None)
|
|
288
|
-
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
|
|
289
314
|
self._bubble(stack, new_hash)
|
|
290
315
|
return
|
|
291
316
|
|
|
292
317
|
# 4.4 – routing bit
|
|
293
318
|
next_bit = self._bit(key, key_pos)
|
|
294
|
-
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
|
|
295
320
|
|
|
296
321
|
# 4.6 – no child → easy append leaf
|
|
297
322
|
if child_hash is None:
|
|
298
|
-
self._append_leaf(current, next_bit, key, key_pos, value, stack)
|
|
323
|
+
self._append_leaf(current, next_bit, key, key_pos, value, stack)
|
|
299
324
|
return
|
|
300
325
|
|
|
301
326
|
# 4.7 – push current node onto stack
|
|
302
|
-
stack.append((current, current.hash(), int(next_bit)))
|
|
327
|
+
stack.append((current, current.hash(), int(next_bit)))
|
|
303
328
|
|
|
304
329
|
# 4.8 – fetch child and continue
|
|
305
|
-
child = self._fetch(storage_node, child_hash)
|
|
306
|
-
if child is None:
|
|
307
|
-
# Dangling pointer: treat as missing child
|
|
308
|
-
parent, _, _ = stack[-1]
|
|
309
|
-
self._append_leaf(parent, next_bit, key, key_pos, value, stack[:-1])
|
|
310
|
-
return
|
|
311
|
-
|
|
312
|
-
current = child
|
|
313
|
-
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
|
|
314
339
|
|
|
315
340
|
def _append_leaf(
|
|
316
341
|
self,
|
|
317
|
-
parent:
|
|
342
|
+
parent: TrieNode,
|
|
318
343
|
dir_bit: bool,
|
|
319
344
|
key: bytes,
|
|
320
345
|
key_pos: int,
|
|
321
346
|
value: bytes,
|
|
322
|
-
stack: List[Tuple[
|
|
347
|
+
stack: List[Tuple[TrieNode, bytes, int]],
|
|
323
348
|
) -> None:
|
|
324
349
|
tail_len = len(key) * 8 - (key_pos + 1)
|
|
325
350
|
tail_bits, tail_len = self._bit_slice(key, key_pos + 1, tail_len)
|
|
@@ -342,8 +367,8 @@ class PatriciaTrie:
|
|
|
342
367
|
|
|
343
368
|
def _split_and_insert(
|
|
344
369
|
self,
|
|
345
|
-
node:
|
|
346
|
-
stack: List[Tuple[
|
|
370
|
+
node: TrieNode,
|
|
371
|
+
stack: List[Tuple[TrieNode, bytes, int]],
|
|
347
372
|
key: bytes,
|
|
348
373
|
key_pos: int,
|
|
349
374
|
value: bytes,
|
|
@@ -416,46 +441,45 @@ class PatriciaTrie:
|
|
|
416
441
|
value: Optional[bytes],
|
|
417
442
|
child0: Optional[bytes],
|
|
418
443
|
child1: Optional[bytes],
|
|
419
|
-
) ->
|
|
420
|
-
node =
|
|
444
|
+
) -> TrieNode:
|
|
445
|
+
node = TrieNode(prefix_len, prefix_bits, value, child0, child1)
|
|
421
446
|
self.nodes[node.hash()] = node
|
|
422
447
|
return node
|
|
423
448
|
|
|
424
|
-
def _invalidate_hash(self, node:
|
|
449
|
+
def _invalidate_hash(self, node: TrieNode) -> None:
|
|
425
450
|
"""Clear cached hash so next .hash() recomputes."""
|
|
426
451
|
node._hash = None # type: ignore
|
|
427
452
|
|
|
428
453
|
def _bubble(
|
|
429
454
|
self,
|
|
430
|
-
stack: List[Tuple[
|
|
455
|
+
stack: List[Tuple[TrieNode, bytes, int]],
|
|
431
456
|
new_hash: bytes
|
|
432
457
|
) -> None:
|
|
433
458
|
"""
|
|
434
459
|
Propagate updated child-hash `new_hash` up the ancestor stack,
|
|
435
460
|
rebasing each parent's pointer, invalidating and re-hashing.
|
|
436
461
|
"""
|
|
437
|
-
while stack:
|
|
438
|
-
parent, old_hash, dir_bit = stack.pop()
|
|
439
|
-
|
|
440
|
-
if dir_bit == 0:
|
|
441
|
-
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
|
|
442
467
|
else:
|
|
443
468
|
parent.child_1 = new_hash
|
|
444
469
|
|
|
445
470
|
self._invalidate_hash(parent)
|
|
446
471
|
new_hash = parent.hash()
|
|
447
472
|
if new_hash != old_hash:
|
|
448
|
-
self.nodes.pop(old_hash, None)
|
|
449
|
-
self.nodes[new_hash] = parent
|
|
450
|
-
|
|
451
|
-
self.root_hash = new_hash
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
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
|
|
459
483
|
) -> tuple[bytes, int]:
|
|
460
484
|
"""
|
|
461
485
|
Extract `length` bits from `buf` starting at `start_bit` (MSB-first),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from threading import RLock
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .. import Node
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_atom_req(node: "Node", atom_id: bytes) -> None:
|
|
11
|
+
"""Mark an atom request as pending."""
|
|
12
|
+
with node.atom_requests_lock:
|
|
13
|
+
node.atom_requests.add(atom_id)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def has_atom_req(node: "Node", atom_id: bytes) -> bool:
|
|
17
|
+
"""Return True if the atom request is currently tracked."""
|
|
18
|
+
with node.atom_requests_lock:
|
|
19
|
+
return atom_id in node.atom_requests
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def pop_atom_req(node: "Node", atom_id: bytes) -> bool:
|
|
23
|
+
"""Remove the pending request if present. Returns True when removed."""
|
|
24
|
+
with node.atom_requests_lock:
|
|
25
|
+
if atom_id in node.atom_requests:
|
|
26
|
+
node.atom_requests.remove(atom_id)
|
|
27
|
+
return True
|
|
28
|
+
return False
|
astreum/storage/setup.py
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def storage_setup(node: Any, config: dict) -> None:
|
|
7
|
+
"""Initialize hot/cold storage helpers on the node."""
|
|
8
|
+
|
|
9
|
+
node.logger.info("Setting up node storage")
|
|
10
|
+
|
|
11
|
+
node.hot_storage = {}
|
|
12
|
+
node.hot_storage_hits = {}
|
|
13
|
+
node.storage_index = {}
|
|
14
|
+
node.hot_storage_size = 0
|
|
15
|
+
node.cold_storage_size = 0
|
|
16
|
+
|
|
17
|
+
node.logger.info(
|
|
18
|
+
"Storage ready (hot_limit=%s bytes, cold_limit=%s bytes, cold_path=%s)",
|
|
19
|
+
config["hot_storage_default_limit"],
|
|
20
|
+
config["cold_storage_limit"],
|
|
21
|
+
config["cold_storage_path"] or "disabled",
|
|
22
|
+
)
|