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.
Files changed (65) hide show
  1. astreum/__init__.py +1 -2
  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 -62
  6. astreum/communication/handlers/object_request.py +226 -138
  7. astreum/communication/handlers/object_response.py +118 -10
  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 +133 -58
  21. astreum/communication/setup.py +272 -113
  22. astreum/communication/util.py +14 -0
  23. astreum/machine/evaluations/low_evaluation.py +5 -5
  24. astreum/machine/models/expression.py +5 -5
  25. astreum/node.py +96 -87
  26. astreum/storage/actions/get.py +285 -183
  27. astreum/storage/actions/set.py +171 -156
  28. astreum/storage/models/atom.py +0 -14
  29. astreum/storage/models/trie.py +2 -2
  30. astreum/storage/providers.py +24 -0
  31. astreum/storage/requests.py +13 -10
  32. astreum/storage/setup.py +20 -15
  33. astreum/utils/config.py +260 -43
  34. astreum/utils/logging.py +1 -1
  35. astreum/{consensus → validation}/__init__.py +0 -4
  36. astreum/validation/constants.py +2 -0
  37. astreum/{consensus → validation}/genesis.py +4 -6
  38. astreum/{consensus → validation}/models/account.py +1 -1
  39. astreum/validation/models/block.py +544 -0
  40. astreum/validation/models/fork.py +511 -0
  41. astreum/{consensus → validation}/models/receipt.py +18 -5
  42. astreum/{consensus → validation}/models/transaction.py +50 -8
  43. astreum/validation/node.py +190 -0
  44. astreum/{consensus → validation}/validator.py +1 -1
  45. astreum/validation/workers/__init__.py +8 -0
  46. astreum/{consensus → validation}/workers/validation.py +360 -333
  47. astreum/verification/__init__.py +4 -0
  48. astreum/{consensus/workers/discovery.py → verification/discover.py} +1 -1
  49. astreum/verification/node.py +61 -0
  50. astreum/verification/worker.py +183 -0
  51. {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/METADATA +45 -9
  52. astreum-0.3.48.dist-info/RECORD +79 -0
  53. astreum/consensus/models/block.py +0 -364
  54. astreum/consensus/models/chain.py +0 -66
  55. astreum/consensus/models/fork.py +0 -100
  56. astreum/consensus/setup.py +0 -83
  57. astreum/consensus/start.py +0 -67
  58. astreum/consensus/workers/__init__.py +0 -9
  59. astreum/consensus/workers/verify.py +0 -90
  60. astreum-0.3.16.dist-info/RECORD +0 -72
  61. /astreum/{consensus → validation}/models/__init__.py +0 -0
  62. /astreum/{consensus → validation}/models/accounts.py +0 -0
  63. {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/WHEEL +0 -0
  64. {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/licenses/LICENSE +0 -0
  65. {astreum-0.3.16.dist-info → astreum-0.3.48.dist-info}/top_level.txt +0 -0
@@ -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
+
@@ -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)
@@ -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
@@ -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
@@ -1,22 +1,27 @@
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 = {}
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 = []
14
15
  node.hot_storage_size = 0
15
16
  node.cold_storage_size = 0
17
+ node.atom_fetch_interval = config["atom_fetch_interval"]
18
+ node.atom_fetch_retries = config["atom_fetch_retries"]
16
19
 
17
20
  node.logger.info(
18
- "Storage ready (hot_limit=%s bytes, cold_limit=%s bytes, cold_path=%s)",
19
- config["hot_storage_default_limit"],
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"],
20
23
  config["cold_storage_limit"],
21
24
  config["cold_storage_path"] or "disabled",
25
+ config["atom_fetch_interval"],
26
+ config["atom_fetch_retries"],
22
27
  )