astreum 0.3.1__py3-none-any.whl → 0.3.16__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 (38) hide show
  1. astreum/__init__.py +4 -2
  2. astreum/communication/handlers/handshake.py +62 -83
  3. astreum/communication/handlers/object_request.py +176 -0
  4. astreum/communication/handlers/object_response.py +115 -0
  5. astreum/communication/handlers/ping.py +6 -20
  6. astreum/communication/handlers/route_request.py +76 -0
  7. astreum/communication/handlers/route_response.py +53 -0
  8. astreum/communication/models/message.py +81 -58
  9. astreum/communication/models/peer.py +42 -14
  10. astreum/communication/models/route.py +2 -7
  11. astreum/communication/processors/__init__.py +0 -0
  12. astreum/communication/processors/incoming.py +98 -0
  13. astreum/communication/processors/outgoing.py +20 -0
  14. astreum/communication/processors/peer.py +59 -0
  15. astreum/communication/setup.py +39 -76
  16. astreum/communication/start.py +9 -10
  17. astreum/communication/util.py +7 -0
  18. astreum/consensus/start.py +9 -10
  19. astreum/consensus/validator.py +17 -8
  20. astreum/consensus/workers/discovery.py +6 -7
  21. astreum/consensus/workers/validation.py +334 -291
  22. astreum/consensus/workers/verify.py +8 -10
  23. astreum/crypto/chacha20poly1305.py +74 -0
  24. astreum/machine/evaluations/high_evaluation.py +237 -237
  25. astreum/machine/evaluations/low_evaluation.py +18 -18
  26. astreum/node.py +29 -7
  27. astreum/storage/actions/get.py +183 -69
  28. astreum/storage/actions/set.py +66 -20
  29. astreum/storage/requests.py +28 -0
  30. astreum/storage/setup.py +3 -25
  31. astreum/utils/config.py +76 -0
  32. {astreum-0.3.1.dist-info → astreum-0.3.16.dist-info}/METADATA +3 -3
  33. astreum-0.3.16.dist-info/RECORD +72 -0
  34. astreum/communication/handlers/storage_request.py +0 -81
  35. astreum-0.3.1.dist-info/RECORD +0 -62
  36. {astreum-0.3.1.dist-info → astreum-0.3.16.dist-info}/WHEEL +0 -0
  37. {astreum-0.3.1.dist-info → astreum-0.3.16.dist-info}/licenses/LICENSE +0 -0
  38. {astreum-0.3.1.dist-info → astreum-0.3.16.dist-info}/top_level.txt +0 -0
astreum/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
-
1
+
2
2
  from astreum.consensus import Account, Accounts, Block, Chain, Fork, Receipt, Transaction
3
- from astreum.machine import Env, Expr
3
+ from astreum.machine import Env, Expr, parse, tokenize
4
4
  from astreum.node import Node
5
5
 
6
6
 
@@ -15,4 +15,6 @@ __all__: list[str] = [
15
15
  "Transaction",
16
16
  "Account",
17
17
  "Accounts",
18
+ "parse",
19
+ "tokenize",
18
20
  ]
@@ -1,83 +1,62 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Sequence
4
-
5
- from cryptography.hazmat.primitives import serialization
6
-
7
- from ..models.peer import Peer
8
- from ..models.message import Message
9
-
10
- if TYPE_CHECKING:
11
- from .... import Node
12
-
13
-
14
- def handle_handshake(node: "Node", addr: Sequence[object], message: Message) -> bool:
15
- """Handle incoming handshake messages.
16
-
17
- Returns True if the outer loop should `continue`, False otherwise.
18
- """
19
- logger = node.logger
20
-
21
- sender_key = message.sender
22
- try:
23
- sender_public_key_bytes = sender_key.public_bytes(
24
- encoding=serialization.Encoding.Raw,
25
- format=serialization.PublicFormat.Raw,
26
- )
27
- except Exception as exc:
28
- logger.warning("Error extracting sender key bytes: %s", exc)
29
- return True
30
-
31
- try:
32
- host, port = addr[0], int(addr[1])
33
- except Exception:
34
- return True
35
- address_key = (host, port)
36
-
37
- old_key_bytes = node.addresses.get(address_key)
38
- node.addresses[address_key] = sender_public_key_bytes
39
-
40
- if old_key_bytes is None:
41
- try:
42
- peer = Peer(node.relay_secret_key, sender_key)
43
- except Exception:
44
- return True
45
- peer.address = address_key
46
-
47
- node.peers[sender_public_key_bytes] = peer
48
- node.peer_route.add_peer(sender_public_key_bytes, peer)
49
-
50
- logger.info(
51
- "Handshake accepted from %s:%s; peer added",
52
- address_key[0],
53
- address_key[1],
54
- )
55
- response = Message(handshake=True, sender=node.relay_public_key)
56
- node.outgoing_queue.put((response.to_bytes(), address_key))
57
- return True
58
-
59
- if old_key_bytes == sender_public_key_bytes:
60
- peer = node.peers.get(sender_public_key_bytes)
61
- if peer is not None:
62
- peer.address = address_key
63
- return False
64
-
65
- node.peers.pop(old_key_bytes, None)
66
- try:
67
- node.peer_route.remove_peer(old_key_bytes)
68
- except Exception:
69
- pass
70
- try:
71
- peer = Peer(node.relay_secret_key, sender_key)
72
- except Exception:
73
- return True
74
- peer.address = address_key
75
-
76
- node.peers[sender_public_key_bytes] = peer
77
- node.peer_route.add_peer(sender_public_key_bytes, peer)
78
- logger.info(
79
- "Peer at %s:%s replaced due to key change",
80
- address_key[0],
81
- address_key[1],
82
- )
83
- return False
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
+ existing_peer = node.get_peer(sender_public_key_bytes)
35
+ if existing_peer is not None:
36
+ existing_peer.address = peer_address
37
+ return False
38
+
39
+ try:
40
+ peer = Peer(
41
+ node_secret_key=node.relay_secret_key,
42
+ peer_public_key=sender_key,
43
+ address=peer_address,
44
+ )
45
+ except Exception:
46
+ return True
47
+
48
+ node.add_peer(sender_public_key_bytes, peer)
49
+ node.peer_route.add_peer(sender_public_key_bytes, peer)
50
+
51
+ node.logger.info(
52
+ "Handshake accepted from %s:%s; peer added",
53
+ peer_address[0],
54
+ peer_address[1],
55
+ )
56
+ response = Message(
57
+ handshake=True,
58
+ sender=node.relay_public_key,
59
+ content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
60
+ )
61
+ node.outgoing_queue.put((response.to_bytes(), peer_address))
62
+ return True
@@ -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, Sequence
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", addr: Sequence[object], payload: bytes) -> None:
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(sender_public_key_bytes)
30
+ validation_route.add_peer(peer.public_key_bytes)
45
31
  else:
46
- validation_route.remove_peer(sender_public_key_bytes)
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)))