astreum 0.3.1__py3-none-any.whl → 0.3.9__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/communication/handlers/handshake.py +89 -83
- astreum/communication/handlers/object_request.py +176 -0
- astreum/communication/handlers/object_response.py +115 -0
- astreum/communication/handlers/ping.py +6 -20
- astreum/communication/handlers/route_request.py +76 -0
- astreum/communication/handlers/route_response.py +53 -0
- astreum/communication/models/message.py +81 -58
- astreum/communication/models/peer.py +42 -14
- astreum/communication/models/route.py +2 -7
- astreum/communication/processors/__init__.py +0 -0
- astreum/communication/processors/incoming.py +98 -0
- astreum/communication/processors/outgoing.py +20 -0
- astreum/communication/setup.py +36 -75
- astreum/communication/start.py +9 -10
- astreum/communication/util.py +7 -0
- astreum/consensus/start.py +9 -10
- astreum/consensus/workers/discovery.py +6 -7
- astreum/consensus/workers/validation.py +307 -291
- astreum/consensus/workers/verify.py +8 -10
- astreum/crypto/chacha20poly1305.py +74 -0
- astreum/machine/evaluations/high_evaluation.py +237 -237
- astreum/machine/evaluations/low_evaluation.py +18 -18
- astreum/node.py +25 -6
- astreum/storage/actions/get.py +183 -69
- astreum/storage/actions/set.py +66 -20
- astreum/storage/requests.py +28 -0
- astreum/storage/setup.py +3 -25
- astreum/utils/config.py +48 -0
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/METADATA +3 -3
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/RECORD +33 -24
- astreum/communication/handlers/storage_request.py +0 -81
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/top_level.txt +0 -0
|
@@ -1,83 +1,89 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Sequence
|
|
4
|
-
|
|
5
|
-
from cryptography.hazmat.primitives import serialization
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from ..models.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
old_key_bytes
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
node.
|
|
48
|
-
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
49
|
-
|
|
50
|
-
logger.info(
|
|
51
|
-
"Handshake accepted from %s:%s; peer added",
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
response = Message(
|
|
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
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Sequence
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives import serialization
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
7
|
+
|
|
8
|
+
from ..models.peer import Peer
|
|
9
|
+
from ..models.message import Message
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .... import Node
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handle_handshake(node: "Node", addr: Sequence[object], message: Message) -> bool:
|
|
16
|
+
"""Handle incoming handshake messages.
|
|
17
|
+
|
|
18
|
+
Returns True if the outer loop should `continue`, False otherwise.
|
|
19
|
+
"""
|
|
20
|
+
sender_public_key_bytes = message.sender_bytes
|
|
21
|
+
try:
|
|
22
|
+
sender_key = X25519PublicKey.from_public_bytes(sender_public_key_bytes)
|
|
23
|
+
except Exception as exc:
|
|
24
|
+
node.logger.warning("Error extracting sender key bytes: %s", exc)
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
host = addr[0]
|
|
29
|
+
port = int.from_bytes(message.content[:2], "big", signed=False)
|
|
30
|
+
except Exception:
|
|
31
|
+
return True
|
|
32
|
+
peer_address = (host, port)
|
|
33
|
+
|
|
34
|
+
old_key_bytes = node.addresses.get(peer_address)
|
|
35
|
+
node.addresses[peer_address] = sender_public_key_bytes
|
|
36
|
+
|
|
37
|
+
if old_key_bytes is None:
|
|
38
|
+
try:
|
|
39
|
+
peer = Peer(
|
|
40
|
+
node_secret_key=node.relay_secret_key,
|
|
41
|
+
peer_public_key=sender_key,
|
|
42
|
+
address=peer_address,
|
|
43
|
+
)
|
|
44
|
+
except Exception:
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
node.add_peer(sender_public_key_bytes, peer)
|
|
48
|
+
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
49
|
+
|
|
50
|
+
node.logger.info(
|
|
51
|
+
"Handshake accepted from %s:%s; peer added",
|
|
52
|
+
peer_address[0],
|
|
53
|
+
peer_address[1],
|
|
54
|
+
)
|
|
55
|
+
response = Message(
|
|
56
|
+
handshake=True,
|
|
57
|
+
sender=node.relay_public_key,
|
|
58
|
+
content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
|
|
59
|
+
)
|
|
60
|
+
node.outgoing_queue.put((response.to_bytes(), peer_address))
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
if old_key_bytes == sender_public_key_bytes:
|
|
64
|
+
peer = node.get_peer(sender_public_key_bytes)
|
|
65
|
+
if peer is not None:
|
|
66
|
+
peer.address = peer_address
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
node.peer_route.remove_peer(old_key_bytes)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
try:
|
|
74
|
+
peer = Peer(
|
|
75
|
+
node_secret_key=node.relay_secret_key,
|
|
76
|
+
peer_public_key=sender_key,
|
|
77
|
+
address=peer_address,
|
|
78
|
+
)
|
|
79
|
+
except Exception:
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
node.replace_peer(old_key_bytes, sender_public_key_bytes, peer)
|
|
83
|
+
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
84
|
+
node.logger.info(
|
|
85
|
+
"Peer at %s:%s replaced due to key change",
|
|
86
|
+
peer_address[0],
|
|
87
|
+
peer_address[1],
|
|
88
|
+
)
|
|
89
|
+
return False
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import socket
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
from typing import TYPE_CHECKING, Tuple
|
|
5
|
+
|
|
6
|
+
from .object_response import ObjectResponse, ObjectResponseType
|
|
7
|
+
from ..models.message import Message, MessageTopic
|
|
8
|
+
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
|
+
)
|
|
88
|
+
obj_res_msg = Message(
|
|
89
|
+
topic=MessageTopic.OBJECT_RESPONSE,
|
|
90
|
+
body=resp.to_bytes(),
|
|
91
|
+
sender=node.relay_public_key,
|
|
92
|
+
)
|
|
93
|
+
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
|
|
104
|
+
)
|
|
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
|
+
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
|
+
)
|
|
124
|
+
obj_res_msg = Message(
|
|
125
|
+
topic=MessageTopic.OBJECT_RESPONSE,
|
|
126
|
+
body=resp.to_bytes(),
|
|
127
|
+
sender=node.relay_public_key,
|
|
128
|
+
)
|
|
129
|
+
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,
|
|
166
|
+
)
|
|
167
|
+
obj_req_msg = Message(
|
|
168
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
169
|
+
body=fwd_req.to_bytes(),
|
|
170
|
+
sender=node.relay_public_key,
|
|
171
|
+
)
|
|
172
|
+
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)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from typing import Tuple, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ..models.message import Message, MessageTopic
|
|
6
|
+
from ...storage.models.atom import Atom
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .. import Node
|
|
10
|
+
from ..models.peer import Peer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ObjectResponseType(IntEnum):
|
|
14
|
+
OBJECT_FOUND = 0
|
|
15
|
+
OBJECT_PROVIDER = 1
|
|
16
|
+
OBJECT_NEAREST_PEER = 2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ObjectResponse:
|
|
20
|
+
type: ObjectResponseType
|
|
21
|
+
data: bytes
|
|
22
|
+
atom_id: bytes
|
|
23
|
+
|
|
24
|
+
def __init__(self, type: ObjectResponseType, data: bytes, atom_id: bytes = None):
|
|
25
|
+
self.type = type
|
|
26
|
+
self.data = data
|
|
27
|
+
self.atom_id = atom_id
|
|
28
|
+
|
|
29
|
+
def to_bytes(self):
|
|
30
|
+
return bytes([self.type.value]) + self.atom_id + self.data
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_bytes(cls, data: bytes) -> "ObjectResponse":
|
|
34
|
+
# need at least 1 byte for type + 32 bytes for atom id
|
|
35
|
+
if len(data) < 1 + 32:
|
|
36
|
+
raise ValueError(f"Too short to be a valid ObjectResponse ({len(data)} bytes)")
|
|
37
|
+
|
|
38
|
+
type_val = data[0]
|
|
39
|
+
try:
|
|
40
|
+
resp_type = ObjectResponseType(type_val)
|
|
41
|
+
except ValueError:
|
|
42
|
+
raise ValueError(f"Unknown ObjectResponseType: {type_val}")
|
|
43
|
+
|
|
44
|
+
atom_id = data[1:33]
|
|
45
|
+
payload = data[33:]
|
|
46
|
+
return cls(resp_type, payload, atom_id)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def decode_object_provider(payload: bytes) -> Tuple[bytes, str, int]:
|
|
50
|
+
expected_len = 32 + 4 + 2
|
|
51
|
+
if len(payload) < expected_len:
|
|
52
|
+
raise ValueError("provider payload too short")
|
|
53
|
+
|
|
54
|
+
provider_public_key = payload[:32]
|
|
55
|
+
provider_ip_bytes = payload[32:36]
|
|
56
|
+
provider_port_bytes = payload[36:38]
|
|
57
|
+
|
|
58
|
+
provider_address = socket.inet_ntoa(provider_ip_bytes)
|
|
59
|
+
provider_port = int.from_bytes(provider_port_bytes, byteorder="big", signed=False)
|
|
60
|
+
return provider_public_key, provider_address, provider_port
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def handle_object_response(node: "Node", peer: "Peer", message: Message) -> None:
|
|
64
|
+
if message.content is None:
|
|
65
|
+
node.logger.warning("OBJECT_RESPONSE from %s missing content", peer.address)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
object_response = ObjectResponse.from_bytes(message.content)
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
node.logger.warning("Error decoding OBJECT_RESPONSE from %s: %s", peer.address, exc)
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if not node.has_atom_req(object_response.atom_id):
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
match object_response.type:
|
|
78
|
+
case ObjectResponseType.OBJECT_FOUND:
|
|
79
|
+
atom = Atom.from_bytes(object_response.data)
|
|
80
|
+
atom_id = atom.object_id()
|
|
81
|
+
if object_response.atom_id == atom_id:
|
|
82
|
+
node.pop_atom_req(atom_id)
|
|
83
|
+
node._hot_storage_set(atom_id, atom)
|
|
84
|
+
else:
|
|
85
|
+
node.logger.warning(
|
|
86
|
+
"OBJECT_FOUND atom ID mismatch (expected=%s got=%s)",
|
|
87
|
+
object_response.atom_id.hex(),
|
|
88
|
+
atom_id.hex(),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
case ObjectResponseType.OBJECT_PROVIDER:
|
|
92
|
+
try:
|
|
93
|
+
_, provider_address, provider_port = decode_object_provider(object_response.data)
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
node.logger.warning("Invalid OBJECT_PROVIDER payload from %s: %s", peer.address, exc)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
from .object_request import ObjectRequest, ObjectRequestType
|
|
99
|
+
|
|
100
|
+
obj_req = ObjectRequest(
|
|
101
|
+
type=ObjectRequestType.OBJECT_GET,
|
|
102
|
+
data=b"",
|
|
103
|
+
atom_id=object_response.atom_id,
|
|
104
|
+
)
|
|
105
|
+
obj_req_bytes = obj_req.to_bytes()
|
|
106
|
+
obj_req_msg = Message(
|
|
107
|
+
topic=MessageTopic.OBJECT_REQUEST,
|
|
108
|
+
body=obj_req_bytes,
|
|
109
|
+
sender=node.relay_public_key,
|
|
110
|
+
)
|
|
111
|
+
obj_req_msg.encrypt(peer.shared_key_bytes)
|
|
112
|
+
node.outgoing_queue.put((obj_req_msg.to_bytes(), (provider_address, provider_port)))
|
|
113
|
+
|
|
114
|
+
case ObjectResponseType.OBJECT_NEAREST_PEER:
|
|
115
|
+
node.logger.debug("Ignoring OBJECT_NEAREST_PEER response from %s", peer.address)
|
|
@@ -1,35 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from ..models.ping import Ping
|
|
7
|
+
from ..models.peer import Peer
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING:
|
|
9
10
|
from .... import Node
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def handle_ping(node: "Node",
|
|
13
|
+
def handle_ping(node: "Node", peer: Peer, payload: bytes) -> None:
|
|
13
14
|
"""Update peer and validation state based on an incoming ping message."""
|
|
14
|
-
logger = node.logger
|
|
15
|
-
try:
|
|
16
|
-
host, port = addr[0], int(addr[1])
|
|
17
|
-
except Exception:
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
address_key = (host, port)
|
|
21
|
-
sender_public_key_bytes = node.addresses.get(address_key)
|
|
22
|
-
if sender_public_key_bytes is None:
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
peer = node.peers.get(sender_public_key_bytes)
|
|
26
|
-
if peer is None:
|
|
27
|
-
return
|
|
28
|
-
|
|
29
15
|
try:
|
|
30
16
|
ping = Ping.from_bytes(payload)
|
|
31
17
|
except Exception as exc:
|
|
32
|
-
logger.warning("Error decoding ping: %s", exc)
|
|
18
|
+
node.logger.warning("Error decoding ping: %s", exc)
|
|
33
19
|
return
|
|
34
20
|
|
|
35
21
|
peer.timestamp = datetime.now(timezone.utc)
|
|
@@ -41,8 +27,8 @@ def handle_ping(node: "Node", addr: Sequence[object], payload: bytes) -> None:
|
|
|
41
27
|
|
|
42
28
|
try:
|
|
43
29
|
if ping.is_validator:
|
|
44
|
-
validation_route.add_peer(
|
|
30
|
+
validation_route.add_peer(peer.public_key_bytes)
|
|
45
31
|
else:
|
|
46
|
-
validation_route.remove_peer(
|
|
32
|
+
validation_route.remove_peer(peer.public_key_bytes)
|
|
47
33
|
except Exception:
|
|
48
34
|
pass
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
|
|
5
|
+
from ..models.message import Message, MessageTopic
|
|
6
|
+
from ..util import xor_distance
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .... import Node
|
|
11
|
+
from ..models.peer import Peer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def handle_route_request(node: "Node", peer: "Peer", message: Message) -> None:
|
|
15
|
+
sender_public_key = getattr(peer, "public_key_bytes", None)
|
|
16
|
+
if not sender_public_key:
|
|
17
|
+
node.logger.warning("Unknown sender for ROUTE_REQUEST from %s", peer.address)
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
if not message.content:
|
|
21
|
+
node.logger.warning("ROUTE_REQUEST missing route id from %s", peer.address)
|
|
22
|
+
return
|
|
23
|
+
route_id = message.content[0]
|
|
24
|
+
if route_id == 0:
|
|
25
|
+
route = node.peer_route
|
|
26
|
+
elif route_id == 1:
|
|
27
|
+
route = node.validation_route
|
|
28
|
+
if route is None:
|
|
29
|
+
node.logger.warning("Validation route not initialized for %s", peer.address)
|
|
30
|
+
return
|
|
31
|
+
else:
|
|
32
|
+
node.logger.warning("Unknown route id %s in ROUTE_REQUEST from %s", route_id, peer.address)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
payload_parts = []
|
|
36
|
+
for bucket in route.buckets.values():
|
|
37
|
+
closest_key = None
|
|
38
|
+
closest_distance = None
|
|
39
|
+
|
|
40
|
+
for peer_key in bucket:
|
|
41
|
+
try:
|
|
42
|
+
distance = xor_distance(sender_public_key, peer_key)
|
|
43
|
+
except ValueError:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
if closest_distance is None or distance < closest_distance:
|
|
47
|
+
closest_distance = distance
|
|
48
|
+
closest_key = peer_key
|
|
49
|
+
|
|
50
|
+
if closest_key is None:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
bucket_peer = node.get_peer(closest_key)
|
|
54
|
+
if bucket_peer is None or bucket_peer.address is None:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
host, port = bucket_peer.address
|
|
58
|
+
try:
|
|
59
|
+
address_bytes = socket.inet_pton(socket.AF_INET, host)
|
|
60
|
+
except OSError:
|
|
61
|
+
try:
|
|
62
|
+
address_bytes = socket.inet_pton(socket.AF_INET6, host)
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
node.logger.warning("Invalid peer address %s: %s", bucket_peer.address, exc)
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
port_bytes = int(port).to_bytes(2, "big", signed=False)
|
|
68
|
+
payload_parts.append(address_bytes + port_bytes)
|
|
69
|
+
|
|
70
|
+
response = Message(
|
|
71
|
+
topic=MessageTopic.ROUTE_RESPONSE,
|
|
72
|
+
content=b"".join(payload_parts),
|
|
73
|
+
sender=node.relay_public_key,
|
|
74
|
+
)
|
|
75
|
+
response.encrypt(peer.shared_key_bytes)
|
|
76
|
+
node.outgoing_queue.put((response.to_bytes(), peer.address))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
|
|
5
|
+
from ..models.message import Message
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .... import Node
|
|
10
|
+
from ..models.peer import Peer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_route_response(node: "Node", peer: "Peer", message: Message) -> None:
|
|
14
|
+
payload = message.content
|
|
15
|
+
if not payload:
|
|
16
|
+
return
|
|
17
|
+
host_len = 16 if node.use_ipv6 else 4
|
|
18
|
+
chunk_size = host_len + 2
|
|
19
|
+
if len(payload) % chunk_size != 0:
|
|
20
|
+
node.logger.warning(
|
|
21
|
+
"ROUTE_RESPONSE payload size mismatch (%s bytes) from %s",
|
|
22
|
+
len(payload),
|
|
23
|
+
peer.address,
|
|
24
|
+
)
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
decoded_addresses = []
|
|
28
|
+
family = socket.AF_INET6 if node.use_ipv6 else socket.AF_INET
|
|
29
|
+
for index in range(0, len(payload), chunk_size):
|
|
30
|
+
host_bytes = payload[index : index + host_len]
|
|
31
|
+
port_bytes = payload[index + host_len : index + chunk_size]
|
|
32
|
+
try:
|
|
33
|
+
host = socket.inet_ntop(family, host_bytes)
|
|
34
|
+
except OSError as exc:
|
|
35
|
+
node.logger.warning(
|
|
36
|
+
"Invalid host bytes in ROUTE_RESPONSE from %s: %s",
|
|
37
|
+
peer.address,
|
|
38
|
+
exc,
|
|
39
|
+
)
|
|
40
|
+
continue
|
|
41
|
+
port = int.from_bytes(port_bytes, "big", signed=False)
|
|
42
|
+
decoded_addresses.append((host, port))
|
|
43
|
+
if not decoded_addresses:
|
|
44
|
+
return
|
|
45
|
+
node.logger.debug("Decoded %s addresses from ROUTE_RESPONSE", len(decoded_addresses))
|
|
46
|
+
|
|
47
|
+
handshake_message = Message(
|
|
48
|
+
handshake=True,
|
|
49
|
+
sender=node.relay_public_key,
|
|
50
|
+
content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
|
|
51
|
+
)
|
|
52
|
+
for host, port in decoded_addresses:
|
|
53
|
+
node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))
|