astreum 0.3.16__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/__init__.py +1 -2
- astreum/communication/__init__.py +15 -11
- astreum/communication/difficulty.py +39 -0
- astreum/communication/disconnect.py +57 -0
- astreum/communication/handlers/handshake.py +105 -62
- astreum/communication/handlers/object_request.py +226 -138
- astreum/communication/handlers/object_response.py +118 -10
- astreum/communication/handlers/ping.py +9 -0
- astreum/communication/handlers/route_request.py +7 -1
- astreum/communication/handlers/route_response.py +7 -1
- astreum/communication/incoming_queue.py +96 -0
- astreum/communication/message_pow.py +36 -0
- astreum/communication/models/peer.py +4 -0
- astreum/communication/models/ping.py +27 -6
- astreum/communication/models/route.py +4 -0
- astreum/communication/{start.py → node.py} +10 -11
- astreum/communication/outgoing_queue.py +108 -0
- astreum/communication/processors/incoming.py +110 -37
- astreum/communication/processors/outgoing.py +35 -2
- astreum/communication/processors/peer.py +133 -58
- astreum/communication/setup.py +272 -113
- astreum/communication/util.py +14 -0
- astreum/machine/evaluations/low_evaluation.py +5 -5
- astreum/machine/models/expression.py +5 -5
- astreum/node.py +96 -87
- astreum/storage/actions/get.py +285 -183
- astreum/storage/actions/set.py +171 -156
- astreum/storage/models/atom.py +0 -14
- astreum/storage/models/trie.py +2 -2
- astreum/storage/providers.py +24 -0
- astreum/storage/requests.py +13 -10
- astreum/storage/setup.py +20 -15
- astreum/utils/config.py +260 -43
- astreum/utils/logging.py +1 -1
- astreum/{consensus → validation}/__init__.py +0 -4
- astreum/validation/constants.py +2 -0
- astreum/{consensus → validation}/genesis.py +4 -6
- astreum/{consensus → validation}/models/account.py +1 -1
- astreum/validation/models/block.py +544 -0
- astreum/validation/models/fork.py +511 -0
- astreum/{consensus → validation}/models/receipt.py +18 -5
- astreum/{consensus → validation}/models/transaction.py +50 -8
- astreum/validation/node.py +190 -0
- astreum/{consensus → validation}/validator.py +1 -1
- astreum/validation/workers/__init__.py +8 -0
- astreum/{consensus → validation}/workers/validation.py +360 -333
- astreum/verification/__init__.py +4 -0
- astreum/{consensus/workers/discovery.py → verification/discover.py} +1 -1
- astreum/verification/node.py +61 -0
- astreum/verification/worker.py +183 -0
- {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/METADATA +45 -9
- astreum-0.3.48.dist-info/RECORD +79 -0
- astreum/consensus/models/block.py +0 -364
- astreum/consensus/models/chain.py +0 -66
- astreum/consensus/models/fork.py +0 -100
- astreum/consensus/setup.py +0 -83
- astreum/consensus/start.py +0 -67
- astreum/consensus/workers/__init__.py +0 -9
- astreum/consensus/workers/verify.py +0 -90
- astreum-0.3.16.dist-info/RECORD +0 -72
- /astreum/{consensus → validation}/models/__init__.py +0 -0
- /astreum/{consensus → validation}/models/accounts.py +0 -0
- {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/WHEEL +0 -0
- {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/top_level.txt +0 -0
astreum/storage/actions/get.py
CHANGED
|
@@ -1,183 +1,285 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
self.logger.debug("Hot storage
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
self.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
try:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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(),
|
|
60
|
+
)
|
|
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(),
|
|
76
|
+
)
|
|
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
|
|
89
|
+
from ...communication.handlers.object_request import (
|
|
90
|
+
ObjectRequest,
|
|
91
|
+
ObjectRequestType,
|
|
92
|
+
)
|
|
93
|
+
from ...communication.models.message import Message, MessageTopic
|
|
94
|
+
from ...communication.outgoing_queue import enqueue_outgoing
|
|
95
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
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
|
+
)
|
|
112
|
+
message.encrypt(shared_key_bytes)
|
|
113
|
+
self.add_atom_req(atom_id, payload_type)
|
|
114
|
+
queued = enqueue_outgoing(
|
|
115
|
+
self,
|
|
116
|
+
(provider_address, provider_port),
|
|
117
|
+
message=message,
|
|
118
|
+
difficulty=1,
|
|
119
|
+
)
|
|
120
|
+
if queued:
|
|
121
|
+
self.logger.debug(
|
|
122
|
+
"Requested atom %s from indexed provider %s:%s",
|
|
123
|
+
atom_id.hex(),
|
|
124
|
+
provider_address,
|
|
125
|
+
provider_port,
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
self.logger.debug(
|
|
129
|
+
"Dropped request for atom %s to indexed provider %s:%s",
|
|
130
|
+
atom_id.hex(),
|
|
131
|
+
provider_address,
|
|
132
|
+
provider_port,
|
|
133
|
+
)
|
|
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
|