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
@@ -1,176 +1,206 @@
1
- import logging
2
- import socket
1
+ import logging
2
+ import socket
3
3
  from enum import IntEnum
4
4
  from typing import TYPE_CHECKING, Tuple
5
5
 
6
6
  from .object_response import ObjectResponse, ObjectResponseType
7
+ from ..outgoing_queue import enqueue_outgoing
7
8
  from ..models.message import Message, MessageTopic
8
9
  from ..util import xor_distance
9
-
10
- if TYPE_CHECKING:
11
- from .. import Node
12
- from ..models.peer import Peer
13
-
14
-
15
- class ObjectRequestType(IntEnum):
16
- OBJECT_GET = 0
17
- OBJECT_PUT = 1
18
-
19
-
20
- class ObjectRequest:
21
- type: ObjectRequestType
22
- data: bytes
23
- atom_id: bytes
24
-
25
- def __init__(self, type: ObjectRequestType, data: bytes, atom_id: bytes = None):
26
- self.type = type
27
- self.data = data
28
- self.atom_id = atom_id
29
-
30
- def to_bytes(self):
31
- return bytes([self.type.value]) + self.atom_id + self.data
32
-
33
- @classmethod
34
- def from_bytes(cls, data: bytes) -> "ObjectRequest":
35
- # need at least 1 byte for type + 32 bytes for hash
36
- if len(data) < 1 + 32:
37
- raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
38
-
39
- type_val = data[0]
40
- try:
41
- req_type = ObjectRequestType(type_val)
42
- except ValueError:
43
- raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
44
-
45
- atom_id_bytes = data[1:33]
46
- payload = data[33:]
47
- return cls(req_type, payload, atom_id_bytes)
48
-
49
-
50
- def encode_peer_contact_bytes(peer: "Peer") -> bytes:
51
- """Return a fixed-width peer contact payload (32-byte key + IPv4 + port)."""
52
- host, port = peer.address
53
- key_bytes = peer.public_key_bytes
54
- try:
55
- ip_bytes = socket.inet_aton(host)
56
- except OSError as exc: # pragma: no cover - inet_aton raises for invalid hosts
57
- raise ValueError(f"invalid IPv4 address: {host}") from exc
58
- if not (0 <= port <= 0xFFFF):
59
- raise ValueError(f"port out of range (0-65535): {port}")
60
- port_bytes = int(port).to_bytes(2, "big", signed=False)
61
- return key_bytes + ip_bytes + port_bytes
62
-
63
-
64
- def handle_object_request(node: "Node", peer: "Peer", message: Message) -> None:
65
- if message.content is None:
66
- node.logger.warning("OBJECT_REQUEST from %s missing content", peer.address)
67
- return
68
-
69
- try:
70
- object_request = ObjectRequest.from_bytes(message.content)
71
- except Exception as exc:
72
- node.logger.warning("Error decoding OBJECT_REQUEST from %s: %s", peer.address, exc)
73
- return
74
-
75
- match object_request.type:
76
- case ObjectRequestType.OBJECT_GET:
77
- atom_id = object_request.atom_id
78
- node.logger.debug("Handling OBJECT_GET for %s from %s", atom_id.hex(), peer.address)
79
-
80
- local_atom = node.local_get(atom_id)
81
- if local_atom is not None:
82
- node.logger.debug("Object %s found locally; returning to %s", atom_id.hex(), peer.address)
83
- resp = ObjectResponse(
84
- type=ObjectResponseType.OBJECT_FOUND,
85
- data=local_atom.to_bytes(),
86
- atom_id=atom_id
87
- )
10
+ from ...storage.providers import provider_id_for_payload, provider_payload_for_id
11
+
12
+ if TYPE_CHECKING:
13
+ from .. import Node
14
+ from ..models.peer import Peer
15
+
16
+
17
+ class ObjectRequestType(IntEnum):
18
+ OBJECT_GET = 0
19
+ OBJECT_PUT = 1
20
+
21
+
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
38
+ if len(data) < 1 + 32:
39
+ raise ValueError(f"Too short for ObjectRequest ({len(data)} bytes)")
40
+
41
+ type_val = data[0]
42
+ try:
43
+ req_type = ObjectRequestType(type_val)
44
+ except ValueError:
45
+ raise ValueError(f"Unknown ObjectRequestType: {type_val!r}")
46
+
47
+ atom_id_bytes = data[1:33]
48
+ payload = data[33:]
49
+ return cls(req_type, payload, atom_id_bytes)
50
+
51
+
52
+ def encode_peer_contact_bytes(peer: "Peer") -> bytes:
53
+ """Return a fixed-width peer contact payload (32-byte key + IPv4 + port)."""
54
+ host, port = peer.address
55
+ key_bytes = peer.public_key_bytes
56
+ try:
57
+ ip_bytes = socket.inet_aton(host)
58
+ except OSError as exc: # pragma: no cover - inet_aton raises for invalid hosts
59
+ raise ValueError(f"invalid IPv4 address: {host}") from exc
60
+ if not (0 <= port <= 0xFFFF):
61
+ raise ValueError(f"port out of range (0-65535): {port}")
62
+ port_bytes = int(port).to_bytes(2, "big", signed=False)
63
+ return key_bytes + ip_bytes + port_bytes
64
+
65
+
66
+ def handle_object_request(node: "Node", peer: "Peer", message: Message) -> None:
67
+ if message.content is None:
68
+ node.logger.warning("OBJECT_REQUEST from %s missing content", peer.address)
69
+ return
70
+
71
+ try:
72
+ object_request = ObjectRequest.from_bytes(message.content)
73
+ except Exception as exc:
74
+ node.logger.warning("Error decoding OBJECT_REQUEST from %s: %s", peer.address, exc)
75
+ return
76
+
77
+ 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
+ )
88
90
  obj_res_msg = Message(
89
91
  topic=MessageTopic.OBJECT_RESPONSE,
90
92
  body=resp.to_bytes(),
91
93
  sender=node.relay_public_key,
92
94
  )
93
95
  obj_res_msg.encrypt(peer.shared_key_bytes)
94
- node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
95
- return
96
-
97
- if atom_id in node.storage_index:
98
- node.logger.debug("Known provider for %s; informing %s", atom_id.hex(), peer.address)
99
- provider_bytes = node.storage_index[atom_id]
100
- resp = ObjectResponse(
101
- type=ObjectResponseType.OBJECT_PROVIDER,
102
- data=provider_bytes,
103
- atom_id=atom_id
96
+ enqueue_outgoing(
97
+ node,
98
+ peer.address,
99
+ message=obj_res_msg,
100
+ difficulty=peer.difficulty,
104
101
  )
105
- obj_res_msg = Message(
106
- topic=MessageTopic.OBJECT_RESPONSE,
107
- body=resp.to_bytes(),
108
- sender=node.relay_public_key,
109
- )
110
- obj_res_msg.encrypt(peer.shared_key_bytes)
111
- node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
112
102
  return
113
-
114
- nearest_peer = node.peer_route.closest_peer_for_hash(atom_id)
115
- if nearest_peer:
116
- node.logger.debug("Forwarding requester %s to nearest peer for %s", peer.address, atom_id.hex())
117
- peer_info = encode_peer_contact_bytes(nearest_peer)
118
- resp = ObjectResponse(
119
- type=ObjectResponseType.OBJECT_PROVIDER,
120
- # type=ObjectResponseType.OBJECT_NEAREST_PEER,
121
- data=peer_info,
122
- atom_id=atom_id
123
- )
103
+
104
+ if atom_id in node.storage_index:
105
+ provider_id = node.storage_index[atom_id]
106
+ provider_bytes = provider_payload_for_id(node, provider_id)
107
+ if provider_bytes is not None:
108
+ node.logger.debug("Known provider for %s; informing %s", atom_id.hex(), peer.address)
109
+ resp = ObjectResponse(
110
+ type=ObjectResponseType.OBJECT_PROVIDER,
111
+ data=provider_bytes,
112
+ atom_id=atom_id
113
+ )
114
+ obj_res_msg = Message(
115
+ topic=MessageTopic.OBJECT_RESPONSE,
116
+ body=resp.to_bytes(),
117
+ sender=node.relay_public_key,
118
+ )
119
+ obj_res_msg.encrypt(peer.shared_key_bytes)
120
+ enqueue_outgoing(
121
+ node,
122
+ peer.address,
123
+ message=obj_res_msg,
124
+ difficulty=peer.difficulty,
125
+ )
126
+ return
127
+ node.logger.warning(
128
+ "Unknown provider id %s for %s",
129
+ provider_id,
130
+ atom_id.hex(),
131
+ )
132
+
133
+ nearest_peer = node.peer_route.closest_peer_for_hash(atom_id)
134
+ if nearest_peer:
135
+ node.logger.debug("Forwarding requester %s to nearest peer for %s", peer.address, atom_id.hex())
136
+ peer_info = encode_peer_contact_bytes(nearest_peer)
137
+ resp = ObjectResponse(
138
+ type=ObjectResponseType.OBJECT_PROVIDER,
139
+ # type=ObjectResponseType.OBJECT_NEAREST_PEER,
140
+ data=peer_info,
141
+ atom_id=atom_id
142
+ )
124
143
  obj_res_msg = Message(
125
144
  topic=MessageTopic.OBJECT_RESPONSE,
126
145
  body=resp.to_bytes(),
127
146
  sender=node.relay_public_key,
128
147
  )
129
148
  obj_res_msg.encrypt(nearest_peer.shared_key_bytes)
130
- node.outgoing_queue.put((obj_res_msg.to_bytes(), peer.address))
131
-
132
- case ObjectRequestType.OBJECT_PUT:
133
- node.logger.debug("Handling OBJECT_PUT for %s from %s", object_request.atom_id.hex(), peer.address)
134
-
135
- nearest_peer = node.peer_route.closest_peer_for_hash(object_request.atom_id)
136
- is_self_closest = False
137
- if nearest_peer is None or nearest_peer.address is None:
138
- is_self_closest = True
139
- else:
140
- try:
141
- self_distance = xor_distance(object_request.atom_id, node.relay_public_key_bytes)
142
- peer_distance = xor_distance(object_request.atom_id, nearest_peer.public_key_bytes)
143
- except Exception as exc:
144
- node.logger.warning(
145
- "Failed distance comparison for OBJECT_PUT %s: %s",
146
- object_request.atom_id.hex(),
147
- exc,
148
- )
149
- is_self_closest = True
150
- else:
151
- is_self_closest = self_distance <= peer_distance
152
-
153
- if is_self_closest:
154
- node.logger.debug("Storing provider info for %s locally", object_request.atom_id.hex())
155
- node.storage_index[object_request.atom_id] = object_request.data
156
- else:
157
- node.logger.debug(
158
- "Forwarding OBJECT_PUT for %s to nearer peer %s",
159
- object_request.atom_id.hex(),
160
- nearest_peer.address,
161
- )
162
- fwd_req = ObjectRequest(
163
- type=ObjectRequestType.OBJECT_PUT,
164
- data=object_request.data,
165
- atom_id=object_request.atom_id,
149
+ enqueue_outgoing(
150
+ node,
151
+ peer.address,
152
+ message=obj_res_msg,
153
+ difficulty=peer.difficulty,
166
154
  )
155
+
156
+ case ObjectRequestType.OBJECT_PUT:
157
+ node.logger.debug("Handling OBJECT_PUT for %s from %s", object_request.atom_id.hex(), peer.address)
158
+
159
+ nearest_peer = node.peer_route.closest_peer_for_hash(object_request.atom_id)
160
+ is_self_closest = False
161
+ if nearest_peer is None or nearest_peer.address is None:
162
+ is_self_closest = True
163
+ else:
164
+ try:
165
+ self_distance = xor_distance(object_request.atom_id, node.relay_public_key_bytes)
166
+ peer_distance = xor_distance(object_request.atom_id, nearest_peer.public_key_bytes)
167
+ except Exception as exc:
168
+ node.logger.warning(
169
+ "Failed distance comparison for OBJECT_PUT %s: %s",
170
+ object_request.atom_id.hex(),
171
+ exc,
172
+ )
173
+ is_self_closest = True
174
+ else:
175
+ is_self_closest = self_distance <= peer_distance
176
+
177
+ if is_self_closest:
178
+ node.logger.debug("Storing provider info for %s locally", object_request.atom_id.hex())
179
+ provider_id = provider_id_for_payload(node, object_request.data)
180
+ node.storage_index[object_request.atom_id] = provider_id
181
+ else:
182
+ node.logger.debug(
183
+ "Forwarding OBJECT_PUT for %s to nearer peer %s",
184
+ object_request.atom_id.hex(),
185
+ nearest_peer.address,
186
+ )
187
+ fwd_req = ObjectRequest(
188
+ type=ObjectRequestType.OBJECT_PUT,
189
+ data=object_request.data,
190
+ atom_id=object_request.atom_id,
191
+ )
167
192
  obj_req_msg = Message(
168
193
  topic=MessageTopic.OBJECT_REQUEST,
169
194
  body=fwd_req.to_bytes(),
170
195
  sender=node.relay_public_key,
171
196
  )
172
197
  obj_req_msg.encrypt(nearest_peer.shared_key_bytes)
173
- node.outgoing_queue.put((obj_req_msg.to_bytes(), nearest_peer.address))
174
-
175
- case _:
176
- node.logger.warning("Unknown ObjectRequestType %s from %s", object_request.type, peer.address)
198
+ enqueue_outgoing(
199
+ node,
200
+ nearest_peer.address,
201
+ message=obj_req_msg,
202
+ difficulty=nearest_peer.difficulty,
203
+ )
204
+
205
+ case _:
206
+ node.logger.warning("Unknown ObjectRequestType %s from %s", object_request.type, peer.address)
@@ -2,6 +2,7 @@ import socket
2
2
  from enum import IntEnum
3
3
  from typing import Tuple, TYPE_CHECKING
4
4
 
5
+ from ..outgoing_queue import enqueue_outgoing
5
6
  from ..models.message import Message, MessageTopic
6
7
  from ...storage.models.atom import Atom
7
8
 
@@ -109,7 +110,12 @@ def handle_object_response(node: "Node", peer: "Peer", message: Message) -> None
109
110
  sender=node.relay_public_key,
110
111
  )
111
112
  obj_req_msg.encrypt(peer.shared_key_bytes)
112
- node.outgoing_queue.put((obj_req_msg.to_bytes(), (provider_address, provider_port)))
113
+ enqueue_outgoing(
114
+ node,
115
+ (provider_address, provider_port),
116
+ message=obj_req_msg,
117
+ difficulty=1,
118
+ )
113
119
 
114
120
  case ObjectResponseType.OBJECT_NEAREST_PEER:
115
121
  node.logger.debug("Ignoring OBJECT_NEAREST_PEER response from %s", peer.address)
@@ -20,6 +20,15 @@ def handle_ping(node: "Node", peer: Peer, payload: bytes) -> None:
20
20
 
21
21
  peer.timestamp = datetime.now(timezone.utc)
22
22
  peer.latest_block = ping.latest_block
23
+ peer.difficulty = ping.difficulty
24
+ if peer.is_default_seed and ping.latest_block:
25
+ if getattr(node, "latest_block_hash", None) != ping.latest_block:
26
+ node.latest_block_hash = ping.latest_block
27
+ node.latest_block = None
28
+ node.logger.info(
29
+ "Updated latest block hash from default seed %s",
30
+ peer.address[0] if peer.address else "unknown",
31
+ )
23
32
 
24
33
  validation_route = node.validation_route
25
34
  if validation_route is None:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import socket
4
4
 
5
+ from ..outgoing_queue import enqueue_outgoing
5
6
  from ..models.message import Message, MessageTopic
6
7
  from ..util import xor_distance
7
8
 
@@ -73,4 +74,9 @@ def handle_route_request(node: "Node", peer: "Peer", message: Message) -> None:
73
74
  sender=node.relay_public_key,
74
75
  )
75
76
  response.encrypt(peer.shared_key_bytes)
76
- node.outgoing_queue.put((response.to_bytes(), peer.address))
77
+ enqueue_outgoing(
78
+ node,
79
+ peer.address,
80
+ message=response,
81
+ difficulty=peer.difficulty,
82
+ )
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import socket
4
4
 
5
+ from ..outgoing_queue import enqueue_outgoing
5
6
  from ..models.message import Message
6
7
 
7
8
  from typing import TYPE_CHECKING
@@ -50,4 +51,9 @@ def handle_route_response(node: "Node", peer: "Peer", message: Message) -> None:
50
51
  content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
51
52
  )
52
53
  for host, port in decoded_addresses:
53
- node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))
54
+ enqueue_outgoing(
55
+ node,
56
+ (host, port),
57
+ message=handshake_message,
58
+ difficulty=1,
59
+ )
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Tuple
4
+
5
+ from blake3 import blake3
6
+
7
+ from .difficulty import message_difficulty
8
+ from .message_pow import NONCE_SIZE, _leading_zero_bits
9
+
10
+ if TYPE_CHECKING:
11
+ from .. import Node
12
+
13
+
14
+ INCOMING_QUEUE_ITEM_OVERHEAD_BYTES = 6
15
+
16
+
17
+ def enqueue_incoming(
18
+ node: "Node",
19
+ address: Tuple[str, int],
20
+ payload: bytes,
21
+ ) -> bool:
22
+ """Enqueue an incoming UDP payload while tracking queued bytes.
23
+ Increments `node.incoming_queue_size` by `len(payload) + 6` and enforces
24
+ `node.incoming_queue_size_limit` (bytes) as a soft cap by dropping enqueues that
25
+ would exceed the limit. If `node.incoming_queue_timeout` is > 0, it waits up to
26
+ that many seconds (using `communication_stop_event.wait`) for space before dropping.
27
+ """
28
+ required_difficulty = message_difficulty(node)
29
+ if len(payload) <= NONCE_SIZE:
30
+ node.logger.warning(
31
+ "Incoming payload too short for difficulty check (len=%s, required=%s)",
32
+ len(payload),
33
+ required_difficulty,
34
+ )
35
+ return False
36
+
37
+ nonce_bytes = payload[:NONCE_SIZE]
38
+ message_bytes = payload[NONCE_SIZE:]
39
+ digest = blake3(message_bytes + nonce_bytes).digest()
40
+ zeros = _leading_zero_bits(digest)
41
+ if zeros < required_difficulty:
42
+ node.logger.warning(
43
+ "Incoming payload failed difficulty check (zeros=%s required=%s bytes=%s)",
44
+ zeros,
45
+ required_difficulty,
46
+ len(payload),
47
+ )
48
+ return False
49
+
50
+ accounted_size = len(payload) + INCOMING_QUEUE_ITEM_OVERHEAD_BYTES
51
+ timeout = float(node.incoming_queue_timeout or 0)
52
+
53
+ with node.incoming_queue_size_lock:
54
+ current_size = int(node.incoming_queue_size)
55
+ limit = int(node.incoming_queue_size_limit)
56
+ projected_size = current_size + accounted_size
57
+ if projected_size > limit:
58
+ if timeout <= 0:
59
+ node.logger.warning(
60
+ "Incoming queue size limit reached (%s > %s); dropping inbound payload (bytes=%s)",
61
+ projected_size,
62
+ limit,
63
+ len(payload),
64
+ )
65
+ return False
66
+ wait_for_space = True
67
+ else:
68
+ node.incoming_queue_size = projected_size
69
+ wait_for_space = False
70
+
71
+ if wait_for_space:
72
+ if node.communication_stop_event.wait(timeout):
73
+ return False
74
+ with node.incoming_queue_size_lock:
75
+ current_size = int(node.incoming_queue_size)
76
+ limit = int(node.incoming_queue_size_limit)
77
+ projected_size = current_size + accounted_size
78
+ if projected_size > limit:
79
+ node.logger.warning(
80
+ "Incoming queue still full after waiting %ss (%s > %s); dropping inbound payload (bytes=%s)",
81
+ timeout,
82
+ projected_size,
83
+ limit,
84
+ len(payload),
85
+ )
86
+ return False
87
+ node.incoming_queue_size = projected_size
88
+
89
+ try:
90
+ node.incoming_queue.put((message_bytes, address, accounted_size))
91
+ except Exception:
92
+ with node.incoming_queue_size_lock:
93
+ node.incoming_queue_size = max(0, int(node.incoming_queue_size) - accounted_size)
94
+ raise
95
+
96
+ return True
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from blake3 import blake3
4
+
5
+ NONCE_SIZE = 8
6
+ MAX_MESSAGE_NONCE = (1 << (NONCE_SIZE * 8)) - 1
7
+
8
+
9
+ def _leading_zero_bits(buf: bytes) -> int:
10
+ """Return the number of leading zero bits in the provided buffer."""
11
+ zeros = 0
12
+ for byte in buf:
13
+ if byte == 0:
14
+ zeros += 8
15
+ continue
16
+ zeros += 8 - int(byte).bit_length()
17
+ break
18
+ return zeros
19
+
20
+
21
+ def calculate_message_nonce(message_bytes: bytes, difficulty: int) -> int:
22
+ """Find a nonce such that blake3(message_bytes + nonce_bytes) meets difficulty.
23
+
24
+ message_bytes should exclude any nonce prefix that will be added on the wire.
25
+ """
26
+ target = max(1, int(difficulty))
27
+ nonce = 0
28
+ message_bytes = bytes(message_bytes)
29
+ while True:
30
+ if nonce > MAX_MESSAGE_NONCE:
31
+ raise ValueError("nonce search exhausted")
32
+ nonce_bytes = int(nonce).to_bytes(NONCE_SIZE, "big", signed=False)
33
+ digest = blake3(message_bytes + nonce_bytes).digest()
34
+ if _leading_zero_bits(digest) >= target:
35
+ return nonce
36
+ nonce += 1
@@ -13,11 +13,15 @@ class Peer:
13
13
  peer_public_key: X25519PublicKey,
14
14
  latest_block: Optional[bytes] = None,
15
15
  address: Optional[Tuple[str, int]] = None,
16
+ is_default_seed: bool = False,
17
+ difficulty: int = 1,
16
18
  ):
17
19
  self.shared_key_bytes = node_secret_key.exchange(peer_public_key)
18
20
  self.timestamp = datetime.now(timezone.utc)
19
21
  self.latest_block = latest_block
22
+ self.difficulty = max(1, int(difficulty or 1))
20
23
  self.address = address
24
+ self.is_default_seed = bool(is_default_seed)
21
25
  self.public_key_bytes = peer_public_key.public_bytes(
22
26
  encoding=serialization.Encoding.Raw,
23
27
  format=serialization.PublicFormat.Raw,
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ from typing import Optional
4
5
 
5
6
 
6
7
  class PingFormatError(ValueError):
@@ -10,24 +11,44 @@ class PingFormatError(ValueError):
10
11
  @dataclass
11
12
  class Ping:
12
13
  is_validator: bool
13
- latest_block: bytes
14
+ difficulty: int
15
+ latest_block: Optional[bytes]
14
16
 
15
- PAYLOAD_SIZE = 33
17
+ PAYLOAD_SIZE = 34
18
+ ZERO_BLOCK = b"\x00" * 32
16
19
 
17
20
  def __post_init__(self) -> None:
18
- lb = bytes(self.latest_block or b"")
21
+ self.difficulty = int(self.difficulty)
22
+ if self.difficulty < 1 or self.difficulty > 255:
23
+ raise ValueError("difficulty must be between 1 and 255")
24
+ if self.latest_block is None:
25
+ return
26
+ lb = bytes(self.latest_block)
19
27
  if len(lb) != 32:
20
28
  raise ValueError("latest_block must be exactly 32 bytes")
21
29
  self.latest_block = lb
22
30
 
23
31
  def to_bytes(self) -> bytes:
24
- return (b"\x01" if self.is_validator else b"\x00") + self.latest_block
32
+ flag = b"\x01" if self.is_validator else b"\x00"
33
+ difficulty = bytes([self.difficulty])
34
+ latest_block = self.latest_block if self.latest_block is not None else self.ZERO_BLOCK
35
+ return flag + difficulty + latest_block
25
36
 
26
37
  @classmethod
27
38
  def from_bytes(cls, data: bytes) -> "Ping":
28
39
  if len(data) != cls.PAYLOAD_SIZE:
29
- raise PingFormatError("ping payload must be exactly 33 bytes")
40
+ raise PingFormatError("ping payload must be 34 bytes")
30
41
  flag = data[0]
31
42
  if flag not in (0, 1):
32
43
  raise PingFormatError("ping validator flag must be 0 or 1")
33
- return cls(is_validator=bool(flag), latest_block=data[1:])
44
+ difficulty = data[1]
45
+ if difficulty < 1:
46
+ raise PingFormatError("ping difficulty must be >= 1")
47
+ latest_block = data[2:]
48
+ if latest_block == cls.ZERO_BLOCK:
49
+ latest_block = None
50
+ return cls(
51
+ is_validator=bool(flag),
52
+ difficulty=difficulty,
53
+ latest_block=latest_block,
54
+ )
@@ -42,6 +42,8 @@ class Route:
42
42
 
43
43
  def add_peer(self, peer_public_key: PeerKey, peer: Optional[Peer] = None):
44
44
  peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
45
+ if peer_public_key_bytes == self.relay_public_key_bytes:
46
+ return
45
47
  bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
46
48
  if len(self.buckets[bucket_idx]) < self.bucket_size:
47
49
  bucket = self.buckets[bucket_idx]
@@ -52,6 +54,8 @@ class Route:
52
54
 
53
55
  def remove_peer(self, peer_public_key: PeerKey):
54
56
  peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
57
+ if peer_public_key_bytes == self.relay_public_key_bytes:
58
+ return
55
59
  bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
56
60
  bucket = self.buckets.get(bucket_idx)
57
61
  if not bucket: