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.
- astreum/__init__.py +5 -4
- astreum/communication/__init__.py +15 -11
- astreum/communication/difficulty.py +39 -0
- astreum/communication/disconnect.py +57 -0
- astreum/communication/handlers/handshake.py +105 -89
- astreum/communication/handlers/object_request.py +179 -149
- astreum/communication/handlers/object_response.py +7 -1
- 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 +134 -0
- astreum/communication/setup.py +273 -112
- astreum/communication/util.py +14 -0
- astreum/node.py +99 -89
- astreum/storage/actions/get.py +79 -48
- astreum/storage/actions/set.py +171 -156
- astreum/storage/providers.py +24 -0
- astreum/storage/setup.py +23 -22
- astreum/utils/config.py +247 -30
- 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/validation/models/block.py +544 -0
- astreum/validation/models/fork.py +511 -0
- astreum/{consensus → validation}/models/receipt.py +17 -4
- astreum/{consensus → validation}/models/transaction.py +45 -3
- astreum/validation/node.py +190 -0
- astreum/{consensus → validation}/validator.py +18 -9
- astreum/validation/workers/__init__.py +8 -0
- astreum/{consensus → validation}/workers/validation.py +361 -307
- 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.9.dist-info → astreum-0.3.46.dist-info}/METADATA +43 -9
- astreum-0.3.46.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.9.dist-info/RECORD +0 -71
- /astreum/{consensus → validation}/models/__init__.py +0 -0
- /astreum/{consensus → validation}/models/account.py +0 -0
- /astreum/{consensus → validation}/models/accounts.py +0 -0
- {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/WHEEL +0 -0
- {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
11
|
-
|
|
12
|
-
from ..
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
self.
|
|
28
|
-
self.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
+
difficulty: int
|
|
15
|
+
latest_block: Optional[bytes]
|
|
14
16
|
|
|
15
|
-
PAYLOAD_SIZE =
|
|
17
|
+
PAYLOAD_SIZE = 34
|
|
18
|
+
ZERO_BLOCK = b"\x00" * 32
|
|
16
19
|
|
|
17
20
|
def __post_init__(self) -> None:
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|