astreum 0.3.9__py3-none-any.whl → 0.3.46__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 (60) hide show
  1. astreum/__init__.py +5 -4
  2. astreum/communication/__init__.py +15 -11
  3. astreum/communication/difficulty.py +39 -0
  4. astreum/communication/disconnect.py +57 -0
  5. astreum/communication/handlers/handshake.py +105 -89
  6. astreum/communication/handlers/object_request.py +179 -149
  7. astreum/communication/handlers/object_response.py +7 -1
  8. astreum/communication/handlers/ping.py +9 -0
  9. astreum/communication/handlers/route_request.py +7 -1
  10. astreum/communication/handlers/route_response.py +7 -1
  11. astreum/communication/incoming_queue.py +96 -0
  12. astreum/communication/message_pow.py +36 -0
  13. astreum/communication/models/peer.py +4 -0
  14. astreum/communication/models/ping.py +27 -6
  15. astreum/communication/models/route.py +4 -0
  16. astreum/communication/{start.py → node.py} +10 -11
  17. astreum/communication/outgoing_queue.py +108 -0
  18. astreum/communication/processors/incoming.py +110 -37
  19. astreum/communication/processors/outgoing.py +35 -2
  20. astreum/communication/processors/peer.py +134 -0
  21. astreum/communication/setup.py +273 -112
  22. astreum/communication/util.py +14 -0
  23. astreum/node.py +99 -89
  24. astreum/storage/actions/get.py +79 -48
  25. astreum/storage/actions/set.py +171 -156
  26. astreum/storage/providers.py +24 -0
  27. astreum/storage/setup.py +23 -22
  28. astreum/utils/config.py +247 -30
  29. astreum/utils/logging.py +1 -1
  30. astreum/{consensus → validation}/__init__.py +0 -4
  31. astreum/validation/constants.py +2 -0
  32. astreum/{consensus → validation}/genesis.py +4 -6
  33. astreum/validation/models/block.py +544 -0
  34. astreum/validation/models/fork.py +511 -0
  35. astreum/{consensus → validation}/models/receipt.py +17 -4
  36. astreum/{consensus → validation}/models/transaction.py +45 -3
  37. astreum/validation/node.py +190 -0
  38. astreum/{consensus → validation}/validator.py +18 -9
  39. astreum/validation/workers/__init__.py +8 -0
  40. astreum/{consensus → validation}/workers/validation.py +361 -307
  41. astreum/verification/__init__.py +4 -0
  42. astreum/{consensus/workers/discovery.py → verification/discover.py} +1 -1
  43. astreum/verification/node.py +61 -0
  44. astreum/verification/worker.py +183 -0
  45. {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/METADATA +43 -9
  46. astreum-0.3.46.dist-info/RECORD +79 -0
  47. astreum/consensus/models/block.py +0 -364
  48. astreum/consensus/models/chain.py +0 -66
  49. astreum/consensus/models/fork.py +0 -100
  50. astreum/consensus/setup.py +0 -83
  51. astreum/consensus/start.py +0 -67
  52. astreum/consensus/workers/__init__.py +0 -9
  53. astreum/consensus/workers/verify.py +0 -90
  54. astreum-0.3.9.dist-info/RECORD +0 -71
  55. /astreum/{consensus → validation}/models/__init__.py +0 -0
  56. /astreum/{consensus → validation}/models/account.py +0 -0
  57. /astreum/{consensus → validation}/models/accounts.py +0 -0
  58. {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/WHEEL +0 -0
  59. {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/licenses/LICENSE +0 -0
  60. {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
  from typing import Optional
5
5
 
6
6
  from ..models.atom import Atom
7
+ from ..providers import provider_payload_for_id
7
8
 
8
9
 
9
10
  def _hot_storage_get(self, key: bytes) -> Optional[Atom]:
@@ -24,11 +25,12 @@ def _network_get(self, key: bytes) -> Optional[Atom]:
24
25
  return None
25
26
  self.logger.debug("Attempting network fetch for %s", key.hex())
26
27
  try:
27
- from ...communication.handlers.object_request import (
28
- ObjectRequest,
29
- ObjectRequestType,
30
- )
31
- from ...communication.models.message import Message, MessageTopic
28
+ from ...communication.handlers.object_request import (
29
+ ObjectRequest,
30
+ ObjectRequestType,
31
+ )
32
+ from ...communication.models.message import Message, MessageTopic
33
+ from ...communication.outgoing_queue import enqueue_outgoing
32
34
  except Exception as exc:
33
35
  self.logger.warning(
34
36
  "Communication module unavailable; cannot fetch %s: %s",
@@ -71,12 +73,24 @@ def _network_get(self, key: bytes) -> Optional[Atom]:
71
73
  self.logger.warning("Failed to track object request for %s: %s", key.hex(), exc)
72
74
 
73
75
  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
- )
76
+ queued = enqueue_outgoing(
77
+ self,
78
+ closest_peer.address,
79
+ message=message,
80
+ difficulty=closest_peer.difficulty,
81
+ )
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
+ )
80
94
  except Exception as exc:
81
95
  self.logger.warning(
82
96
  "Failed to queue OBJECT_GET for %s to %s: %s",
@@ -102,43 +116,60 @@ def storage_get(self, key: bytes) -> Optional[Atom]:
102
116
  if not self.is_connected:
103
117
  return None
104
118
 
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
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
125
+ from ...communication.handlers.object_request import (
126
+ ObjectRequest,
127
+ ObjectRequestType,
128
+ )
129
+ from ...communication.models.message import Message, MessageTopic
130
+ from ...communication.outgoing_queue import enqueue_outgoing
131
+ 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
+ )
147
+ message.encrypt(shared_key_bytes)
148
+ self.add_atom_req(key)
149
+ queued = enqueue_outgoing(
150
+ self,
151
+ (provider_address, provider_port),
152
+ message=message,
153
+ difficulty=1,
154
+ )
155
+ if queued:
156
+ self.logger.debug(
157
+ "Requested atom %s from indexed provider %s:%s",
158
+ key.hex(),
159
+ provider_address,
160
+ provider_port,
161
+ )
162
+ else:
163
+ self.logger.debug(
164
+ "Dropped request for atom %s to indexed provider %s:%s",
165
+ key.hex(),
166
+ provider_address,
167
+ provider_port,
168
+ )
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())
142
173
 
143
174
  self.logger.debug("Falling back to network fetch for %s", key.hex())
144
175
  return self._network_get(key)
@@ -1,159 +1,160 @@
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()
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
+ from ..providers import provider_id_for_payload
10
+
11
+
12
+ def _hot_storage_set(self, key: bytes, value: Atom) -> bool:
13
+ """Store atom in hot storage without exceeding the configured limit."""
14
+ node_logger = self.logger
15
+ projected = self.hot_storage_size + value.size
16
+ hot_limit = self.config["hot_storage_limit"]
17
+ if projected > hot_limit:
18
+ node_logger.warning(
19
+ "Hot storage limit reached (%s > %s); skipping atom %s",
20
+ projected,
21
+ hot_limit,
22
+ key.hex(),
23
+ )
24
+ return False
25
+
26
+ self.hot_storage[key] = value
27
+ self.hot_storage_size = projected
28
+ node_logger.debug(
29
+ "Stored atom %s in hot storage (bytes=%s, total=%s)",
30
+ key.hex(),
31
+ value.size,
32
+ projected,
33
+ )
34
+ return True
35
+
36
+
37
+ def _cold_storage_set(self, key: bytes, atom: Atom) -> None:
38
+ """Persist an atom into the cold storage directory if it already exists."""
39
+ node_logger = self.logger
40
+ atom_hex = key.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_id: bytes) -> None:
79
+ """Advertise an atom id to the closest known peer so they can fetch it from us."""
80
+ node_logger = self.logger
81
+ atom_hex = atom_id.hex()
83
82
  try:
84
83
  from ...communication.handlers.object_request import (
85
84
  ObjectRequest,
86
85
  ObjectRequestType,
87
86
  )
88
87
  from ...communication.models.message import Message, MessageTopic
88
+ from ...communication.outgoing_queue import enqueue_outgoing
89
89
  except Exception as exc:
90
90
  node_logger.warning(
91
91
  "Communication module unavailable; cannot advertise atom %s: %s",
92
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
-
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
+ provider_id = provider_id_for_payload(self, provider_payload)
145
+ self.storage_index[atom_id] = provider_id
146
+ return
147
+
148
+ target_addr = closest_peer.address
149
+
150
+ obj_req = ObjectRequest(
151
+ type=ObjectRequestType.OBJECT_PUT,
152
+ data=provider_payload,
153
+ atom_id=atom_id,
154
+ )
155
+
156
+ message_body = obj_req.to_bytes()
157
+
157
158
  message = Message(
158
159
  topic=MessageTopic.OBJECT_REQUEST,
159
160
  content=message_body,
@@ -161,18 +162,32 @@ def _network_set(self, atom: Atom) -> None:
161
162
  )
162
163
  message.encrypt(closest_peer.shared_key_bytes)
163
164
  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],
165
+ queued = enqueue_outgoing(
166
+ self,
167
+ target_addr,
168
+ message=message,
169
+ difficulty=closest_peer.difficulty,
170
170
  )
171
+ if queued:
172
+ node_logger.debug(
173
+ "Advertised atom %s to peer at %s:%s",
174
+ atom_hex,
175
+ target_addr[0],
176
+ target_addr[1],
177
+ )
178
+ else:
179
+ node_logger.debug(
180
+ "Dropped atom advertisement %s to peer at %s:%s",
181
+ atom_hex,
182
+ target_addr[0],
183
+ target_addr[1],
184
+ )
171
185
  except Exception as exc:
172
186
  node_logger.error(
173
187
  "Failed to queue advertisement for atom %s to %s:%s: %s",
174
188
  atom_hex,
175
- target_addr[0],
176
- target_addr[1],
177
- exc,
178
- )
189
+ target_addr[0],
190
+ target_addr[1],
191
+ exc,
192
+ )
193
+
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+
6
+
7
+ def provider_id_for_payload(node, payload: bytes) -> int:
8
+ """Return the provider id for a payload, inserting if new."""
9
+ for idx, existing in enumerate(node.storage_providers):
10
+ if existing == payload:
11
+ return idx
12
+ node.storage_providers.append(payload)
13
+ return len(node.storage_providers) - 1
14
+
15
+
16
+
17
+ def provider_payload_for_id(node, provider_id: int) -> Optional[bytes]:
18
+ """Return the provider payload for a provider id, or None."""
19
+ if not isinstance(provider_id, int) or provider_id < 0:
20
+ return None
21
+ try:
22
+ return node.storage_providers[provider_id]
23
+ except IndexError:
24
+ return None
astreum/storage/setup.py CHANGED
@@ -1,22 +1,23 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
-
6
- def storage_setup(node: Any, config: dict) -> None:
7
- """Initialize hot/cold storage helpers on the node."""
8
-
9
- node.logger.info("Setting up node storage")
10
-
11
- node.hot_storage = {}
12
- node.hot_storage_hits = {}
13
- node.storage_index = {}
14
- node.hot_storage_size = 0
15
- node.cold_storage_size = 0
16
-
17
- node.logger.info(
18
- "Storage ready (hot_limit=%s bytes, cold_limit=%s bytes, cold_path=%s)",
19
- config["hot_storage_default_limit"],
20
- config["cold_storage_limit"],
21
- config["cold_storage_path"] or "disabled",
22
- )
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def storage_setup(node: Any, config: dict) -> None:
7
+ """Initialize hot/cold storage helpers on the node."""
8
+
9
+ node.logger.info("Setting up node storage")
10
+
11
+ node.hot_storage = {}
12
+ node.hot_storage_hits = {}
13
+ node.storage_index = {}
14
+ node.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
+ )