astreum 0.3.46__py3-none-any.whl → 0.3.48__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/communication/handlers/object_request.py +103 -45
- astreum/communication/handlers/object_response.py +111 -9
- astreum/communication/setup.py +1 -1
- astreum/machine/evaluations/low_evaluation.py +5 -5
- astreum/machine/models/expression.py +5 -5
- astreum/node.py +13 -11
- astreum/storage/actions/get.py +257 -186
- astreum/storage/models/atom.py +0 -14
- astreum/storage/models/trie.py +2 -2
- astreum/storage/requests.py +13 -10
- astreum/storage/setup.py +14 -10
- astreum/utils/config.py +28 -0
- astreum/validation/models/account.py +1 -1
- astreum/validation/models/block.py +3 -3
- astreum/validation/models/receipt.py +1 -1
- astreum/validation/models/transaction.py +7 -7
- {astreum-0.3.46.dist-info → astreum-0.3.48.dist-info}/METADATA +3 -1
- {astreum-0.3.46.dist-info → astreum-0.3.48.dist-info}/RECORD +21 -21
- {astreum-0.3.46.dist-info → astreum-0.3.48.dist-info}/WHEEL +0 -0
- {astreum-0.3.46.dist-info → astreum-0.3.48.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.3.46.dist-info → astreum-0.3.48.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import socket
|
|
3
3
|
from enum import IntEnum
|
|
4
|
-
from typing import TYPE_CHECKING, Tuple
|
|
4
|
+
from typing import Optional, TYPE_CHECKING, Tuple
|
|
5
5
|
|
|
6
|
-
from .object_response import
|
|
6
|
+
from .object_response import (
|
|
7
|
+
ObjectResponse,
|
|
8
|
+
ObjectResponseType,
|
|
9
|
+
OBJECT_FOUND_ATOM_PAYLOAD,
|
|
10
|
+
OBJECT_FOUND_LIST_PAYLOAD,
|
|
11
|
+
encode_object_found_atom_payload,
|
|
12
|
+
encode_object_found_list_payload,
|
|
13
|
+
)
|
|
7
14
|
from ..outgoing_queue import enqueue_outgoing
|
|
8
15
|
from ..models.message import Message, MessageTopic
|
|
9
16
|
from ..util import xor_distance
|
|
@@ -19,22 +26,34 @@ class ObjectRequestType(IntEnum):
|
|
|
19
26
|
OBJECT_PUT = 1
|
|
20
27
|
|
|
21
28
|
|
|
22
|
-
class ObjectRequest:
|
|
23
|
-
type: ObjectRequestType
|
|
24
|
-
data: bytes
|
|
25
|
-
atom_id: bytes
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
self
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
class ObjectRequest:
|
|
30
|
+
type: ObjectRequestType
|
|
31
|
+
data: bytes
|
|
32
|
+
atom_id: bytes
|
|
33
|
+
payload_type: Optional[int]
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
type: ObjectRequestType,
|
|
38
|
+
data: bytes = b"",
|
|
39
|
+
atom_id: bytes = None,
|
|
40
|
+
payload_type: Optional[int] = None,
|
|
41
|
+
):
|
|
42
|
+
self.type = type
|
|
43
|
+
self.data = data
|
|
44
|
+
self.atom_id = atom_id
|
|
45
|
+
self.payload_type = payload_type
|
|
46
|
+
|
|
47
|
+
def to_bytes(self):
|
|
48
|
+
if self.type == ObjectRequestType.OBJECT_GET and self.payload_type is not None:
|
|
49
|
+
payload = bytes([self.payload_type]) + self.data
|
|
50
|
+
else:
|
|
51
|
+
payload = self.data
|
|
52
|
+
return bytes([self.type.value]) + self.atom_id + payload
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_bytes(cls, data: bytes) -> "ObjectRequest":
|
|
56
|
+
# need at least 1 byte for type + 32 bytes for hash
|
|
38
57
|
if len(data) < 1 + 32:
|
|
39
58
|
raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
|
|
40
59
|
|
|
@@ -44,9 +63,16 @@ class ObjectRequest:
|
|
|
44
63
|
except ValueError:
|
|
45
64
|
raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
|
|
46
65
|
|
|
47
|
-
atom_id_bytes = data[1:33]
|
|
48
|
-
payload
|
|
49
|
-
|
|
66
|
+
atom_id_bytes = data[1:33]
|
|
67
|
+
payload = data[33:]
|
|
68
|
+
if req_type == ObjectRequestType.OBJECT_GET:
|
|
69
|
+
if payload:
|
|
70
|
+
payload_type = payload[0]
|
|
71
|
+
payload = payload[1:]
|
|
72
|
+
else:
|
|
73
|
+
payload_type = None
|
|
74
|
+
return cls(req_type, payload, atom_id_bytes, payload_type=payload_type)
|
|
75
|
+
return cls(req_type, payload, atom_id_bytes)
|
|
50
76
|
|
|
51
77
|
|
|
52
78
|
def encode_peer_contact_bytes(peer: "Peer") -> bytes:
|
|
@@ -75,31 +101,63 @@ def handle_object_request(node: "Node", peer: "Peer", message: Message) -> None:
|
|
|
75
101
|
return
|
|
76
102
|
|
|
77
103
|
match object_request.type:
|
|
78
|
-
case ObjectRequestType.OBJECT_GET:
|
|
79
|
-
atom_id = object_request.atom_id
|
|
80
|
-
node.logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), peer.address)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
atom_id
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
case ObjectRequestType.OBJECT_GET:
|
|
105
|
+
atom_id = object_request.atom_id
|
|
106
|
+
node.logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), peer.address)
|
|
107
|
+
payload_type = object_request.payload_type
|
|
108
|
+
if payload_type is None:
|
|
109
|
+
payload_type = OBJECT_FOUND_ATOM_PAYLOAD
|
|
110
|
+
|
|
111
|
+
if payload_type == OBJECT_FOUND_ATOM_PAYLOAD:
|
|
112
|
+
local_atom = node.get_atom_from_local_storage(atom_id=atom_id)
|
|
113
|
+
if local_atom is not None:
|
|
114
|
+
node.logger.debug("Object %s found locally; returning to %s", atom_id.hex(), peer.address)
|
|
115
|
+
resp = ObjectResponse(
|
|
116
|
+
type=ObjectResponseType.OBJECT_FOUND,
|
|
117
|
+
data=encode_object_found_atom_payload(local_atom),
|
|
118
|
+
atom_id=atom_id
|
|
119
|
+
)
|
|
120
|
+
obj_res_msg = Message(
|
|
121
|
+
topic=MessageTopic.OBJECT_RESPONSE,
|
|
122
|
+
body=resp.to_bytes(),
|
|
123
|
+
sender=node.relay_public_key,
|
|
124
|
+
)
|
|
125
|
+
obj_res_msg.encrypt(peer.shared_key_bytes)
|
|
126
|
+
enqueue_outgoing(
|
|
127
|
+
node,
|
|
128
|
+
peer.address,
|
|
129
|
+
message=obj_res_msg,
|
|
130
|
+
difficulty=peer.difficulty,
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
elif payload_type == OBJECT_FOUND_LIST_PAYLOAD:
|
|
134
|
+
local_atoms = node.get_atom_list_from_local_storage(root_hash=atom_id)
|
|
135
|
+
if local_atoms is not None:
|
|
136
|
+
node.logger.debug("Object list %s found locally; returning to %s", atom_id.hex(), peer.address)
|
|
137
|
+
resp = ObjectResponse(
|
|
138
|
+
type=ObjectResponseType.OBJECT_FOUND,
|
|
139
|
+
data=encode_object_found_list_payload(local_atoms),
|
|
140
|
+
atom_id=atom_id
|
|
141
|
+
)
|
|
142
|
+
obj_res_msg = Message(
|
|
143
|
+
topic=MessageTopic.OBJECT_RESPONSE,
|
|
144
|
+
body=resp.to_bytes(),
|
|
145
|
+
sender=node.relay_public_key,
|
|
146
|
+
)
|
|
147
|
+
obj_res_msg.encrypt(peer.shared_key_bytes)
|
|
148
|
+
enqueue_outgoing(
|
|
149
|
+
node,
|
|
150
|
+
peer.address,
|
|
151
|
+
message=obj_res_msg,
|
|
152
|
+
difficulty=peer.difficulty,
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
else:
|
|
156
|
+
node.logger.warning(
|
|
157
|
+
"Unknown OBJECT_GET payload type %s for %s",
|
|
158
|
+
payload_type,
|
|
159
|
+
atom_id.hex(),
|
|
101
160
|
)
|
|
102
|
-
return
|
|
103
161
|
|
|
104
162
|
if atom_id in node.storage_index:
|
|
105
163
|
provider_id = node.storage_index[atom_id]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import socket
|
|
2
2
|
from enum import IntEnum
|
|
3
|
-
from typing import Tuple, TYPE_CHECKING
|
|
3
|
+
from typing import List, Tuple, TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from ..outgoing_queue import enqueue_outgoing
|
|
6
6
|
from ..models.message import Message, MessageTopic
|
|
7
7
|
from ...storage.models.atom import Atom
|
|
8
|
+
from ...storage.requests import get_atom_req_payload
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
11
|
from .. import Node
|
|
@@ -17,6 +18,10 @@ class ObjectResponseType(IntEnum):
|
|
|
17
18
|
OBJECT_NEAREST_PEER = 2
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
OBJECT_FOUND_ATOM_PAYLOAD = 1
|
|
22
|
+
OBJECT_FOUND_LIST_PAYLOAD = 2
|
|
23
|
+
|
|
24
|
+
|
|
20
25
|
class ObjectResponse:
|
|
21
26
|
type: ObjectResponseType
|
|
22
27
|
data: bytes
|
|
@@ -47,6 +52,37 @@ class ObjectResponse:
|
|
|
47
52
|
return cls(resp_type, payload, atom_id)
|
|
48
53
|
|
|
49
54
|
|
|
55
|
+
def encode_object_found_atom_payload(atom: Atom) -> bytes:
|
|
56
|
+
return bytes([OBJECT_FOUND_ATOM_PAYLOAD]) + atom.to_bytes()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def encode_object_found_list_payload(atoms: List[Atom]) -> bytes:
|
|
60
|
+
parts = [bytes([OBJECT_FOUND_LIST_PAYLOAD])]
|
|
61
|
+
for atom in atoms:
|
|
62
|
+
atom_bytes = atom.to_bytes()
|
|
63
|
+
parts.append(len(atom_bytes).to_bytes(4, "big", signed=False))
|
|
64
|
+
parts.append(atom_bytes)
|
|
65
|
+
return b"".join(parts)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def decode_object_found_list_payload(payload: bytes) -> List[Atom]:
|
|
69
|
+
atoms: List[Atom] = []
|
|
70
|
+
offset = 0
|
|
71
|
+
while offset < len(payload):
|
|
72
|
+
if len(payload) - offset < 4:
|
|
73
|
+
raise ValueError("truncated atom length")
|
|
74
|
+
atom_len = int.from_bytes(payload[offset : offset + 4], "big", signed=False)
|
|
75
|
+
offset += 4
|
|
76
|
+
if atom_len <= 0:
|
|
77
|
+
raise ValueError("invalid atom length")
|
|
78
|
+
end = offset + atom_len
|
|
79
|
+
if end > len(payload):
|
|
80
|
+
raise ValueError("truncated atom payload")
|
|
81
|
+
atoms.append(Atom.from_bytes(payload[offset:end]))
|
|
82
|
+
offset = end
|
|
83
|
+
return atoms
|
|
84
|
+
|
|
85
|
+
|
|
50
86
|
def decode_object_provider(payload: bytes) -> Tuple[bytes, str, int]:
|
|
51
87
|
expected_len = 32 + 4 + 2
|
|
52
88
|
if len(payload) < expected_len:
|
|
@@ -77,17 +113,78 @@ def handle_object_response(node: "Node", peer: "Peer", message: Message) -> None
|
|
|
77
113
|
|
|
78
114
|
match object_response.type:
|
|
79
115
|
case ObjectResponseType.OBJECT_FOUND:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if object_response.atom_id == atom_id:
|
|
83
|
-
node.pop_atom_req(atom_id)
|
|
84
|
-
node._hot_storage_set(atom_id, atom)
|
|
85
|
-
else:
|
|
116
|
+
payload = object_response.data
|
|
117
|
+
if not payload:
|
|
86
118
|
node.logger.warning(
|
|
87
|
-
"OBJECT_FOUND
|
|
119
|
+
"OBJECT_FOUND payload for %s missing content",
|
|
88
120
|
object_response.atom_id.hex(),
|
|
89
|
-
atom_id.hex(),
|
|
90
121
|
)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
payload_type = payload[0]
|
|
125
|
+
body = payload[1:]
|
|
126
|
+
|
|
127
|
+
if payload_type == OBJECT_FOUND_ATOM_PAYLOAD:
|
|
128
|
+
try:
|
|
129
|
+
atom = Atom.from_bytes(body)
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
node.logger.warning(
|
|
132
|
+
"Invalid OBJECT_FOUND atom payload for %s: %s",
|
|
133
|
+
object_response.atom_id.hex(),
|
|
134
|
+
exc,
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
atom_id = atom.object_id()
|
|
139
|
+
if object_response.atom_id != atom_id:
|
|
140
|
+
node.logger.warning(
|
|
141
|
+
"OBJECT_FOUND atom ID mismatch (expected=%s got=%s)",
|
|
142
|
+
object_response.atom_id.hex(),
|
|
143
|
+
atom_id.hex(),
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
node.pop_atom_req(atom_id)
|
|
148
|
+
node._hot_storage_set(atom_id, atom)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
if payload_type == OBJECT_FOUND_LIST_PAYLOAD:
|
|
152
|
+
try:
|
|
153
|
+
atoms = decode_object_found_list_payload(body)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
node.logger.warning(
|
|
156
|
+
"Invalid OBJECT_FOUND list payload for %s: %s",
|
|
157
|
+
object_response.atom_id.hex(),
|
|
158
|
+
exc,
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
if not atoms:
|
|
163
|
+
node.logger.warning(
|
|
164
|
+
"OBJECT_FOUND list payload for %s contained no atoms",
|
|
165
|
+
object_response.atom_id.hex(),
|
|
166
|
+
)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
root_id = atoms[0].object_id()
|
|
170
|
+
if object_response.atom_id != root_id:
|
|
171
|
+
node.logger.warning(
|
|
172
|
+
"OBJECT_FOUND list root ID mismatch (expected=%s got=%s)",
|
|
173
|
+
object_response.atom_id.hex(),
|
|
174
|
+
root_id.hex(),
|
|
175
|
+
)
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
node.pop_atom_req(root_id)
|
|
179
|
+
for atom in atoms:
|
|
180
|
+
node._hot_storage_set(atom.object_id(), atom)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
node.logger.warning(
|
|
184
|
+
"Unknown OBJECT_FOUND payload type %s for %s",
|
|
185
|
+
payload_type,
|
|
186
|
+
object_response.atom_id.hex(),
|
|
187
|
+
)
|
|
91
188
|
|
|
92
189
|
case ObjectResponseType.OBJECT_PROVIDER:
|
|
93
190
|
try:
|
|
@@ -98,10 +195,15 @@ def handle_object_response(node: "Node", peer: "Peer", message: Message) -> None
|
|
|
98
195
|
|
|
99
196
|
from .object_request import ObjectRequest, ObjectRequestType
|
|
100
197
|
|
|
198
|
+
payload_type = get_atom_req_payload(node, object_response.atom_id)
|
|
199
|
+
if payload_type is None:
|
|
200
|
+
payload_type = OBJECT_FOUND_ATOM_PAYLOAD
|
|
201
|
+
|
|
101
202
|
obj_req = ObjectRequest(
|
|
102
203
|
type=ObjectRequestType.OBJECT_GET,
|
|
103
204
|
data=b"",
|
|
104
205
|
atom_id=object_response.atom_id,
|
|
206
|
+
payload_type=payload_type,
|
|
105
207
|
)
|
|
106
208
|
obj_req_bytes = obj_req.to_bytes()
|
|
107
209
|
obj_req_msg = Message(
|
astreum/communication/setup.py
CHANGED
|
@@ -206,7 +206,7 @@ def communication_setup(node: "Node", config: dict):
|
|
|
206
206
|
|
|
207
207
|
# connection state & atom request tracking
|
|
208
208
|
node.is_connected = False
|
|
209
|
-
node.atom_requests =
|
|
209
|
+
node.atom_requests = {}
|
|
210
210
|
node.atom_requests_lock = threading.RLock()
|
|
211
211
|
|
|
212
212
|
# sockets + queues + threads
|
|
@@ -143,7 +143,7 @@ def low_eval(self, code: List[bytes], meter: Meter) -> Expr:
|
|
|
143
143
|
if idx < 0 or length < 0:
|
|
144
144
|
return error_expr("low_eval", "bad slice")
|
|
145
145
|
|
|
146
|
-
atom = self.
|
|
146
|
+
atom = self.get_atom(atom_id=id_b)
|
|
147
147
|
if atom is None:
|
|
148
148
|
return error_expr("low_eval", "unknown atom")
|
|
149
149
|
|
|
@@ -173,7 +173,7 @@ def low_eval(self, code: List[bytes], meter: Meter) -> Expr:
|
|
|
173
173
|
if not meter.charge_bytes(len(id1_b) + len(id2_b)):
|
|
174
174
|
return error_expr("low_eval", "meter limit")
|
|
175
175
|
|
|
176
|
-
atom = self.
|
|
176
|
+
atom = self.get_atom(atom_id=id_b)
|
|
177
177
|
if atom is None:
|
|
178
178
|
return error_expr("low_eval", "unknown atom")
|
|
179
179
|
|
|
@@ -195,8 +195,8 @@ def low_eval(self, code: List[bytes], meter: Meter) -> Expr:
|
|
|
195
195
|
id2_b = stack.pop()
|
|
196
196
|
id1_b = stack.pop()
|
|
197
197
|
|
|
198
|
-
atom1 = self.
|
|
199
|
-
atom2 = self.
|
|
198
|
+
atom1 = self.get_atom(atom_id=id1_b)
|
|
199
|
+
atom2 = self.get_atom(atom_id=id2_b)
|
|
200
200
|
if atom1 is None or atom2 is None:
|
|
201
201
|
return error_expr("low_eval", "unknown atom")
|
|
202
202
|
|
|
@@ -263,7 +263,7 @@ def low_eval(self, code: List[bytes], meter: Meter) -> Expr:
|
|
|
263
263
|
if length > 32:
|
|
264
264
|
return error_expr("low_eval", "load too wide")
|
|
265
265
|
|
|
266
|
-
atom = self.
|
|
266
|
+
atom = self.get_atom(atom_id=id_b)
|
|
267
267
|
if atom is None:
|
|
268
268
|
return error_expr("low_eval", "unknown atom")
|
|
269
269
|
|
|
@@ -46,16 +46,16 @@ class Expr:
|
|
|
46
46
|
if not isinstance(root_hash, (bytes, bytearray)):
|
|
47
47
|
raise TypeError("root hash must be bytes-like")
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
if not callable(
|
|
51
|
-
raise TypeError("node must provide a callable '
|
|
49
|
+
get_atom = getattr(node, "get_atom", None)
|
|
50
|
+
if not callable(get_atom):
|
|
51
|
+
raise TypeError("node must provide a callable 'get_atom'")
|
|
52
52
|
|
|
53
53
|
expr_id = bytes(root_hash)
|
|
54
54
|
|
|
55
55
|
def _require(atom_id: Optional[bytes], context: str):
|
|
56
56
|
if not atom_id:
|
|
57
57
|
raise ValueError(f"missing atom id while decoding {context}")
|
|
58
|
-
atom =
|
|
58
|
+
atom = get_atom(atom_id)
|
|
59
59
|
if atom is None:
|
|
60
60
|
raise ValueError(f"missing atom data while decoding {context}")
|
|
61
61
|
return atom
|
|
@@ -197,7 +197,7 @@ def error_expr(topic: str, message: str) -> Expr.ListExpr:
|
|
|
197
197
|
|
|
198
198
|
def get_expr_list_from_storage(self, key: bytes) -> Optional["ListExpr"]:
|
|
199
199
|
"""Load a list expression from storage using the given atom list root hash."""
|
|
200
|
-
atoms = self.
|
|
200
|
+
atoms = self.get_atom_list(key)
|
|
201
201
|
if atoms is None:
|
|
202
202
|
return None
|
|
203
203
|
|
astreum/node.py
CHANGED
|
@@ -20,14 +20,15 @@ from astreum.verification.node import verify_blockchain
|
|
|
20
20
|
from astreum.machine import Expr, high_eval, low_eval, script_eval
|
|
21
21
|
from astreum.machine.models.environment import Env, env_get, env_set
|
|
22
22
|
from astreum.machine.models.expression import get_expr_list_from_storage
|
|
23
|
-
from astreum.storage.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
from astreum.storage.actions.get import (
|
|
24
|
+
_hot_storage_get,
|
|
25
|
+
_cold_storage_get,
|
|
26
|
+
_network_get,
|
|
27
|
+
get_atom_from_local_storage,
|
|
28
|
+
get_atom,
|
|
29
|
+
get_atom_list_from_local_storage,
|
|
30
|
+
get_atom_list,
|
|
31
|
+
)
|
|
31
32
|
from astreum.storage.actions.set import (
|
|
32
33
|
_hot_storage_set,
|
|
33
34
|
_cold_storage_set,
|
|
@@ -86,11 +87,12 @@ class Node:
|
|
|
86
87
|
_cold_storage_set = _cold_storage_set
|
|
87
88
|
_network_set = _network_set
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
get_atom_from_local_storage = get_atom_from_local_storage
|
|
91
|
+
get_atom = get_atom
|
|
92
|
+
get_atom_list_from_local_storage = get_atom_list_from_local_storage
|
|
93
|
+
get_atom_list = get_atom_list
|
|
91
94
|
|
|
92
95
|
get_expr_list_from_storage = get_expr_list_from_storage
|
|
93
|
-
get_atom_list_from_storage = get_atom_list_from_storage
|
|
94
96
|
|
|
95
97
|
add_atom_req = add_atom_req
|
|
96
98
|
has_atom_req = has_atom_req
|
astreum/storage/actions/get.py
CHANGED
|
@@ -1,127 +1,91 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
from ..
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
atom
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
self.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from time import sleep
|
|
5
|
+
from typing import List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from ..models.atom import Atom, ZERO32
|
|
8
|
+
from ..providers import provider_payload_for_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _hot_storage_get(self, key: bytes) -> Optional[Atom]:
|
|
12
|
+
"""Retrieve an atom from in-memory cache while tracking hit statistics."""
|
|
13
|
+
atom = self.hot_storage.get(key)
|
|
14
|
+
if atom is not None:
|
|
15
|
+
self.hot_storage_hits[key] = self.hot_storage_hits.get(key, 0) + 1
|
|
16
|
+
self.logger.debug("Hot storage hit for %s", key.hex())
|
|
17
|
+
else:
|
|
18
|
+
self.logger.debug("Hot storage miss for %s", key.hex())
|
|
19
|
+
return atom
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _network_get(self, atom_id: bytes, payload_type: int) -> Optional[Union[Atom, List[Atom]]]:
|
|
23
|
+
"""Attempt to fetch an atom from network peers when local storage misses."""
|
|
24
|
+
from ...communication.handlers.object_response import (
|
|
25
|
+
OBJECT_FOUND_ATOM_PAYLOAD,
|
|
26
|
+
OBJECT_FOUND_LIST_PAYLOAD,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def _wait_for_atom(atom_id: bytes, interval: float, retries: int) -> Optional[Atom]:
|
|
30
|
+
if interval <= 0 or retries <= 0:
|
|
31
|
+
return self.get_atom_from_local_storage(atom_id=atom_id)
|
|
32
|
+
for _ in range(retries):
|
|
33
|
+
atom = self.get_atom_from_local_storage(atom_id=atom_id)
|
|
34
|
+
if atom is not None:
|
|
35
|
+
return atom
|
|
36
|
+
sleep(interval)
|
|
37
|
+
return self.get_atom_from_local_storage(atom_id=atom_id)
|
|
38
|
+
|
|
39
|
+
def _wait_for_list(root_hash: bytes, interval: float, retries: int) -> Optional[List[Atom]]:
|
|
40
|
+
if interval <= 0 or retries <= 0:
|
|
41
|
+
return self.get_atom_list_from_local_storage(root_hash=root_hash)
|
|
42
|
+
for _ in range(retries):
|
|
43
|
+
atoms = self.get_atom_list_from_local_storage(root_hash=root_hash)
|
|
44
|
+
if atoms is not None:
|
|
45
|
+
return atoms
|
|
46
|
+
sleep(interval)
|
|
47
|
+
return self.get_atom_list_from_local_storage(root_hash=root_hash)
|
|
48
|
+
|
|
49
|
+
def _wait_for_payload() -> Optional[Union[Atom, List[Atom]]]:
|
|
50
|
+
wait_interval = self.config["atom_fetch_interval"]
|
|
51
|
+
wait_retries = self.config["atom_fetch_retries"]
|
|
52
|
+
if payload_type == OBJECT_FOUND_ATOM_PAYLOAD:
|
|
53
|
+
return _wait_for_atom(atom_id, wait_interval, wait_retries)
|
|
54
|
+
if payload_type == OBJECT_FOUND_LIST_PAYLOAD:
|
|
55
|
+
return _wait_for_list(atom_id, wait_interval, wait_retries)
|
|
56
|
+
self.logger.warning(
|
|
57
|
+
"Unknown payload type %s for %s",
|
|
58
|
+
payload_type,
|
|
59
|
+
atom_id.hex(),
|
|
31
60
|
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if closest_peer is None or closest_peer.address is None:
|
|
49
|
-
self.logger.debug("No peer available to fetch %s", key.hex())
|
|
50
|
-
return None
|
|
51
|
-
|
|
52
|
-
obj_req = ObjectRequest(
|
|
53
|
-
type=ObjectRequestType.OBJECT_GET,
|
|
54
|
-
data=b"",
|
|
55
|
-
atom_id=key,
|
|
56
|
-
)
|
|
57
|
-
try:
|
|
58
|
-
message = Message(
|
|
59
|
-
topic=MessageTopic.OBJECT_REQUEST,
|
|
60
|
-
content=obj_req.to_bytes(),
|
|
61
|
-
sender=self.relay_public_key,
|
|
62
|
-
)
|
|
63
|
-
except Exception as exc:
|
|
64
|
-
self.logger.warning("Failed to build object request for %s: %s", key.hex(), exc)
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
# encrypt the outbound request for the target peer
|
|
68
|
-
message.encrypt(closest_peer.shared_key_bytes)
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
self.add_atom_req(key)
|
|
72
|
-
except Exception as exc:
|
|
73
|
-
self.logger.warning("Failed to track object request for %s: %s", key.hex(), exc)
|
|
74
|
-
|
|
75
|
-
try:
|
|
76
|
-
queued = enqueue_outgoing(
|
|
77
|
-
self,
|
|
78
|
-
closest_peer.address,
|
|
79
|
-
message=message,
|
|
80
|
-
difficulty=closest_peer.difficulty,
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
if payload_type == OBJECT_FOUND_ATOM_PAYLOAD:
|
|
64
|
+
local_atom = self.get_atom_from_local_storage(atom_id=atom_id)
|
|
65
|
+
if local_atom is not None:
|
|
66
|
+
return local_atom
|
|
67
|
+
elif payload_type == OBJECT_FOUND_LIST_PAYLOAD:
|
|
68
|
+
local_atoms = self.get_atom_list_from_local_storage(root_hash=atom_id)
|
|
69
|
+
if local_atoms is not None:
|
|
70
|
+
return local_atoms
|
|
71
|
+
else:
|
|
72
|
+
self.logger.warning(
|
|
73
|
+
"Unknown payload type %s for %s",
|
|
74
|
+
payload_type,
|
|
75
|
+
atom_id.hex(),
|
|
81
76
|
)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
except Exception as exc:
|
|
95
|
-
self.logger.warning(
|
|
96
|
-
"Failed to queue OBJECT_GET for %s to %s: %s",
|
|
97
|
-
key.hex(),
|
|
98
|
-
closest_peer.address,
|
|
99
|
-
exc,
|
|
100
|
-
)
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def storage_get(self, key: bytes) -> Optional[Atom]:
|
|
105
|
-
"""Retrieve an Atom by checking local storage first, then the network."""
|
|
106
|
-
self.logger.debug("Fetching atom %s", key.hex())
|
|
107
|
-
atom = self._hot_storage_get(key)
|
|
108
|
-
if atom is not None:
|
|
109
|
-
self.logger.debug("Returning atom %s from hot storage", key.hex())
|
|
110
|
-
return atom
|
|
111
|
-
atom = self._cold_storage_get(key)
|
|
112
|
-
if atom is not None:
|
|
113
|
-
self.logger.debug("Returning atom %s from cold storage", key.hex())
|
|
114
|
-
return atom
|
|
115
|
-
|
|
116
|
-
if not self.is_connected:
|
|
117
|
-
return None
|
|
118
|
-
|
|
119
|
-
provider_id = self.storage_index.get(key)
|
|
120
|
-
if provider_id is not None:
|
|
121
|
-
provider_payload = provider_payload_for_id(self, provider_id)
|
|
122
|
-
if provider_payload is not None:
|
|
123
|
-
try:
|
|
124
|
-
from ...communication.handlers.object_response import decode_object_provider
|
|
77
|
+
|
|
78
|
+
if not getattr(self, "is_connected", False):
|
|
79
|
+
self.logger.debug("Network fetch skipped for %s; node not connected", atom_id.hex())
|
|
80
|
+
return None
|
|
81
|
+
self.logger.debug("Attempting network fetch for %s", atom_id.hex())
|
|
82
|
+
|
|
83
|
+
provider_id = self.storage_index.get(atom_id)
|
|
84
|
+
if provider_id is not None:
|
|
85
|
+
provider_payload = provider_payload_for_id(self, provider_id)
|
|
86
|
+
if provider_payload is not None:
|
|
87
|
+
try:
|
|
88
|
+
from ...communication.handlers.object_response import decode_object_provider
|
|
125
89
|
from ...communication.handlers.object_request import (
|
|
126
90
|
ObjectRequest,
|
|
127
91
|
ObjectRequestType,
|
|
@@ -129,23 +93,24 @@ def storage_get(self, key: bytes) -> Optional[Atom]:
|
|
|
129
93
|
from ...communication.models.message import Message, MessageTopic
|
|
130
94
|
from ...communication.outgoing_queue import enqueue_outgoing
|
|
131
95
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
132
|
-
|
|
133
|
-
provider_key, provider_address, provider_port = decode_object_provider(provider_payload)
|
|
134
|
-
provider_public_key = X25519PublicKey.from_public_bytes(provider_key)
|
|
135
|
-
shared_key_bytes = self.relay_secret_key.exchange(provider_public_key)
|
|
136
|
-
|
|
137
|
-
obj_req = ObjectRequest(
|
|
138
|
-
type=ObjectRequestType.OBJECT_GET,
|
|
139
|
-
data=b"",
|
|
140
|
-
atom_id=
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
96
|
+
|
|
97
|
+
provider_key, provider_address, provider_port = decode_object_provider(provider_payload)
|
|
98
|
+
provider_public_key = X25519PublicKey.from_public_bytes(provider_key)
|
|
99
|
+
shared_key_bytes = self.relay_secret_key.exchange(provider_public_key)
|
|
100
|
+
|
|
101
|
+
obj_req = ObjectRequest(
|
|
102
|
+
type=ObjectRequestType.OBJECT_GET,
|
|
103
|
+
data=b"",
|
|
104
|
+
atom_id=atom_id,
|
|
105
|
+
payload_type=payload_type,
|
|
106
|
+
)
|
|
107
|
+
message = Message(
|
|
108
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
109
|
+
content=obj_req.to_bytes(),
|
|
110
|
+
sender=self.relay_public_key,
|
|
111
|
+
)
|
|
147
112
|
message.encrypt(shared_key_bytes)
|
|
148
|
-
self.add_atom_req(
|
|
113
|
+
self.add_atom_req(atom_id, payload_type)
|
|
149
114
|
queued = enqueue_outgoing(
|
|
150
115
|
self,
|
|
151
116
|
(provider_address, provider_port),
|
|
@@ -155,60 +120,166 @@ def storage_get(self, key: bytes) -> Optional[Atom]:
|
|
|
155
120
|
if queued:
|
|
156
121
|
self.logger.debug(
|
|
157
122
|
"Requested atom %s from indexed provider %s:%s",
|
|
158
|
-
|
|
123
|
+
atom_id.hex(),
|
|
159
124
|
provider_address,
|
|
160
125
|
provider_port,
|
|
161
126
|
)
|
|
162
127
|
else:
|
|
163
128
|
self.logger.debug(
|
|
164
129
|
"Dropped request for atom %s to indexed provider %s:%s",
|
|
165
|
-
|
|
130
|
+
atom_id.hex(),
|
|
166
131
|
provider_address,
|
|
167
132
|
provider_port,
|
|
168
133
|
)
|
|
169
|
-
except Exception as exc:
|
|
170
|
-
self.logger.warning("Failed indexed fetch for %s: %s",
|
|
171
|
-
return
|
|
172
|
-
self.logger.warning("Unknown provider id %s for %s", provider_id,
|
|
173
|
-
|
|
174
|
-
self.logger.debug("Falling back to network fetch for %s",
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
except
|
|
206
|
-
self.logger.warning("
|
|
207
|
-
return None
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
self.
|
|
214
|
-
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
self.logger.warning("Failed indexed fetch for %s: %s", atom_id.hex(), exc)
|
|
136
|
+
return _wait_for_payload()
|
|
137
|
+
self.logger.warning("Unknown provider id %s for %s", provider_id, atom_id.hex())
|
|
138
|
+
|
|
139
|
+
self.logger.debug("Falling back to network fetch for %s", atom_id.hex())
|
|
140
|
+
|
|
141
|
+
from ...communication.handlers.object_request import (
|
|
142
|
+
ObjectRequest,
|
|
143
|
+
ObjectRequestType,
|
|
144
|
+
)
|
|
145
|
+
from ...communication.models.message import Message, MessageTopic
|
|
146
|
+
from ...communication.outgoing_queue import enqueue_outgoing
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
closest_peer = self.peer_route.closest_peer_for_hash(atom_id)
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
self.logger.warning("Peer lookup failed for %s: %s", atom_id.hex(), exc)
|
|
152
|
+
return _wait_for_payload()
|
|
153
|
+
|
|
154
|
+
if closest_peer is None or closest_peer.address is None:
|
|
155
|
+
self.logger.debug("No peer available to fetch %s", atom_id.hex())
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
obj_req = ObjectRequest(
|
|
159
|
+
type=ObjectRequestType.OBJECT_GET,
|
|
160
|
+
data=b"",
|
|
161
|
+
atom_id=atom_id,
|
|
162
|
+
payload_type=payload_type,
|
|
163
|
+
)
|
|
164
|
+
try:
|
|
165
|
+
message = Message(
|
|
166
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
167
|
+
content=obj_req.to_bytes(),
|
|
168
|
+
sender=self.relay_public_key,
|
|
169
|
+
)
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
self.logger.warning("Failed to build object request for %s: %s", atom_id.hex(), exc)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
# encrypt the outbound request for the target peer
|
|
175
|
+
message.encrypt(closest_peer.shared_key_bytes)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
self.add_atom_req(atom_id, payload_type)
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
self.logger.warning("Failed to track object request for %s: %s", atom_id.hex(), exc)
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
queued = enqueue_outgoing(
|
|
184
|
+
self,
|
|
185
|
+
closest_peer.address,
|
|
186
|
+
message=message,
|
|
187
|
+
difficulty=closest_peer.difficulty,
|
|
188
|
+
)
|
|
189
|
+
if queued:
|
|
190
|
+
self.logger.debug(
|
|
191
|
+
"Queued OBJECT_GET for %s to peer %s",
|
|
192
|
+
atom_id.hex(),
|
|
193
|
+
closest_peer.address,
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
self.logger.debug(
|
|
197
|
+
"Dropped OBJECT_GET for %s to peer %s",
|
|
198
|
+
atom_id.hex(),
|
|
199
|
+
closest_peer.address,
|
|
200
|
+
)
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
self.logger.warning(
|
|
203
|
+
"Failed to queue OBJECT_GET for %s to %s: %s",
|
|
204
|
+
atom_id.hex(),
|
|
205
|
+
closest_peer.address,
|
|
206
|
+
exc,
|
|
207
|
+
)
|
|
208
|
+
return _wait_for_payload()
|
|
209
|
+
|
|
210
|
+
def get_atom_from_local_storage(self, atom_id: bytes) -> Optional[Atom]:
|
|
211
|
+
"""Retrieve an Atom by checking only local hot and cold storage."""
|
|
212
|
+
self.logger.debug("Fetching atom %s (local only)", atom_id.hex())
|
|
213
|
+
atom = self._hot_storage_get(atom_id)
|
|
214
|
+
if atom is not None:
|
|
215
|
+
self.logger.debug("Returning atom %s from hot storage", atom_id.hex())
|
|
216
|
+
return atom
|
|
217
|
+
atom = self._cold_storage_get(atom_id)
|
|
218
|
+
if atom is not None:
|
|
219
|
+
self.logger.debug("Returning atom %s from cold storage", atom_id.hex())
|
|
220
|
+
return atom
|
|
221
|
+
self.logger.debug("Local storage miss for %s", atom_id.hex())
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_atom(self, atom_id: bytes) -> Optional[Atom]:
|
|
226
|
+
"""Retrieve an atom locally first, then request it from the network."""
|
|
227
|
+
atom = self.get_atom_from_local_storage(atom_id=atom_id)
|
|
228
|
+
if atom is not None:
|
|
229
|
+
return atom
|
|
230
|
+
from ...communication.handlers.object_response import OBJECT_FOUND_ATOM_PAYLOAD
|
|
231
|
+
|
|
232
|
+
result = self._network_get(atom_id, OBJECT_FOUND_ATOM_PAYLOAD)
|
|
233
|
+
if isinstance(result, Atom):
|
|
234
|
+
return result
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_atom_list_from_local_storage(self, root_hash: bytes) -> Optional[List[Atom]]:
|
|
239
|
+
"""Follow a local-only atom list chain, returning atoms or None on gaps."""
|
|
240
|
+
next_id = root_hash
|
|
241
|
+
atoms: List[Atom] = []
|
|
242
|
+
while next_id != ZERO32:
|
|
243
|
+
atom = self.get_atom_from_local_storage(atom_id=next_id)
|
|
244
|
+
if atom is None:
|
|
245
|
+
return None
|
|
246
|
+
atoms.append(atom)
|
|
247
|
+
next_id = atom.next_id
|
|
248
|
+
return atoms
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_atom_list(self, root_hash: bytes) -> Optional[List[Atom]]:
|
|
252
|
+
"""Retrieve an atom list locally first, then request it from the network."""
|
|
253
|
+
atoms = self.get_atom_list_from_local_storage(root_hash=root_hash)
|
|
254
|
+
if atoms is not None:
|
|
255
|
+
return atoms
|
|
256
|
+
from ...communication.handlers.object_response import OBJECT_FOUND_LIST_PAYLOAD
|
|
257
|
+
|
|
258
|
+
result = self._network_get(root_hash, OBJECT_FOUND_LIST_PAYLOAD)
|
|
259
|
+
if isinstance(result, list):
|
|
260
|
+
return result
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _cold_storage_get(self, key: bytes) -> Optional[Atom]:
|
|
265
|
+
"""Read an atom from the cold storage directory if configured."""
|
|
266
|
+
if not self.config["cold_storage_path"]:
|
|
267
|
+
self.logger.debug("Cold storage disabled; cannot fetch %s", key.hex())
|
|
268
|
+
return None
|
|
269
|
+
filename = f"{key.hex().upper()}.bin"
|
|
270
|
+
file_path = Path(self.config["cold_storage_path"]) / filename
|
|
271
|
+
try:
|
|
272
|
+
data = file_path.read_bytes()
|
|
273
|
+
except FileNotFoundError:
|
|
274
|
+
self.logger.debug("Cold storage miss for %s", key.hex())
|
|
275
|
+
return None
|
|
276
|
+
except OSError as exc:
|
|
277
|
+
self.logger.warning("Error reading cold storage file %s: %s", file_path, exc)
|
|
278
|
+
return None
|
|
279
|
+
try:
|
|
280
|
+
atom = Atom.from_bytes(data)
|
|
281
|
+
self.logger.debug("Loaded atom %s from cold storage", key.hex())
|
|
282
|
+
return atom
|
|
283
|
+
except ValueError as exc:
|
|
284
|
+
self.logger.warning("Cold storage data corrupted for %s: %s", file_path, exc)
|
|
285
|
+
return None
|
astreum/storage/models/atom.py
CHANGED
|
@@ -91,17 +91,3 @@ def bytes_list_to_atoms(values: List[bytes]) -> Tuple[bytes, List[Atom]]:
|
|
|
91
91
|
|
|
92
92
|
atoms.reverse()
|
|
93
93
|
return (next_hash if values else ZERO32), atoms
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def get_atom_list_from_storage(self, root_hash: bytes) -> Optional[List["Atom"]]:
|
|
97
|
-
"""Follow the list chain starting at root_hash, returning atoms or None on gaps."""
|
|
98
|
-
next_id: bytes = root_hash
|
|
99
|
-
atom_list: List["Atom"] = []
|
|
100
|
-
while next_id != ZERO32:
|
|
101
|
-
elem = self.storage_get(key=next_id)
|
|
102
|
-
if elem:
|
|
103
|
-
atom_list.append(elem)
|
|
104
|
-
next_id = elem.next_id
|
|
105
|
-
else:
|
|
106
|
-
return None
|
|
107
|
-
return atom_list
|
astreum/storage/models/trie.py
CHANGED
|
@@ -99,7 +99,7 @@ class TrieNode:
|
|
|
99
99
|
if head_hash == ZERO32:
|
|
100
100
|
raise ValueError("empty atom chain for Patricia node")
|
|
101
101
|
|
|
102
|
-
atom_chain = node.
|
|
102
|
+
atom_chain = node.get_atom_list(head_hash)
|
|
103
103
|
if atom_chain is None or len(atom_chain) != 5:
|
|
104
104
|
raise ValueError("malformed Patricia atom chain")
|
|
105
105
|
|
|
@@ -177,7 +177,7 @@ class Trie:
|
|
|
177
177
|
if cached is not None:
|
|
178
178
|
return cached
|
|
179
179
|
|
|
180
|
-
if storage_node.
|
|
180
|
+
if storage_node.get_atom(atom_id=h) is None:
|
|
181
181
|
return None
|
|
182
182
|
|
|
183
183
|
pat_node = TrieNode.from_atoms(storage_node, h)
|
astreum/storage/requests.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from threading import RLock
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import Optional, TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
7
|
from .. import Node
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def add_atom_req(node: "Node", atom_id: bytes) -> None:
|
|
11
|
-
"""Mark an atom request as pending."""
|
|
10
|
+
def add_atom_req(node: "Node", atom_id: bytes, payload_type: Optional[int] = None) -> None:
|
|
11
|
+
"""Mark an atom request as pending with an optional payload type."""
|
|
12
12
|
with node.atom_requests_lock:
|
|
13
|
-
node.atom_requests
|
|
13
|
+
node.atom_requests[atom_id] = payload_type
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def has_atom_req(node: "Node", atom_id: bytes) -> bool:
|
|
@@ -19,10 +19,13 @@ def has_atom_req(node: "Node", atom_id: bytes) -> bool:
|
|
|
19
19
|
return atom_id in node.atom_requests
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def pop_atom_req(node: "Node", atom_id: bytes) ->
|
|
23
|
-
"""Remove the pending request if present
|
|
22
|
+
def pop_atom_req(node: "Node", atom_id: bytes) -> Optional[int]:
|
|
23
|
+
"""Remove the pending request if present and return its payload type."""
|
|
24
24
|
with node.atom_requests_lock:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
return node.atom_requests.pop(atom_id, None)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_atom_req_payload(node: "Node", atom_id: bytes) -> Optional[int]:
|
|
29
|
+
"""Return the payload type for a pending request without removing it."""
|
|
30
|
+
with node.atom_requests_lock:
|
|
31
|
+
return node.atom_requests.get(atom_id)
|
astreum/storage/setup.py
CHANGED
|
@@ -11,13 +11,17 @@ def storage_setup(node: Any, config: dict) -> None:
|
|
|
11
11
|
node.hot_storage = {}
|
|
12
12
|
node.hot_storage_hits = {}
|
|
13
13
|
node.storage_index = {}
|
|
14
|
-
node.storage_providers = []
|
|
15
|
-
node.hot_storage_size = 0
|
|
16
|
-
node.cold_storage_size = 0
|
|
17
|
-
|
|
18
|
-
node.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
config["
|
|
23
|
-
|
|
14
|
+
node.storage_providers = []
|
|
15
|
+
node.hot_storage_size = 0
|
|
16
|
+
node.cold_storage_size = 0
|
|
17
|
+
node.atom_fetch_interval = config["atom_fetch_interval"]
|
|
18
|
+
node.atom_fetch_retries = config["atom_fetch_retries"]
|
|
19
|
+
|
|
20
|
+
node.logger.info(
|
|
21
|
+
"Storage ready (hot_limit=%s bytes, cold_limit=%s bytes, cold_path=%s, atom_fetch_interval=%s, atom_fetch_retries=%s)",
|
|
22
|
+
config["hot_storage_limit"],
|
|
23
|
+
config["cold_storage_limit"],
|
|
24
|
+
config["cold_storage_path"] or "disabled",
|
|
25
|
+
config["atom_fetch_interval"],
|
|
26
|
+
config["atom_fetch_retries"],
|
|
27
|
+
)
|
astreum/utils/config.py
CHANGED
|
@@ -11,6 +11,8 @@ DEFAULT_PEER_TIMEOUT_SECONDS = 15 * 60 # 15 minutes
|
|
|
11
11
|
DEFAULT_PEER_TIMEOUT_INTERVAL_SECONDS = 10 # 10 seconds
|
|
12
12
|
DEFAULT_BOOTSTRAP_RETRY_INTERVAL_SECONDS = 30 # 30 seconds
|
|
13
13
|
DEFAULT_STORAGE_INDEX_INTERVAL_SECONDS = 600 # 10 minutes
|
|
14
|
+
DEFAULT_ATOM_FETCH_INTERVAL_SECONDS = 0.25
|
|
15
|
+
DEFAULT_ATOM_FETCH_RETRIES = 8
|
|
14
16
|
DEFAULT_INCOMING_QUEUE_SIZE_LIMIT_BYTES = 64 * 1024 * 1024 # 64 MiB
|
|
15
17
|
DEFAULT_INCOMING_QUEUE_TIMEOUT_SECONDS = 1.0
|
|
16
18
|
DEFAULT_OUTGOING_QUEUE_SIZE_LIMIT_BYTES = 64 * 1024 * 1024 # 64 MiB
|
|
@@ -189,6 +191,32 @@ def config_setup(config: Dict = {}):
|
|
|
189
191
|
raise ValueError("storage_index_interval must be a positive integer")
|
|
190
192
|
config["storage_index_interval"] = storage_index_interval
|
|
191
193
|
|
|
194
|
+
atom_fetch_interval_raw = config.get(
|
|
195
|
+
"atom_fetch_interval", DEFAULT_ATOM_FETCH_INTERVAL_SECONDS
|
|
196
|
+
)
|
|
197
|
+
try:
|
|
198
|
+
atom_fetch_interval = float(atom_fetch_interval_raw)
|
|
199
|
+
except (TypeError, ValueError) as exc:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"atom_fetch_interval must be a number: {atom_fetch_interval_raw!r}"
|
|
202
|
+
) from exc
|
|
203
|
+
if atom_fetch_interval < 0:
|
|
204
|
+
raise ValueError("atom_fetch_interval must be a non-negative number")
|
|
205
|
+
config["atom_fetch_interval"] = atom_fetch_interval
|
|
206
|
+
|
|
207
|
+
atom_fetch_retries_raw = config.get(
|
|
208
|
+
"atom_fetch_retries", DEFAULT_ATOM_FETCH_RETRIES
|
|
209
|
+
)
|
|
210
|
+
try:
|
|
211
|
+
atom_fetch_retries = int(atom_fetch_retries_raw)
|
|
212
|
+
except (TypeError, ValueError) as exc:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"atom_fetch_retries must be an integer: {atom_fetch_retries_raw!r}"
|
|
215
|
+
) from exc
|
|
216
|
+
if atom_fetch_retries < 0:
|
|
217
|
+
raise ValueError("atom_fetch_retries must be a non-negative integer")
|
|
218
|
+
config["atom_fetch_retries"] = atom_fetch_retries
|
|
219
|
+
|
|
192
220
|
outgoing_queue_limit_raw = config.get(
|
|
193
221
|
"outgoing_queue_size_limit", DEFAULT_OUTGOING_QUEUE_SIZE_LIMIT_BYTES
|
|
194
222
|
)
|
|
@@ -35,7 +35,7 @@ class Account:
|
|
|
35
35
|
@classmethod
|
|
36
36
|
def from_atom(cls, node: Any, root_id: bytes) -> "Account":
|
|
37
37
|
|
|
38
|
-
account_atoms = node.
|
|
38
|
+
account_atoms = node.get_atom_list(root_id)
|
|
39
39
|
|
|
40
40
|
if account_atoms is None or len(account_atoms) != 5:
|
|
41
41
|
raise ValueError("malformed account atom list")
|
|
@@ -204,7 +204,7 @@ class Block:
|
|
|
204
204
|
@classmethod
|
|
205
205
|
def from_atom(cls, node: Any, block_id: bytes) -> "Block":
|
|
206
206
|
|
|
207
|
-
block_header = node.
|
|
207
|
+
block_header = node.get_atom_list(block_id)
|
|
208
208
|
if block_header is None or len(block_header) != 4:
|
|
209
209
|
raise ValueError("malformed block atom chain")
|
|
210
210
|
type_atom, version_atom, sig_atom, body_list_atom = block_header
|
|
@@ -223,7 +223,7 @@ class Block:
|
|
|
223
223
|
if body_list_atom.next_id != ZERO32:
|
|
224
224
|
raise ValueError("malformed block (body list tail)")
|
|
225
225
|
|
|
226
|
-
detail_atoms = node.
|
|
226
|
+
detail_atoms = node.get_atom_list(body_list_atom.data)
|
|
227
227
|
if detail_atoms is None:
|
|
228
228
|
raise ValueError("missing block body list nodes")
|
|
229
229
|
|
|
@@ -304,7 +304,7 @@ class Block:
|
|
|
304
304
|
def _load_hash_list(head: bytes) -> Optional[List[bytes]]:
|
|
305
305
|
if head == ZERO32:
|
|
306
306
|
return []
|
|
307
|
-
atoms = node.
|
|
307
|
+
atoms = node.get_atom_list(head)
|
|
308
308
|
if atoms is None:
|
|
309
309
|
_log_warning("Block verify missing list atoms head=%s block=%s", _hex(head), _hex(self.atom_hash))
|
|
310
310
|
return None
|
|
@@ -73,7 +73,7 @@ class Receipt:
|
|
|
73
73
|
|
|
74
74
|
@classmethod
|
|
75
75
|
def from_atom(cls, node: Any, receipt_id: bytes) -> Receipt:
|
|
76
|
-
atom_chain = node.
|
|
76
|
+
atom_chain = node.get_atom_list(receipt_id)
|
|
77
77
|
if atom_chain is None or len(atom_chain) != 6:
|
|
78
78
|
raise ValueError("malformed receipt atom chain")
|
|
79
79
|
|
|
@@ -78,9 +78,9 @@ class Transaction:
|
|
|
78
78
|
node: Any,
|
|
79
79
|
transaction_id: bytes,
|
|
80
80
|
) -> Transaction:
|
|
81
|
-
|
|
82
|
-
if not callable(
|
|
83
|
-
raise NotImplementedError("node does not expose
|
|
81
|
+
get_atom = getattr(node, "get_atom", None)
|
|
82
|
+
if not callable(get_atom):
|
|
83
|
+
raise NotImplementedError("node does not expose an atom getter")
|
|
84
84
|
|
|
85
85
|
def _atom_kind(atom: Optional[Atom]) -> Optional[AtomKind]:
|
|
86
86
|
kind_value = getattr(atom, "kind", None)
|
|
@@ -100,7 +100,7 @@ class Transaction:
|
|
|
100
100
|
) -> Atom:
|
|
101
101
|
if not atom_id or atom_id == ZERO32:
|
|
102
102
|
raise ValueError(f"missing {context}")
|
|
103
|
-
atom =
|
|
103
|
+
atom = get_atom(atom_id)
|
|
104
104
|
if atom is None:
|
|
105
105
|
raise ValueError(f"missing {context}")
|
|
106
106
|
if expected_kind is not None:
|
|
@@ -127,7 +127,7 @@ class Transaction:
|
|
|
127
127
|
if body_list_atom.next_id and body_list_atom.next_id != ZERO32:
|
|
128
128
|
raise ValueError("malformed transaction (body list tail)")
|
|
129
129
|
|
|
130
|
-
detail_atoms = node.
|
|
130
|
+
detail_atoms = node.get_atom_list(body_list_atom.data)
|
|
131
131
|
if detail_atoms is None:
|
|
132
132
|
raise ValueError("missing transaction body list nodes")
|
|
133
133
|
if len(detail_atoms) != 6:
|
|
@@ -167,7 +167,7 @@ class Transaction:
|
|
|
167
167
|
transaction_id: bytes,
|
|
168
168
|
) -> Optional[List[Atom]]:
|
|
169
169
|
"""Load the transaction atom chain from storage, returning the atoms or None."""
|
|
170
|
-
atoms = node.
|
|
170
|
+
atoms = node.get_atom_list(transaction_id)
|
|
171
171
|
if atoms is None or len(atoms) < 4:
|
|
172
172
|
return None
|
|
173
173
|
type_atom = atoms[0]
|
|
@@ -178,7 +178,7 @@ class Transaction:
|
|
|
178
178
|
return None
|
|
179
179
|
|
|
180
180
|
body_list_atom = atoms[-1]
|
|
181
|
-
detail_atoms = node.
|
|
181
|
+
detail_atoms = node.get_atom_list(body_list_atom.data)
|
|
182
182
|
if detail_atoms is None:
|
|
183
183
|
return None
|
|
184
184
|
atoms.extend(detail_atoms)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.48
|
|
4
4
|
Summary: Python library to interact with the Astreum blockchain and its virtual machine.
|
|
5
5
|
Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
|
|
6
6
|
Project-URL: Homepage, https://github.com/astreum/lib-py
|
|
@@ -33,6 +33,8 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
|
|
|
33
33
|
| `hot_storage_limit` | int | `1073741824` | Maximum bytes kept in the hot cache before new atoms are skipped (1 GiB). |
|
|
34
34
|
| `cold_storage_limit` | int | `10737418240` | Cold storage write threshold (10 GiB by default); set to `0` to skip the limit. |
|
|
35
35
|
| `cold_storage_path` | string | `None` | Directory where persisted atoms live; Astreum creates it on startup and skips cold storage when unset. |
|
|
36
|
+
| `atom_fetch_interval` | float | `0.25` | Poll interval (seconds) while waiting for missing atoms in `get_atom_list_from_storage`; `0` disables waiting. |
|
|
37
|
+
| `atom_fetch_retries` | int | `8` | Number of poll attempts for missing atoms; max wait is roughly `interval * retries`, `0` disables waiting. |
|
|
36
38
|
| `logging_retention_days` | int | `90` | Number of days to keep rotated log files (daily gzip). |
|
|
37
39
|
| `chain_id` | int | `0` | Chain identifier used for validation (0 = test, 1 = main). |
|
|
38
40
|
| `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
astreum/__init__.py,sha256=ibzwB_Rq3mBCgzFBVx7ssHo7-MFxpiayn5cHMIZ3Gd4,351
|
|
2
|
-
astreum/node.py,sha256=
|
|
2
|
+
astreum/node.py,sha256=6K6XeAd6h9b9LYtXE9MeJ2med0eOMZbEU5I1_RoLt30,3174
|
|
3
3
|
astreum/communication/__init__.py,sha256=E0-UtzXwyX6svdbL52fI3tnUY8ILmQJ6rqw3qX2YZi0,357
|
|
4
4
|
astreum/communication/difficulty.py,sha256=XUw3xfppecVy_kBsaOXBCcxICz4d3qwKvu8L4rXNtyY,886
|
|
5
5
|
astreum/communication/disconnect.py,sha256=m5rwR_TJxBk4KWAUtF-qcikssVj2u-6zse1TTYZquj4,1613
|
|
@@ -7,12 +7,12 @@ astreum/communication/incoming_queue.py,sha256=ccKmjSmkMt8Kvi7XS8Mr-iI-swJSWxjmL
|
|
|
7
7
|
astreum/communication/message_pow.py,sha256=diDxc2aXjTxBQw5GWK5b-8Gybxa8AWSAcGmxF-tODnI,1103
|
|
8
8
|
astreum/communication/node.py,sha256=GrcPRffWEv74W_gezYRnKVvGHBblmGEsn5Wvtw8pxC8,1700
|
|
9
9
|
astreum/communication/outgoing_queue.py,sha256=CDA-A9vrkNmK-Gm4wS5htnhQhFaLaejW8dKNTsFDSNo,3818
|
|
10
|
-
astreum/communication/setup.py,sha256=
|
|
10
|
+
astreum/communication/setup.py,sha256=I1uZDxb5qyPVZ6XNi_tngtgF1CXGV4otxdNoUeRoKHs,11911
|
|
11
11
|
astreum/communication/util.py,sha256=tDF6TNP-u7Q7K96JhnuWHEwfq4pASoYYcF5MakBicrg,1942
|
|
12
12
|
astreum/communication/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
astreum/communication/handlers/handshake.py,sha256=XGeHslDhDn0zJS-DvvcxHwrPzmIhTMpunKOjGc8F2pg,3525
|
|
14
|
-
astreum/communication/handlers/object_request.py,sha256=
|
|
15
|
-
astreum/communication/handlers/object_response.py,sha256=
|
|
14
|
+
astreum/communication/handlers/object_request.py,sha256=Wik_jEA9GiwuIaliUYThGvCLojiC2CbdGu0DfULAUa4,10791
|
|
15
|
+
astreum/communication/handlers/object_response.py,sha256=nDHn81GAWMFe6iOhw4NyR0ZQxGxfr0ehZ7HcISWdtZA,7671
|
|
16
16
|
astreum/communication/handlers/ping.py,sha256=bbB8JQfLR0oYgdharx0xarsn7YIaCdxlZb-AdmLw99g,1345
|
|
17
17
|
astreum/communication/handlers/route_request.py,sha256=lZgapJH0RfLgp9c14H864BU3K3t03WHRGLcgpeu7SEg,2584
|
|
18
18
|
astreum/communication/handlers/route_response.py,sha256=ktEft9XevZYVp6rZNCrbFIZ-HZTlYZLIUZcdRR4vkU8,1857
|
|
@@ -36,22 +36,22 @@ astreum/machine/parser.py,sha256=Z_Y0Sax0rPh8JcIo19-iNDQoc5GTdGQkmfFyLpCB4bw,175
|
|
|
36
36
|
astreum/machine/tokenizer.py,sha256=6wPqR_D3h5BEvR78XKtD45ouy77RZBbz4Yh4jHSmN4o,2394
|
|
37
37
|
astreum/machine/evaluations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
38
|
astreum/machine/evaluations/high_evaluation.py,sha256=cqYudR9WAdVz9dURDyuQhZsuhWbmjbdw9x3UxDEYpPI,9971
|
|
39
|
-
astreum/machine/evaluations/low_evaluation.py,sha256=
|
|
39
|
+
astreum/machine/evaluations/low_evaluation.py,sha256=t3xfZCKrvRMBTmc4PUp8tywr2uIOScgQnWaR6eMG3wE,10370
|
|
40
40
|
astreum/machine/evaluations/script_evaluation.py,sha256=eWouYUwTYzaqUyXqEe-lAJFIluW0gMeCDdXqle88oWw,864
|
|
41
41
|
astreum/machine/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
42
|
astreum/machine/models/environment.py,sha256=WjP6GRX_8e0-BAhzRLvQ6fYtKQEVR0LZi7DZNZS0TSE,1019
|
|
43
|
-
astreum/machine/models/expression.py,sha256=
|
|
43
|
+
astreum/machine/models/expression.py,sha256=KjN6TxikqpRK_vwx5f4N4RNZCqoKYBdR6rGBBJp2-Bk,7637
|
|
44
44
|
astreum/machine/models/meter.py,sha256=5q2PFW7_jmgKVM1-vwE4RRjMfPEthUA4iu1CwR-Axws,505
|
|
45
45
|
astreum/storage/__init__.py,sha256=Flk6WXT2xGFHWWJiZHK3O5OpjoLTOFMqqIiJTtD58kY,111
|
|
46
46
|
astreum/storage/providers.py,sha256=-nOEfoecraTBhPA3ERgz8UwEOJ9DKD-wnHtaSoR6WjU,740
|
|
47
|
-
astreum/storage/requests.py,sha256=
|
|
48
|
-
astreum/storage/setup.py,sha256=
|
|
49
|
-
astreum/storage/actions/get.py,sha256=
|
|
47
|
+
astreum/storage/requests.py,sha256=5D9F2uWdwqhvfPM3TWrtCMopseFyPV6WKNKbxT5uLlY,1067
|
|
48
|
+
astreum/storage/setup.py,sha256=t6EtAan9N7wrTEcU927z-sM5L5mboMnKk218fwfTRy8,893
|
|
49
|
+
astreum/storage/actions/get.py,sha256=2yA00Odjefzf2v5Qpwl6H0LSG8sb7KLmfbxkA9Fnivo,11088
|
|
50
50
|
astreum/storage/actions/set.py,sha256=TGD1JS9zRLO7AVDTWwP2st7DWabfB51trVAghUrja4A,6386
|
|
51
|
-
astreum/storage/models/atom.py,sha256=
|
|
52
|
-
astreum/storage/models/trie.py,sha256=
|
|
51
|
+
astreum/storage/models/atom.py,sha256=fAIXW7bMzsyioZL4UOyu_Rpjvw2amWNQNbyTE3m56sk,2707
|
|
52
|
+
astreum/storage/models/trie.py,sha256=kZelNuMTGKnG21Rt4Fo72bp4d1P_5W8zAH7hWk4zN1k,17577
|
|
53
53
|
astreum/utils/bytes.py,sha256=9QTWC2JCdwWLB5R2mPtmjPro0IUzE58DL3uEul4AheE,846
|
|
54
|
-
astreum/utils/config.py,sha256=
|
|
54
|
+
astreum/utils/config.py,sha256=b2ei_LtPxnQs__dJvBnNfjQiQjnLOJM0-EGrO_1PEYw,11256
|
|
55
55
|
astreum/utils/integer.py,sha256=iQt-klWOYVghu_NOT341MmHbOle4FDT3by4PNKNXscg,736
|
|
56
56
|
astreum/utils/logging.py,sha256=YbFtt_h6_3mpnOomffGW0DnI1Y1cwl_vb6Zsu1_W_Xg,6692
|
|
57
57
|
astreum/validation/__init__.py,sha256=cRlrwE3MqtBrda9ZxLmtCEOY3P5oJnXpjE4YDNHCnpI,322
|
|
@@ -60,20 +60,20 @@ astreum/validation/genesis.py,sha256=7JSZEa5-AdaWn2sCO0G2bh8-4OG-mio585U1lJ9ZjWk
|
|
|
60
60
|
astreum/validation/node.py,sha256=AuY186eqlmXMIbFnQrDO8tn6gmuaj-sI6l1htIwoxnA,6935
|
|
61
61
|
astreum/validation/validator.py,sha256=MmxkOMWQeUg4peIt5FHz1A1Fv-RvbGNCa49sDj8b7QM,3873
|
|
62
62
|
astreum/validation/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
|
-
astreum/validation/models/account.py,sha256=
|
|
63
|
+
astreum/validation/models/account.py,sha256=WRRaNHqQuoc0hBrDmPF2gZvQCl5557QX1FOYDOmpkqE,2666
|
|
64
64
|
astreum/validation/models/accounts.py,sha256=iUMs6LvmMea-gxd6-ujkFjqhWmuW1cl9XTWGXQkpLys,2388
|
|
65
|
-
astreum/validation/models/block.py,sha256=
|
|
65
|
+
astreum/validation/models/block.py,sha256=SfjKO3mez67vHHxOdpsJn1WihKlBou0RsBzl7KiOnqI,21081
|
|
66
66
|
astreum/validation/models/fork.py,sha256=R9yBKThlvAzqGf4nAe5yVyFDX7tIX5TfnH_KDEZ7Azg,20630
|
|
67
|
-
astreum/validation/models/receipt.py,sha256=
|
|
68
|
-
astreum/validation/models/transaction.py,sha256=
|
|
67
|
+
astreum/validation/models/receipt.py,sha256=IRDrAEApdMIuBnRUuvueZ0X45Fq2iEvl40sbW3u1mso,3883
|
|
68
|
+
astreum/validation/models/transaction.py,sha256=TKylwk3AqWW6pcXFAowR7A4BT43nQxPRQ-NseE3ELtQ,8825
|
|
69
69
|
astreum/validation/workers/__init__.py,sha256=GH8G4j7ONbtcoqBiX1d1I16Ikiu3fjGM6pUSQXqDtiw,228
|
|
70
70
|
astreum/validation/workers/validation.py,sha256=60L5KiX6e5-5KN4NLAF9jclfyRqDX_ju4qOTx92PnY4,15349
|
|
71
71
|
astreum/verification/__init__.py,sha256=Ec7_CTXbHYtiw1KK3oJx0s96loSnVX0i863_FLHv_es,130
|
|
72
72
|
astreum/verification/discover.py,sha256=ubMdNTE8gzDQ9B8NzycrHpKVHfnqaBQgNkEHywoVjws,2449
|
|
73
73
|
astreum/verification/node.py,sha256=xYhVRhRW_wKIdFiWzrC5A-xeHy1P6cRJwG5MWRA8KTM,2005
|
|
74
74
|
astreum/verification/worker.py,sha256=BM8feAJ0IVKyHYzdC3YRZwEItOxUhQdmtu2RtksIMvI,6460
|
|
75
|
-
astreum-0.3.
|
|
76
|
-
astreum-0.3.
|
|
77
|
-
astreum-0.3.
|
|
78
|
-
astreum-0.3.
|
|
79
|
-
astreum-0.3.
|
|
75
|
+
astreum-0.3.48.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
|
|
76
|
+
astreum-0.3.48.dist-info/METADATA,sha256=pR6MmP4QXroddMYtP9BHnOaVA1C_glrH5fY42waippM,10987
|
|
77
|
+
astreum-0.3.48.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
78
|
+
astreum-0.3.48.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
|
|
79
|
+
astreum-0.3.48.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|