astreum 0.2.61__py3-none-any.whl → 0.3.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +89 -0
  4. astreum/communication/handlers/object_request.py +176 -0
  5. astreum/communication/handlers/object_response.py +115 -0
  6. astreum/communication/handlers/ping.py +34 -0
  7. astreum/communication/handlers/route_request.py +76 -0
  8. astreum/communication/handlers/route_response.py +53 -0
  9. astreum/communication/models/__init__.py +0 -0
  10. astreum/communication/models/message.py +124 -0
  11. astreum/communication/models/peer.py +51 -0
  12. astreum/{_communication → communication/models}/route.py +7 -12
  13. astreum/communication/processors/__init__.py +0 -0
  14. astreum/communication/processors/incoming.py +98 -0
  15. astreum/communication/processors/outgoing.py +20 -0
  16. astreum/communication/setup.py +166 -0
  17. astreum/communication/start.py +37 -0
  18. astreum/{_communication → communication}/util.py +7 -0
  19. astreum/consensus/__init__.py +20 -0
  20. astreum/consensus/genesis.py +66 -0
  21. astreum/consensus/models/__init__.py +0 -0
  22. astreum/consensus/models/account.py +84 -0
  23. astreum/consensus/models/accounts.py +72 -0
  24. astreum/consensus/models/block.py +364 -0
  25. astreum/{_consensus → consensus/models}/chain.py +7 -7
  26. astreum/{_consensus → consensus/models}/fork.py +8 -8
  27. astreum/consensus/models/receipt.py +98 -0
  28. astreum/{_consensus → consensus/models}/transaction.py +76 -78
  29. astreum/{_consensus → consensus}/setup.py +18 -50
  30. astreum/consensus/start.py +67 -0
  31. astreum/consensus/validator.py +95 -0
  32. astreum/{_consensus → consensus}/workers/discovery.py +19 -1
  33. astreum/consensus/workers/validation.py +307 -0
  34. astreum/{_consensus → consensus}/workers/verify.py +29 -2
  35. astreum/crypto/chacha20poly1305.py +74 -0
  36. astreum/machine/__init__.py +20 -0
  37. astreum/machine/evaluations/__init__.py +0 -0
  38. astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
  39. astreum/machine/evaluations/low_evaluation.py +281 -0
  40. astreum/machine/evaluations/script_evaluation.py +27 -0
  41. astreum/machine/models/__init__.py +0 -0
  42. astreum/machine/models/environment.py +31 -0
  43. astreum/{_lispeum → machine/models}/expression.py +36 -8
  44. astreum/machine/tokenizer.py +90 -0
  45. astreum/node.py +78 -767
  46. astreum/storage/__init__.py +7 -0
  47. astreum/storage/actions/get.py +183 -0
  48. astreum/storage/actions/set.py +178 -0
  49. astreum/{_storage → storage/models}/atom.py +55 -57
  50. astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
  51. astreum/storage/requests.py +28 -0
  52. astreum/storage/setup.py +22 -15
  53. astreum/utils/config.py +48 -0
  54. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
  55. astreum-0.3.9.dist-info/RECORD +71 -0
  56. astreum/_communication/message.py +0 -101
  57. astreum/_communication/peer.py +0 -23
  58. astreum/_communication/setup.py +0 -322
  59. astreum/_consensus/__init__.py +0 -20
  60. astreum/_consensus/account.py +0 -95
  61. astreum/_consensus/accounts.py +0 -38
  62. astreum/_consensus/block.py +0 -311
  63. astreum/_consensus/genesis.py +0 -72
  64. astreum/_consensus/receipt.py +0 -136
  65. astreum/_consensus/workers/validation.py +0 -125
  66. astreum/_lispeum/__init__.py +0 -16
  67. astreum/_lispeum/environment.py +0 -13
  68. astreum/_lispeum/low_evaluation.py +0 -123
  69. astreum/_lispeum/tokenizer.py +0 -22
  70. astreum/_node.py +0 -198
  71. astreum/_storage/__init__.py +0 -7
  72. astreum/_storage/setup.py +0 -35
  73. astreum/format.py +0 -75
  74. astreum/models/block.py +0 -441
  75. astreum/models/merkle.py +0 -205
  76. astreum/models/patricia.py +0 -393
  77. astreum/storage/object.py +0 -68
  78. astreum-0.2.61.dist-info/RECORD +0 -57
  79. /astreum/{models → communication/handlers}/__init__.py +0 -0
  80. /astreum/{_communication → communication/models}/ping.py +0 -0
  81. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  82. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  83. /astreum/{_lispeum → machine}/parser.py +0 -0
  84. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
  85. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
  86. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,7 @@
1
+ from .models.atom import Atom
2
+ from .setup import storage_setup
3
+
4
+ __all__ = [
5
+ "Atom",
6
+ "storage_setup",
7
+ ]
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from ..models.atom import Atom
7
+
8
+
9
+ def _hot_storage_get(self, key: bytes) -> Optional[Atom]:
10
+ """Retrieve an atom from in-memory cache while tracking hit statistics."""
11
+ atom = self.hot_storage.get(key)
12
+ if atom is not None:
13
+ self.hot_storage_hits[key] = self.hot_storage_hits.get(key, 0) + 1
14
+ self.logger.debug("Hot storage hit for %s", key.hex())
15
+ else:
16
+ self.logger.debug("Hot storage miss for %s", key.hex())
17
+ return atom
18
+
19
+
20
+ def _network_get(self, key: bytes) -> Optional[Atom]:
21
+ """Attempt to fetch an atom from network peers when local storage misses."""
22
+ if not getattr(self, "is_connected", False):
23
+ self.logger.debug("Network fetch skipped for %s; node not connected", key.hex())
24
+ return None
25
+ self.logger.debug("Attempting network fetch for %s", key.hex())
26
+ try:
27
+ from ...communication.handlers.object_request import (
28
+ ObjectRequest,
29
+ ObjectRequestType,
30
+ )
31
+ from ...communication.models.message import Message, MessageTopic
32
+ except Exception as exc:
33
+ self.logger.warning(
34
+ "Communication module unavailable; cannot fetch %s: %s",
35
+ key.hex(),
36
+ exc,
37
+ )
38
+ return None
39
+
40
+ try:
41
+ closest_peer = self.peer_route.closest_peer_for_hash(key)
42
+ except Exception as exc:
43
+ self.logger.warning("Peer lookup failed for %s: %s", key.hex(), exc)
44
+ return None
45
+
46
+ if closest_peer is None or closest_peer.address is None:
47
+ self.logger.debug("No peer available to fetch %s", key.hex())
48
+ return None
49
+
50
+ obj_req = ObjectRequest(
51
+ type=ObjectRequestType.OBJECT_GET,
52
+ data=b"",
53
+ atom_id=key,
54
+ )
55
+ try:
56
+ message = Message(
57
+ topic=MessageTopic.OBJECT_REQUEST,
58
+ content=obj_req.to_bytes(),
59
+ sender=self.relay_public_key,
60
+ )
61
+ except Exception as exc:
62
+ self.logger.warning("Failed to build object request for %s: %s", key.hex(), exc)
63
+ return None
64
+
65
+ # encrypt the outbound request for the target peer
66
+ message.encrypt(closest_peer.shared_key_bytes)
67
+
68
+ try:
69
+ self.add_atom_req(key)
70
+ except Exception as exc:
71
+ self.logger.warning("Failed to track object request for %s: %s", key.hex(), exc)
72
+
73
+ try:
74
+ self.outgoing_queue.put((message.to_bytes(), closest_peer.address))
75
+ self.logger.debug(
76
+ "Queued OBJECT_GET for %s to peer %s",
77
+ key.hex(),
78
+ closest_peer.address,
79
+ )
80
+ except Exception as exc:
81
+ self.logger.warning(
82
+ "Failed to queue OBJECT_GET for %s to %s: %s",
83
+ key.hex(),
84
+ closest_peer.address,
85
+ exc,
86
+ )
87
+ return None
88
+
89
+
90
+ def storage_get(self, key: bytes) -> Optional[Atom]:
91
+ """Retrieve an Atom by checking local storage first, then the network."""
92
+ self.logger.debug("Fetching atom %s", key.hex())
93
+ atom = self._hot_storage_get(key)
94
+ if atom is not None:
95
+ self.logger.debug("Returning atom %s from hot storage", key.hex())
96
+ return atom
97
+ atom = self._cold_storage_get(key)
98
+ if atom is not None:
99
+ self.logger.debug("Returning atom %s from cold storage", key.hex())
100
+ return atom
101
+
102
+ if not self.is_connected:
103
+ return None
104
+
105
+ provider_payload = self.storage_index.get(key)
106
+ if provider_payload is not None:
107
+ try:
108
+ from ...communication.handlers.object_response import decode_object_provider
109
+ from ...communication.handlers.object_request import (
110
+ ObjectRequest,
111
+ ObjectRequestType,
112
+ )
113
+ from ...communication.models.message import Message, MessageTopic
114
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
115
+
116
+ provider_key, provider_address, provider_port = decode_object_provider(provider_payload)
117
+ provider_public_key = X25519PublicKey.from_public_bytes(provider_key)
118
+ shared_key_bytes = self.relay_secret_key.exchange(provider_public_key)
119
+
120
+ obj_req = ObjectRequest(
121
+ type=ObjectRequestType.OBJECT_GET,
122
+ data=b"",
123
+ atom_id=key,
124
+ )
125
+ message = Message(
126
+ topic=MessageTopic.OBJECT_REQUEST,
127
+ content=obj_req.to_bytes(),
128
+ sender=self.relay_public_key,
129
+ )
130
+ message.encrypt(shared_key_bytes)
131
+ self.add_atom_req(key)
132
+ self.outgoing_queue.put((message.to_bytes(), (provider_address, provider_port)))
133
+ self.logger.debug(
134
+ "Requested atom %s from indexed provider %s:%s",
135
+ key.hex(),
136
+ provider_address,
137
+ provider_port,
138
+ )
139
+ except Exception as exc:
140
+ self.logger.warning("Failed indexed fetch for %s: %s", key.hex(), exc)
141
+ return None
142
+
143
+ self.logger.debug("Falling back to network fetch for %s", key.hex())
144
+ return self._network_get(key)
145
+
146
+
147
+ def local_get(self, key: bytes) -> Optional[Atom]:
148
+ """Retrieve an Atom by checking only local hot and cold storage."""
149
+ self.logger.debug("Fetching atom %s (local only)", key.hex())
150
+ atom = self._hot_storage_get(key)
151
+ if atom is not None:
152
+ self.logger.debug("Returning atom %s from hot storage", key.hex())
153
+ return atom
154
+ atom = self._cold_storage_get(key)
155
+ if atom is not None:
156
+ self.logger.debug("Returning atom %s from cold storage", key.hex())
157
+ return atom
158
+ self.logger.debug("Local storage miss for %s", key.hex())
159
+ return None
160
+
161
+
162
+ def _cold_storage_get(self, key: bytes) -> Optional[Atom]:
163
+ """Read an atom from the cold storage directory if configured."""
164
+ if not self.config["cold_storage_path"]:
165
+ self.logger.debug("Cold storage disabled; cannot fetch %s", key.hex())
166
+ return None
167
+ filename = f"{key.hex().upper()}.bin"
168
+ file_path = Path(self.config["cold_storage_path"]) / filename
169
+ try:
170
+ data = file_path.read_bytes()
171
+ except FileNotFoundError:
172
+ self.logger.debug("Cold storage miss for %s", key.hex())
173
+ return None
174
+ except OSError as exc:
175
+ self.logger.warning("Error reading cold storage file %s: %s", file_path, exc)
176
+ return None
177
+ try:
178
+ atom = Atom.from_bytes(data)
179
+ self.logger.debug("Loaded atom %s from cold storage", key.hex())
180
+ return atom
181
+ except ValueError as exc:
182
+ self.logger.warning("Cold storage data corrupted for %s: %s", file_path, exc)
183
+ return None
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ import socket
4
+ from pathlib import Path
5
+
6
+ from cryptography.hazmat.primitives import serialization
7
+
8
+ from ..models.atom import Atom
9
+
10
+
11
+ def _hot_storage_set(self, key: bytes, value: Atom) -> bool:
12
+ """Store atom in hot storage without exceeding the configured limit."""
13
+ node_logger = self.logger
14
+ projected = self.hot_storage_size + value.size
15
+ hot_limit = self.config["hot_storage_default_limit"]
16
+ if projected > hot_limit:
17
+ node_logger.warning(
18
+ "Hot storage limit reached (%s > %s); skipping atom %s",
19
+ projected,
20
+ hot_limit,
21
+ key.hex(),
22
+ )
23
+ return False
24
+
25
+ self.hot_storage[key] = value
26
+ self.hot_storage_size = projected
27
+ node_logger.debug(
28
+ "Stored atom %s in hot storage (bytes=%s, total=%s)",
29
+ key.hex(),
30
+ value.size,
31
+ projected,
32
+ )
33
+ return True
34
+
35
+
36
+ def _cold_storage_set(self, atom: Atom) -> None:
37
+ """Persist an atom into the cold storage directory if it already exists."""
38
+ node_logger = self.logger
39
+ atom_id = atom.object_id()
40
+ atom_hex = atom_id.hex()
41
+ if not self.config["cold_storage_path"]:
42
+ node_logger.debug("Cold storage disabled; skipping atom %s", atom_hex)
43
+ return
44
+ atom_bytes = atom.to_bytes()
45
+ projected = self.cold_storage_size + len(atom_bytes)
46
+ cold_limit = self.config["cold_storage_limit"]
47
+ if cold_limit and projected > cold_limit:
48
+ node_logger.warning(
49
+ "Cold storage limit reached (%s > %s); skipping atom %s",
50
+ projected,
51
+ cold_limit,
52
+ atom_hex,
53
+ )
54
+ return
55
+ directory = Path(self.config["cold_storage_path"])
56
+ if not directory.exists():
57
+ node_logger.warning(
58
+ "Cold storage path %s missing; skipping atom %s",
59
+ directory,
60
+ atom_hex,
61
+ )
62
+ return
63
+ filename = f"{atom_hex.upper()}.bin"
64
+ file_path = directory / filename
65
+ try:
66
+ file_path.write_bytes(atom_bytes)
67
+ self.cold_storage_size = projected
68
+ node_logger.debug("Persisted atom %s to cold storage", atom_hex)
69
+ except OSError as exc:
70
+ node_logger.error(
71
+ "Failed writing atom %s to cold storage %s: %s",
72
+ atom_hex,
73
+ file_path,
74
+ exc,
75
+ )
76
+
77
+
78
+ def _network_set(self, atom: Atom) -> None:
79
+ """Advertise an atom to the closest known peer so they can fetch it from us."""
80
+ node_logger = self.logger
81
+ atom_id = atom.object_id()
82
+ atom_hex = atom_id.hex()
83
+ try:
84
+ from ...communication.handlers.object_request import (
85
+ ObjectRequest,
86
+ ObjectRequestType,
87
+ )
88
+ from ...communication.models.message import Message, MessageTopic
89
+ except Exception as exc:
90
+ node_logger.warning(
91
+ "Communication module unavailable; cannot advertise atom %s: %s",
92
+ atom_hex,
93
+ exc,
94
+ )
95
+ return
96
+
97
+ try:
98
+ provider_ip, provider_port = self.incoming_socket.getsockname()[:2]
99
+ except Exception as exc:
100
+ node_logger.warning(
101
+ "Unable to determine provider address for atom %s: %s",
102
+ atom_hex,
103
+ exc,
104
+ )
105
+ return
106
+
107
+ try:
108
+ provider_ip_bytes = socket.inet_aton(provider_ip)
109
+ provider_port_bytes = int(provider_port).to_bytes(2, "big", signed=False)
110
+ provider_key_bytes = self.relay_public_key_bytes
111
+ except Exception as exc:
112
+ node_logger.warning("Unable to encode provider info for %s: %s", atom_hex, exc)
113
+ return
114
+
115
+ provider_payload = provider_key_bytes + provider_ip_bytes + provider_port_bytes
116
+
117
+ try:
118
+ closest_peer = self.peer_route.closest_peer_for_hash(atom_id)
119
+ except Exception as exc:
120
+ node_logger.warning("Peer lookup failed for atom %s: %s", atom_hex, exc)
121
+ return
122
+
123
+ is_self_closest = False
124
+ if closest_peer is None or closest_peer.address is None:
125
+ is_self_closest = True
126
+ else:
127
+ try:
128
+ from ...communication.util import xor_distance
129
+ except Exception as exc:
130
+ node_logger.warning("Failed to import xor_distance for atom %s: %s", atom_hex, exc)
131
+ is_self_closest = True
132
+ else:
133
+ try:
134
+ self_distance = xor_distance(atom_id, self.relay_public_key_bytes)
135
+ peer_distance = xor_distance(atom_id, closest_peer.public_key_bytes)
136
+ except Exception as exc:
137
+ node_logger.warning("Failed computing distance for atom %s: %s", atom_hex, exc)
138
+ is_self_closest = True
139
+ else:
140
+ is_self_closest = self_distance <= peer_distance
141
+
142
+ if is_self_closest:
143
+ node_logger.debug("Self is closest; indexing provider for atom %s", atom_hex)
144
+ self.storage_index[atom_id] = provider_payload
145
+ return
146
+
147
+ target_addr = closest_peer.address
148
+
149
+ obj_req = ObjectRequest(
150
+ type=ObjectRequestType.OBJECT_PUT,
151
+ data=provider_payload,
152
+ atom_id=atom_id,
153
+ )
154
+
155
+ message_body = obj_req.to_bytes()
156
+
157
+ message = Message(
158
+ topic=MessageTopic.OBJECT_REQUEST,
159
+ content=message_body,
160
+ sender=self.relay_public_key,
161
+ )
162
+ message.encrypt(closest_peer.shared_key_bytes)
163
+ try:
164
+ self.outgoing_queue.put((message.to_bytes(), target_addr))
165
+ node_logger.debug(
166
+ "Advertised atom %s to peer at %s:%s",
167
+ atom_hex,
168
+ target_addr[0],
169
+ target_addr[1],
170
+ )
171
+ except Exception as exc:
172
+ node_logger.error(
173
+ "Failed to queue advertisement for atom %s to %s:%s: %s",
174
+ atom_hex,
175
+ target_addr[0],
176
+ target_addr[1],
177
+ exc,
178
+ )
@@ -1,18 +1,18 @@
1
-
2
-
1
+
2
+
3
3
  from enum import IntEnum
4
4
  from typing import List, Optional, Tuple
5
-
5
+
6
6
  from blake3 import blake3
7
-
8
- ZERO32 = b"\x00"*32
9
-
10
- def u64_le(n: int) -> bytes:
11
- return int(n).to_bytes(8, "little", signed=False)
12
-
13
- def hash_bytes(b: bytes) -> bytes:
14
- return blake3(b).digest()
15
-
7
+
8
+ ZERO32 = b"\x00"*32
9
+
10
+ def u64_le(n: int) -> bytes:
11
+ return int(n).to_bytes(8, "little", signed=False)
12
+
13
+ def hash_bytes(b: bytes) -> bytes:
14
+ return blake3(b).digest()
15
+
16
16
  class AtomKind(IntEnum):
17
17
  SYMBOL = 0
18
18
  BYTES = 1
@@ -20,35 +20,23 @@ class AtomKind(IntEnum):
20
20
 
21
21
 
22
22
  class Atom:
23
- data: bytes
24
- next: bytes
25
- size: int
26
-
27
- def __init__(
28
- self,
29
- data: bytes,
30
- next: bytes = ZERO32,
31
- size: Optional[int] = None,
32
- kind: AtomKind = AtomKind.BYTES,
33
- ):
23
+ data: bytes
24
+ kind: AtomKind
25
+ next_id: bytes
26
+ size: int
27
+
28
+ def __init__(self, data: bytes, kind: AtomKind, next_id: bytes = ZERO32):
34
29
  self.data = data
35
- self.next = next
36
- self.size = len(data) if size is None else size
37
30
  self.kind = kind
31
+ self.next_id = next_id
32
+ self.size = len(data)
33
+
38
34
 
39
- @staticmethod
40
- def from_data(
41
- data: bytes,
42
- next_hash: bytes = ZERO32,
43
- kind: AtomKind = AtomKind.BYTES,
44
- ) -> "Atom":
45
- return Atom(data=data, next=next_hash, size=len(data), kind=kind)
46
-
47
35
  def generate_id(self) -> bytes:
48
36
  """Compute the object id using this atom's metadata."""
49
37
  kind_bytes = int(self.kind).to_bytes(1, "little", signed=False)
50
38
  return blake3(
51
- kind_bytes + self.data_hash() + self.next + u64_le(self.size)
39
+ kind_bytes + self.data_hash() + self.next_id + u64_le(self.size)
52
40
  ).digest()
53
41
 
54
42
  def data_hash(self) -> bytes:
@@ -68,11 +56,11 @@ class Atom:
68
56
  kind_bytes = int(kind).to_bytes(1, "little", signed=False)
69
57
  expected = blake3(kind_bytes + data_hash + next_hash + u64_le(size)).digest()
70
58
  return object_id == expected
71
-
59
+
72
60
  def to_bytes(self) -> bytes:
73
61
  """Serialize as next-hash + kind byte + payload."""
74
62
  kind_byte = int(self.kind).to_bytes(1, "little", signed=False)
75
- return self.next + kind_byte + self.data
63
+ return self.next_id + kind_byte + self.data
76
64
 
77
65
  @staticmethod
78
66
  def from_bytes(buf: bytes) -> "Atom":
@@ -86,24 +74,34 @@ class Atom:
86
74
  kind = AtomKind(kind_value)
87
75
  except ValueError as exc:
88
76
  raise ValueError(f"unknown atom kind: {kind_value}") from exc
89
- return Atom(data=data, next=next_hash, size=len(data), kind=kind)
90
-
91
- def bytes_list_to_atoms(values: List[bytes]) -> Tuple[bytes, List[Atom]]:
92
- """Build a forward-ordered linked list of atoms from byte payloads.
93
-
94
- Returns the head object's hash (ZERO32 if no values) and the atoms created.
95
- """
96
- next_hash = ZERO32
97
- atoms: List[Atom] = []
98
-
99
- for value in reversed(values):
100
- atom = Atom.from_data(
101
- data=bytes(value),
102
- next_hash=next_hash,
103
- kind=AtomKind.BYTES,
104
- )
105
- atoms.append(atom)
106
- next_hash = atom.object_id()
107
-
108
- atoms.reverse()
109
- return (next_hash if values else ZERO32), atoms
77
+ return Atom(data=data, next_id=next_hash, kind=kind)
78
+
79
+ def bytes_list_to_atoms(values: List[bytes]) -> Tuple[bytes, List[Atom]]:
80
+ """Build a forward-ordered linked list of atoms from byte payloads.
81
+
82
+ Returns the head object's hash (ZERO32 if no values) and the atoms created.
83
+ """
84
+ next_hash = ZERO32
85
+ atoms: List[Atom] = []
86
+
87
+ for value in reversed(values):
88
+ atom = Atom(data=bytes(value), next_id=next_hash, kind=AtomKind.BYTES)
89
+ atoms.append(atom)
90
+ next_hash = atom.object_id()
91
+
92
+ atoms.reverse()
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