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.
@@ -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 ObjectResponse, ObjectResponseType
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
- def __init__(self, type: ObjectRequestType, data: bytes, atom_id: bytes = None):
28
- self.type = type
29
- self.data = data
30
- self.atom_id = atom_id
31
-
32
- def to_bytes(self):
33
- return bytes([self.type.value]) + self.atom_id + self.data
34
-
35
- @classmethod
36
- def from_bytes(cls, data: bytes) -> "ObjectRequest":
37
- # need at least 1 byte for type + 32 bytes for hash
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 = data[33:]
49
- return cls(req_type, payload, atom_id_bytes)
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
- local_atom = node.local_get(atom_id)
83
- if local_atom is not None:
84
- node.logger.debug("Object %s found locally; returning to %s", atom_id.hex(), peer.address)
85
- resp = ObjectResponse(
86
- type=ObjectResponseType.OBJECT_FOUND,
87
- data=local_atom.to_bytes(),
88
- atom_id=atom_id
89
- )
90
- obj_res_msg = Message(
91
- topic=MessageTopic.OBJECT_RESPONSE,
92
- body=resp.to_bytes(),
93
- sender=node.relay_public_key,
94
- )
95
- obj_res_msg.encrypt(peer.shared_key_bytes)
96
- enqueue_outgoing(
97
- node,
98
- peer.address,
99
- message=obj_res_msg,
100
- difficulty=peer.difficulty,
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
- atom = Atom.from_bytes(object_response.data)
81
- atom_id = atom.object_id()
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 atom ID mismatch (expected=%s got=%s)",
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(
@@ -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 = set()
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.storage_get(key=id_b)
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.storage_get(key=id_b)
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.storage_get(key=id1_b)
199
- atom2 = self.storage_get(key=id2_b)
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.storage_get(key=id_b)
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
- storage_get = getattr(node, "storage_get", None)
50
- if not callable(storage_get):
51
- raise TypeError("node must provide a callable 'storage_get'")
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 = storage_get(atom_id)
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.get_atom_list_from_storage(root_hash=key)
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.models.atom import get_atom_list_from_storage
24
- from astreum.storage.actions.get import (
25
- _hot_storage_get,
26
- _cold_storage_get,
27
- _network_get,
28
- storage_get,
29
- local_get,
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
- storage_get = storage_get
90
- local_get = local_get
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
@@ -1,127 +1,91 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from typing import Optional
5
-
6
- from ..models.atom import Atom
7
- from ..providers import provider_payload_for_id
8
-
9
-
10
- def _hot_storage_get(self, key: bytes) -> Optional[Atom]:
11
- """Retrieve an atom from in-memory cache while tracking hit statistics."""
12
- atom = self.hot_storage.get(key)
13
- if atom is not None:
14
- self.hot_storage_hits[key] = self.hot_storage_hits.get(key, 0) + 1
15
- self.logger.debug("Hot storage hit for %s", key.hex())
16
- else:
17
- self.logger.debug("Hot storage miss for %s", key.hex())
18
- return atom
19
-
20
-
21
- def _network_get(self, key: bytes) -> Optional[Atom]:
22
- """Attempt to fetch an atom from network peers when local storage misses."""
23
- if not getattr(self, "is_connected", False):
24
- self.logger.debug("Network fetch skipped for %s; node not connected", key.hex())
25
- return None
26
- self.logger.debug("Attempting network fetch for %s", key.hex())
27
- try:
28
- from ...communication.handlers.object_request import (
29
- ObjectRequest,
30
- ObjectRequestType,
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
- from ...communication.models.message import Message, MessageTopic
33
- from ...communication.outgoing_queue import enqueue_outgoing
34
- except Exception as exc:
35
- self.logger.warning(
36
- "Communication module unavailable; cannot fetch %s: %s",
37
- key.hex(),
38
- exc,
39
- )
40
- return None
41
-
42
- try:
43
- closest_peer = self.peer_route.closest_peer_for_hash(key)
44
- except Exception as exc:
45
- self.logger.warning("Peer lookup failed for %s: %s", key.hex(), exc)
46
- return None
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
- if queued:
83
- self.logger.debug(
84
- "Queued OBJECT_GET for %s to peer %s",
85
- key.hex(),
86
- closest_peer.address,
87
- )
88
- else:
89
- self.logger.debug(
90
- "Dropped OBJECT_GET for %s to peer %s",
91
- key.hex(),
92
- closest_peer.address,
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=key,
141
- )
142
- message = Message(
143
- topic=MessageTopic.OBJECT_REQUEST,
144
- content=obj_req.to_bytes(),
145
- sender=self.relay_public_key,
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(key)
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
- key.hex(),
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
- key.hex(),
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", key.hex(), exc)
171
- return None
172
- self.logger.warning("Unknown provider id %s for %s", provider_id, key.hex())
173
-
174
- self.logger.debug("Falling back to network fetch for %s", key.hex())
175
- return self._network_get(key)
176
-
177
-
178
- def local_get(self, key: bytes) -> Optional[Atom]:
179
- """Retrieve an Atom by checking only local hot and cold storage."""
180
- self.logger.debug("Fetching atom %s (local only)", key.hex())
181
- atom = self._hot_storage_get(key)
182
- if atom is not None:
183
- self.logger.debug("Returning atom %s from hot storage", key.hex())
184
- return atom
185
- atom = self._cold_storage_get(key)
186
- if atom is not None:
187
- self.logger.debug("Returning atom %s from cold storage", key.hex())
188
- return atom
189
- self.logger.debug("Local storage miss for %s", key.hex())
190
- return None
191
-
192
-
193
- def _cold_storage_get(self, key: bytes) -> Optional[Atom]:
194
- """Read an atom from the cold storage directory if configured."""
195
- if not self.config["cold_storage_path"]:
196
- self.logger.debug("Cold storage disabled; cannot fetch %s", key.hex())
197
- return None
198
- filename = f"{key.hex().upper()}.bin"
199
- file_path = Path(self.config["cold_storage_path"]) / filename
200
- try:
201
- data = file_path.read_bytes()
202
- except FileNotFoundError:
203
- self.logger.debug("Cold storage miss for %s", key.hex())
204
- return None
205
- except OSError as exc:
206
- self.logger.warning("Error reading cold storage file %s: %s", file_path, exc)
207
- return None
208
- try:
209
- atom = Atom.from_bytes(data)
210
- self.logger.debug("Loaded atom %s from cold storage", key.hex())
211
- return atom
212
- except ValueError as exc:
213
- self.logger.warning("Cold storage data corrupted for %s: %s", file_path, exc)
214
- return None
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
@@ -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
@@ -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.get_atom_list_from_storage(head_hash)
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.storage_get(h) is None:
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)
@@ -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.add(atom_id)
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) -> bool:
23
- """Remove the pending request if present. Returns True when removed."""
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
- if atom_id in node.atom_requests:
26
- node.atom_requests.remove(atom_id)
27
- return True
28
- return False
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.logger.info(
19
- "Storage ready (hot_limit=%s bytes, cold_limit=%s bytes, cold_path=%s)",
20
- config["hot_storage_limit"],
21
- config["cold_storage_limit"],
22
- config["cold_storage_path"] or "disabled",
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.get_atom_list_from_storage(root_hash=root_id)
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.get_atom_list_from_storage(block_id)
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.get_atom_list_from_storage(body_list_atom.data)
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.get_atom_list_from_storage(head)
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.get_atom_list_from_storage(receipt_id)
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
- storage_get = getattr(node, "storage_get", None)
82
- if not callable(storage_get):
83
- raise NotImplementedError("node does not expose a storage getter")
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 = storage_get(atom_id)
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.get_atom_list_from_storage(body_list_atom.data)
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.get_atom_list_from_storage(transaction_id)
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.get_atom_list_from_storage(body_list_atom.data)
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.46
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=vqcS5gFXyy2l1WW6gdGSElo6h3QeyizfwJFpIGlZTAs,3104
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=Rc6jjA9_HD68ED5NI29MFfEaN9ZcTBLs6ng__25PVRE,11915
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=dpRZ-CAZ6ahEKErKXXE__k8OaPYIkWpDLVveI9SU3GQ,8376
15
- astreum/communication/handlers/object_response.py,sha256=p4ixSMsnsGfeHudIrvbuc8Nx5aa5orLFtgll0p9wtvc,4092
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=_93r6DKkCwnaOKmVGSp8JBlUPZpKrA1GECqVnwLb9es,10370
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=yYr9ktk-NWPL4EXwHz7ePvr9eNvfSBQe3yzRUz06yas,7675
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=q_rxG_k7POth93HmsUTCLSyNw-4EdFqqNExhIwm7Q0g,818
48
- astreum/storage/setup.py,sha256=lG6_-zDx_UvqUcEh8o287Zp9szovYHVwBK4x_Np1BXQ,659
49
- astreum/storage/actions/get.py,sha256=xuM5Q2oqEIKu0myC0-ayCBdSAD1ouc9LwdCiIvs5xv4,8099
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=FY_bgtoju59Yo7TL1DTFTr9_pRMNBuH6-u59D6bz2fc,3163
52
- astreum/storage/models/trie.py,sha256=Bn3ssPGI7YGS4iUH5ESvpG1NE6Ljx2Xo7wkEpQhjKUY,17587
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=tcIBLPvwhxwca0SqbMe4fmXIG38OEPrBC4-Ovg2C9ts,10158
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=3QcT59QUZynysLSbiywidFYVzYJ3LR6qia7JwXOwn4I,2690
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=KeQLAWOmrcAuqs9vxmGilTC-XGhUrh27VOErZ0G-7Oc,21120
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=84foS48tOWpHYqzsR4OM34ZPzxpr_DiKShBPz684SRs,3896
68
- astreum/validation/models/transaction.py,sha256=HIcqWm9iFHz-w9aCYprbP0rx8skcmw36G_8x_gkuuL4,8878
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.46.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
76
- astreum-0.3.46.dist-info/METADATA,sha256=HH9jPPWkYUzF74hWJMFtDZlSPW4Sc9yzpW4p6iAtQ8w,10503
77
- astreum-0.3.46.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
- astreum-0.3.46.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
79
- astreum-0.3.46.dist-info/RECORD,,
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,,