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.
- astreum/__init__.py +4 -2
- astreum/communication/handlers/handshake.py +62 -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/processors/peer.py +59 -0
- astreum/communication/setup.py +39 -76
- astreum/communication/start.py +9 -10
- astreum/communication/util.py +7 -0
- astreum/consensus/start.py +9 -10
- astreum/consensus/validator.py +17 -8
- astreum/consensus/workers/discovery.py +6 -7
- astreum/consensus/workers/validation.py +334 -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 +29 -7
- 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 +76 -0
- {astreum-0.3.1.dist-info → astreum-0.3.16.dist-info}/METADATA +3 -3
- astreum-0.3.16.dist-info/RECORD +72 -0
- astreum/communication/handlers/storage_request.py +0 -81
- astreum-0.3.1.dist-info/RECORD +0 -62
- {astreum-0.3.1.dist-info → astreum-0.3.16.dist-info}/WHEEL +0 -0
- {astreum-0.3.1.dist-info → astreum-0.3.16.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.3.1.dist-info → astreum-0.3.16.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from enum import IntEnum
|
|
2
3
|
from typing import Optional
|
|
3
4
|
from cryptography.hazmat.primitives import serialization
|
|
4
5
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
6
|
+
from astreum.crypto import chacha20poly1305
|
|
5
7
|
|
|
6
8
|
class MessageTopic(IntEnum):
|
|
7
9
|
PING = 0
|
|
@@ -10,92 +12,113 @@ class MessageTopic(IntEnum):
|
|
|
10
12
|
ROUTE_REQUEST = 3
|
|
11
13
|
ROUTE_RESPONSE = 4
|
|
12
14
|
TRANSACTION = 5
|
|
13
|
-
STORAGE_REQUEST = 6
|
|
14
|
-
|
|
15
15
|
|
|
16
|
-
class Message:
|
|
17
|
-
handshake: bool
|
|
18
|
-
sender: Optional[X25519PublicKey]
|
|
19
|
-
|
|
20
|
-
topic: Optional[MessageTopic]
|
|
21
|
-
content: bytes
|
|
22
16
|
|
|
17
|
+
class Message:
|
|
23
18
|
def __init__(
|
|
24
19
|
self,
|
|
25
20
|
*,
|
|
26
21
|
handshake: bool = False,
|
|
27
22
|
sender: Optional[X25519PublicKey] = None,
|
|
28
23
|
topic: Optional[MessageTopic] = None,
|
|
29
|
-
content: bytes =
|
|
24
|
+
content: Optional[bytes] = None,
|
|
30
25
|
body: Optional[bytes] = None,
|
|
26
|
+
sender_bytes: Optional[bytes] = None,
|
|
27
|
+
encrypted: Optional[bytes] = None,
|
|
31
28
|
) -> None:
|
|
32
29
|
if body is not None:
|
|
33
|
-
if content and content != b"":
|
|
30
|
+
if content is not None and content != b"":
|
|
34
31
|
raise ValueError("specify only one of 'content' or 'body'")
|
|
35
32
|
content = body
|
|
36
33
|
|
|
37
34
|
self.handshake = handshake
|
|
38
|
-
self.sender = sender
|
|
39
35
|
self.topic = topic
|
|
40
|
-
self.content = content
|
|
36
|
+
self.content = content if content is not None else b""
|
|
37
|
+
self.encrypted = encrypted
|
|
41
38
|
|
|
42
39
|
if self.handshake:
|
|
43
|
-
if
|
|
44
|
-
raise ValueError("handshake Message requires a sender public key")
|
|
40
|
+
if sender_bytes is None and sender is None:
|
|
41
|
+
raise ValueError("handshake Message requires a sender public key or sender bytes")
|
|
45
42
|
self.topic = None
|
|
46
|
-
self.content = b""
|
|
47
43
|
else:
|
|
48
|
-
if self.topic is None:
|
|
49
|
-
raise ValueError("non-handshake Message requires a topic")
|
|
44
|
+
if self.topic is None and self.encrypted is None:
|
|
45
|
+
raise ValueError("non-handshake Message requires a topic or encrypted payload")
|
|
46
|
+
if sender_bytes is None and sender is None:
|
|
47
|
+
raise ValueError("non-handshake Message requires a sender public key or sender bytes")
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
if sender_bytes is not None:
|
|
50
|
+
self.sender_bytes = sender_bytes
|
|
51
|
+
else:
|
|
52
|
+
if sender is None:
|
|
53
|
+
raise ValueError("sender public key required to derive sender bytes")
|
|
54
|
+
self.sender_bytes = sender.public_bytes(
|
|
55
55
|
encoding=serialization.Encoding.Raw,
|
|
56
|
-
format=serialization.PublicFormat.Raw
|
|
56
|
+
format=serialization.PublicFormat.Raw,
|
|
57
57
|
)
|
|
58
|
+
|
|
59
|
+
def to_bytes(self):
|
|
60
|
+
if self.handshake:
|
|
61
|
+
# handshake byte (1) + raw public key bytes + payload
|
|
62
|
+
return bytes([1]) + self.sender_bytes + self.content
|
|
58
63
|
else:
|
|
59
|
-
# normal message: 0 +
|
|
60
|
-
|
|
64
|
+
# normal message: 0 + sender + encrypted payload (nonce + ciphertext)
|
|
65
|
+
if not self.encrypted:
|
|
66
|
+
raise ValueError("non-handshake Message missing encrypted payload; call encrypt() first")
|
|
67
|
+
return bytes([0]) + self.sender_bytes + self.encrypted
|
|
61
68
|
|
|
62
69
|
@classmethod
|
|
63
70
|
def from_bytes(cls, data: bytes) -> "Message":
|
|
64
71
|
if len(data) < 1:
|
|
65
72
|
raise ValueError("Cannot parse Message: no data")
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
except ValueError:
|
|
78
|
-
raise ValueError("Invalid public key bytes")
|
|
79
|
-
if sender is None:
|
|
80
|
-
raise ValueError("Handshake message missing sender public key")
|
|
81
|
-
msg.handshake = True
|
|
82
|
-
msg.sender = sender
|
|
83
|
-
msg.topic = None
|
|
84
|
-
msg.content = b''
|
|
85
|
-
elif flag == 0:
|
|
86
|
-
# normal message: next byte is topic, rest is content
|
|
87
|
-
if len(data) < 2:
|
|
88
|
-
raise ValueError("Cannot parse Message: missing topic byte")
|
|
89
|
-
topic_val = data[1]
|
|
90
|
-
try:
|
|
91
|
-
topic = MessageTopic(topic_val)
|
|
92
|
-
except ValueError:
|
|
93
|
-
raise ValueError(f"Unknown MessageTopic: {topic_val}")
|
|
94
|
-
msg.handshake = False
|
|
95
|
-
msg.sender = None
|
|
96
|
-
msg.topic = topic
|
|
97
|
-
msg.content = data[2:]
|
|
73
|
+
|
|
74
|
+
if len(data) < 33:
|
|
75
|
+
raise ValueError("Cannot parse Message: missing sender bytes")
|
|
76
|
+
|
|
77
|
+
if data[0] == 1:
|
|
78
|
+
return Message(
|
|
79
|
+
handshake=True,
|
|
80
|
+
sender_bytes=data[1:33],
|
|
81
|
+
content=data[33:],
|
|
82
|
+
)
|
|
83
|
+
|
|
98
84
|
else:
|
|
99
|
-
|
|
85
|
+
if len(data) <= 33:
|
|
86
|
+
raise ValueError("Cannot parse Message: missing encrypted payload")
|
|
87
|
+
|
|
88
|
+
return Message(
|
|
89
|
+
handshake=False,
|
|
90
|
+
sender_bytes=data[1:33],
|
|
91
|
+
encrypted=data[33:],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def encrypt(self, shared_key_bytes: bytes) -> None:
|
|
95
|
+
if self.handshake:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if len(shared_key_bytes) != 32:
|
|
99
|
+
raise ValueError("Shared key must be 32 bytes for ChaCha20-Poly1305")
|
|
100
|
+
|
|
101
|
+
if self.topic is None:
|
|
102
|
+
raise ValueError("Cannot encrypt message without a topic")
|
|
103
|
+
|
|
104
|
+
nonce = os.urandom(12)
|
|
105
|
+
data_to_encrypt = bytes([self.topic.value]) + self.content
|
|
106
|
+
ciphertext = chacha20poly1305.encrypt(shared_key_bytes, nonce, data_to_encrypt)
|
|
107
|
+
self.encrypted = nonce + ciphertext
|
|
108
|
+
|
|
109
|
+
def decrypt(self, shared_key_bytes: bytes) -> None:
|
|
110
|
+
if self.handshake:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if len(shared_key_bytes) != 32:
|
|
114
|
+
raise ValueError("Shared key must be 32 bytes for ChaCha20-Poly1305")
|
|
115
|
+
|
|
116
|
+
if not self.encrypted or len(self.encrypted) < 13:
|
|
117
|
+
raise ValueError("Encrypted content missing or too short")
|
|
100
118
|
|
|
101
|
-
|
|
119
|
+
nonce = self.encrypted[:12]
|
|
120
|
+
ciphertext = self.encrypted[12:]
|
|
121
|
+
decrypted = chacha20poly1305.decrypt(shared_key_bytes, nonce, ciphertext)
|
|
122
|
+
topic_value = decrypted[0]
|
|
123
|
+
self.topic = MessageTopic(topic_value)
|
|
124
|
+
self.content = decrypted[1:]
|
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
2
2
|
from cryptography.hazmat.primitives import serialization
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
|
-
from typing import Optional, Tuple
|
|
4
|
+
from typing import Optional, Tuple, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ...node import Node
|
|
5
8
|
|
|
6
9
|
class Peer:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
self.shared_key = my_sec_key.exchange(peer_pub_key)
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
node_secret_key: X25519PrivateKey,
|
|
13
|
+
peer_public_key: X25519PublicKey,
|
|
14
|
+
latest_block: Optional[bytes] = None,
|
|
15
|
+
address: Optional[Tuple[str, int]] = None,
|
|
16
|
+
):
|
|
17
|
+
self.shared_key_bytes = node_secret_key.exchange(peer_public_key)
|
|
16
18
|
self.timestamp = datetime.now(timezone.utc)
|
|
17
|
-
self.latest_block =
|
|
18
|
-
self.address =
|
|
19
|
-
self.
|
|
20
|
-
self.public_key_bytes = peer_pub_key.public_bytes(
|
|
19
|
+
self.latest_block = latest_block
|
|
20
|
+
self.address = address
|
|
21
|
+
self.public_key_bytes = peer_public_key.public_bytes(
|
|
21
22
|
encoding=serialization.Encoding.Raw,
|
|
22
23
|
format=serialization.PublicFormat.Raw,
|
|
23
24
|
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def add_peer(node: "Node", peer_key, peer: "Peer") -> "Peer":
|
|
28
|
+
"""Register a peer entry on the node under lock."""
|
|
29
|
+
with node.peers_lock:
|
|
30
|
+
node.peers[peer_key] = peer
|
|
31
|
+
return peer
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def replace_peer(node: "Node", old_key, peer_key, peer: "Peer") -> "Peer":
|
|
35
|
+
"""Replace an existing peer entry (if any) with a new one under lock."""
|
|
36
|
+
with node.peers_lock:
|
|
37
|
+
node.peers.pop(old_key, None)
|
|
38
|
+
node.peers[peer_key] = peer
|
|
39
|
+
return peer
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_peer(node: "Node", peer_key):
|
|
43
|
+
"""Retrieve a peer entry under lock."""
|
|
44
|
+
with node.peers_lock:
|
|
45
|
+
return node.peers.get(peer_key)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def remove_peer(node: "Node", peer_key):
|
|
49
|
+
"""Remove a peer entry under lock."""
|
|
50
|
+
with node.peers_lock:
|
|
51
|
+
return node.peers.pop(peer_key, None)
|
|
@@ -2,6 +2,7 @@ from typing import Dict, List, Optional, Union
|
|
|
2
2
|
from cryptography.hazmat.primitives import serialization
|
|
3
3
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
4
4
|
from .peer import Peer
|
|
5
|
+
from ..util import xor_distance
|
|
5
6
|
|
|
6
7
|
PeerKey = Union[X25519PublicKey, bytes, bytearray]
|
|
7
8
|
|
|
@@ -39,12 +40,6 @@ class Route:
|
|
|
39
40
|
return key_bytes
|
|
40
41
|
raise TypeError("peer_public_key must be raw bytes or X25519PublicKey")
|
|
41
42
|
|
|
42
|
-
@staticmethod
|
|
43
|
-
def _xor_distance(a: bytes, b: bytes) -> int:
|
|
44
|
-
if len(a) != len(b):
|
|
45
|
-
raise ValueError("xor distance requires equal-length operands")
|
|
46
|
-
return int.from_bytes(bytes(x ^ y for x, y in zip(a, b)), "big", signed=False)
|
|
47
|
-
|
|
48
43
|
def add_peer(self, peer_public_key: PeerKey, peer: Optional[Peer] = None):
|
|
49
44
|
peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
|
|
50
45
|
bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
|
|
@@ -82,7 +77,7 @@ class Route:
|
|
|
82
77
|
for bucket in self.buckets.values():
|
|
83
78
|
for peer_key in bucket:
|
|
84
79
|
try:
|
|
85
|
-
distance =
|
|
80
|
+
distance = xor_distance(target, peer_key)
|
|
86
81
|
except ValueError:
|
|
87
82
|
continue
|
|
88
83
|
if closest_distance is None or distance < closest_distance:
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ..handlers.handshake import handle_handshake
|
|
6
|
+
from ..handlers.object_request import handle_object_request
|
|
7
|
+
from ..handlers.object_response import handle_object_response
|
|
8
|
+
from ..handlers.ping import handle_ping
|
|
9
|
+
from ..handlers.route_request import handle_route_request
|
|
10
|
+
from ..handlers.route_response import handle_route_response
|
|
11
|
+
from ..models.message import Message, MessageTopic
|
|
12
|
+
from ..models.peer import Peer
|
|
13
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .. import Node
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def process_incoming_messages(node: "Node") -> None:
|
|
20
|
+
"""Process incoming messages (placeholder)."""
|
|
21
|
+
while True:
|
|
22
|
+
try:
|
|
23
|
+
data, addr = node.incoming_queue.get()
|
|
24
|
+
except Exception as exc:
|
|
25
|
+
node.logger.exception("Error taking from incoming queue")
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
message = Message.from_bytes(data)
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
node.logger.warning("Error decoding message: %s", exc)
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
if message.handshake:
|
|
35
|
+
if handle_handshake(node, addr, message):
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
peer = None
|
|
39
|
+
try:
|
|
40
|
+
peer = node.get_peer(message.sender_bytes)
|
|
41
|
+
except Exception:
|
|
42
|
+
peer = None
|
|
43
|
+
if peer is None:
|
|
44
|
+
try:
|
|
45
|
+
peer_key = X25519PublicKey.from_public_bytes(message.sender_bytes)
|
|
46
|
+
host, port = addr[0], int(addr[1])
|
|
47
|
+
peer = Peer(
|
|
48
|
+
node_secret_key=node.relay_secret_key,
|
|
49
|
+
peer_public_key=peer_key,
|
|
50
|
+
address=(host, port),
|
|
51
|
+
)
|
|
52
|
+
except Exception:
|
|
53
|
+
peer = None
|
|
54
|
+
|
|
55
|
+
if peer is None:
|
|
56
|
+
node.logger.debug("Unable to resolve peer for message from %s", addr)
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
# decrypt message payload before dispatch
|
|
60
|
+
try:
|
|
61
|
+
message.decrypt(peer.shared_key_bytes)
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
node.logger.warning("Error decrypting message from %s: %s", peer.address, exc)
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
match message.topic:
|
|
67
|
+
case MessageTopic.PING:
|
|
68
|
+
handle_ping(node, peer, message.content)
|
|
69
|
+
|
|
70
|
+
case MessageTopic.OBJECT_REQUEST:
|
|
71
|
+
handle_object_request(node, peer, message)
|
|
72
|
+
|
|
73
|
+
case MessageTopic.OBJECT_RESPONSE:
|
|
74
|
+
handle_object_response(node, peer, message)
|
|
75
|
+
|
|
76
|
+
case MessageTopic.ROUTE_REQUEST:
|
|
77
|
+
handle_route_request(node, peer, message)
|
|
78
|
+
|
|
79
|
+
case MessageTopic.ROUTE_RESPONSE:
|
|
80
|
+
handle_route_response(node, peer, message)
|
|
81
|
+
|
|
82
|
+
case MessageTopic.TRANSACTION:
|
|
83
|
+
if node.validation_secret_key is None:
|
|
84
|
+
continue
|
|
85
|
+
node._validation_transaction_queue.put(message.content)
|
|
86
|
+
|
|
87
|
+
case _:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def populate_incoming_messages(node: "Node") -> None:
|
|
92
|
+
"""Receive UDP packets and feed the incoming queue."""
|
|
93
|
+
while True:
|
|
94
|
+
try:
|
|
95
|
+
data, addr = node.incoming_socket.recvfrom(4096)
|
|
96
|
+
node.incoming_queue.put((data, addr))
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
node.logger.warning("Error populating incoming queue: %s", exc)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Tuple
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .. import Node
|
|
7
|
+
|
|
8
|
+
def process_outgoing_messages(node: "Node") -> None:
|
|
9
|
+
"""Send queued outbound packets."""
|
|
10
|
+
while True:
|
|
11
|
+
try:
|
|
12
|
+
payload, addr = node.outgoing_queue.get()
|
|
13
|
+
except Exception:
|
|
14
|
+
node.logger.exception("Error taking from outgoing queue")
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
node.outgoing_socket.sendto(payload, addr)
|
|
19
|
+
except Exception as exc:
|
|
20
|
+
node.logger.warning("Error sending message to %s: %s", addr, exc)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .. import Node
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def manage_peer(node: "Node") -> None:
|
|
12
|
+
"""Continuously evict peers whose timestamps exceed the configured timeout."""
|
|
13
|
+
node.logger.info(
|
|
14
|
+
"Peer manager started (timeout=%3ds, interval=%3ds)",
|
|
15
|
+
node.config["peer_timeout"],
|
|
16
|
+
node.config["peer_timeout_interval"],
|
|
17
|
+
)
|
|
18
|
+
while True:
|
|
19
|
+
timeout_seconds = node.config["peer_timeout"]
|
|
20
|
+
interval_seconds = node.config["peer_timeout_interval"]
|
|
21
|
+
try:
|
|
22
|
+
peers = getattr(node, "peers", None)
|
|
23
|
+
peer_route = getattr(node, "peer_route", None)
|
|
24
|
+
if not isinstance(peers, dict) or peer_route is None:
|
|
25
|
+
time.sleep(interval_seconds)
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
cutoff = datetime.now(timezone.utc) - timedelta(seconds=timeout_seconds)
|
|
29
|
+
stale_keys = []
|
|
30
|
+
with node.peers_lock:
|
|
31
|
+
for peer_key, peer in list(peers.items()):
|
|
32
|
+
if peer.timestamp < cutoff:
|
|
33
|
+
stale_keys.append(peer_key)
|
|
34
|
+
|
|
35
|
+
removed_count = 0
|
|
36
|
+
for peer_key in stale_keys:
|
|
37
|
+
removed = node.remove_peer(peer_key)
|
|
38
|
+
if removed is None:
|
|
39
|
+
continue
|
|
40
|
+
removed_count += 1
|
|
41
|
+
try:
|
|
42
|
+
peer_route.remove_peer(peer_key)
|
|
43
|
+
except Exception:
|
|
44
|
+
node.logger.debug(
|
|
45
|
+
"Unable to remove peer %s from route",
|
|
46
|
+
peer_key.hex(),
|
|
47
|
+
)
|
|
48
|
+
node.logger.debug(
|
|
49
|
+
"Evicted stale peer %s last seen at %s",
|
|
50
|
+
peer_key.hex(),
|
|
51
|
+
getattr(removed, "timestamp", None),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if removed_count:
|
|
55
|
+
node.logger.info("Peer manager removed %s stale peer(s)", removed_count)
|
|
56
|
+
except Exception:
|
|
57
|
+
node.logger.exception("Peer manager iteration failed")
|
|
58
|
+
|
|
59
|
+
time.sleep(interval_seconds)
|
astreum/communication/setup.py
CHANGED
|
@@ -13,10 +13,12 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from .. import Node
|
|
14
14
|
|
|
15
15
|
from . import Route, Message
|
|
16
|
-
from .
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
from .processors.incoming import (
|
|
17
|
+
process_incoming_messages,
|
|
18
|
+
populate_incoming_messages,
|
|
19
|
+
)
|
|
20
|
+
from .processors.outgoing import process_outgoing_messages
|
|
21
|
+
from .processors.peer import manage_peer
|
|
20
22
|
from .util import address_str_to_host_and_port
|
|
21
23
|
from ..utils.bytes import hex_to_bytes
|
|
22
24
|
|
|
@@ -40,77 +42,15 @@ def make_routes(
|
|
|
40
42
|
val_rt = Route(val_sk.public_key()) if val_sk else None
|
|
41
43
|
return peer_rt, val_rt
|
|
42
44
|
|
|
43
|
-
def setup_outgoing(
|
|
44
|
-
use_ipv6: bool
|
|
45
|
-
) -> Tuple[socket.socket, Queue, threading.Thread]:
|
|
46
|
-
fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
47
|
-
sock = socket.socket(fam, socket.SOCK_DGRAM)
|
|
48
|
-
q = Queue()
|
|
49
|
-
thr = threading.Thread(target=lambda: None, daemon=True)
|
|
50
|
-
thr.start()
|
|
51
|
-
return sock, q, thr
|
|
52
|
-
|
|
53
45
|
def make_maps():
|
|
54
46
|
"""Empty lookup maps: peers and addresses."""
|
|
55
47
|
return
|
|
56
48
|
|
|
57
49
|
|
|
58
|
-
def process_incoming_messages(node: "Node") -> None:
|
|
59
|
-
"""Process incoming messages (placeholder)."""
|
|
60
|
-
node_logger = node.logger
|
|
61
|
-
while True:
|
|
62
|
-
try:
|
|
63
|
-
data, addr = node.incoming_queue.get()
|
|
64
|
-
except Exception as exc:
|
|
65
|
-
node_logger.exception("Error taking from incoming queue")
|
|
66
|
-
continue
|
|
67
|
-
|
|
68
|
-
try:
|
|
69
|
-
message = Message.from_bytes(data)
|
|
70
|
-
except Exception as exc:
|
|
71
|
-
node_logger.warning("Error decoding message: %s", exc)
|
|
72
|
-
continue
|
|
73
|
-
|
|
74
|
-
if message.handshake:
|
|
75
|
-
if handle_handshake(node, addr, message):
|
|
76
|
-
continue
|
|
77
|
-
|
|
78
|
-
match message.topic:
|
|
79
|
-
case MessageTopic.PING:
|
|
80
|
-
handle_ping(node, addr, message.content)
|
|
81
|
-
case MessageTopic.OBJECT_REQUEST:
|
|
82
|
-
pass
|
|
83
|
-
case MessageTopic.OBJECT_RESPONSE:
|
|
84
|
-
pass
|
|
85
|
-
case MessageTopic.ROUTE_REQUEST:
|
|
86
|
-
pass
|
|
87
|
-
case MessageTopic.ROUTE_RESPONSE:
|
|
88
|
-
pass
|
|
89
|
-
case MessageTopic.TRANSACTION:
|
|
90
|
-
if node.validation_secret_key is None:
|
|
91
|
-
continue
|
|
92
|
-
node._validation_transaction_queue.put(message.content)
|
|
93
|
-
|
|
94
|
-
case MessageTopic.STORAGE_REQUEST:
|
|
95
|
-
handle_storage_request(node, addr, message)
|
|
96
|
-
|
|
97
|
-
case _:
|
|
98
|
-
continue
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def populate_incoming_messages(node: "Node") -> None:
|
|
102
|
-
"""Receive UDP packets and feed the incoming queue (placeholder)."""
|
|
103
|
-
node_logger = node.logger
|
|
104
|
-
while True:
|
|
105
|
-
try:
|
|
106
|
-
data, addr = node.incoming_socket.recvfrom(4096)
|
|
107
|
-
node.incoming_queue.put((data, addr))
|
|
108
|
-
except Exception as exc:
|
|
109
|
-
node_logger.warning("Error populating incoming queue: %s", exc)
|
|
110
|
-
|
|
111
50
|
def communication_setup(node: "Node", config: dict):
|
|
112
51
|
node.logger.info("Setting up node communication")
|
|
113
52
|
node.use_ipv6 = config.get('use_ipv6', False)
|
|
53
|
+
node.peers_lock = threading.RLock()
|
|
114
54
|
|
|
115
55
|
# key loading
|
|
116
56
|
node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
|
|
@@ -118,6 +58,10 @@ def communication_setup(node: "Node", config: dict):
|
|
|
118
58
|
|
|
119
59
|
# derive pubs + routes
|
|
120
60
|
node.relay_public_key = node.relay_secret_key.public_key()
|
|
61
|
+
node.relay_public_key_bytes = node.relay_public_key.public_bytes(
|
|
62
|
+
encoding=serialization.Encoding.Raw,
|
|
63
|
+
format=serialization.PublicFormat.Raw,
|
|
64
|
+
)
|
|
121
65
|
node.validation_public_key = (
|
|
122
66
|
node.validation_secret_key.public_key().public_bytes(
|
|
123
67
|
encoding=serialization.Encoding.Raw,
|
|
@@ -131,6 +75,11 @@ def communication_setup(node: "Node", config: dict):
|
|
|
131
75
|
node.validation_secret_key
|
|
132
76
|
)
|
|
133
77
|
|
|
78
|
+
# connection state & atom request tracking
|
|
79
|
+
node.is_connected = False
|
|
80
|
+
node.atom_requests = set()
|
|
81
|
+
node.atom_requests_lock = threading.RLock()
|
|
82
|
+
|
|
134
83
|
# sockets + queues + threads
|
|
135
84
|
incoming_port = config.get('incoming_port', 7373)
|
|
136
85
|
fam = socket.AF_INET6 if node.use_ipv6 else socket.AF_INET
|
|
@@ -158,20 +107,30 @@ def communication_setup(node: "Node", config: dict):
|
|
|
158
107
|
node.incoming_populate_thread.start()
|
|
159
108
|
node.incoming_process_thread.start()
|
|
160
109
|
|
|
161
|
-
|
|
162
|
-
node.
|
|
163
|
-
|
|
164
|
-
)
|
|
110
|
+
node.outgoing_socket = socket.socket(
|
|
111
|
+
socket.AF_INET6 if node.use_ipv6 else socket.AF_INET,
|
|
112
|
+
socket.SOCK_DGRAM,
|
|
113
|
+
)
|
|
114
|
+
node.outgoing_queue = Queue()
|
|
115
|
+
|
|
116
|
+
node.outgoing_thread = threading.Thread(
|
|
117
|
+
target=process_outgoing_messages,
|
|
118
|
+
args=(node,),
|
|
119
|
+
daemon=True,
|
|
120
|
+
)
|
|
121
|
+
node.outgoing_thread.start()
|
|
165
122
|
|
|
166
123
|
# other workers & maps
|
|
167
|
-
|
|
124
|
+
# track atom requests we initiated; guarded by atom_requests_lock on the node
|
|
168
125
|
node.peer_manager_thread = threading.Thread(
|
|
169
|
-
target=
|
|
126
|
+
target=manage_peer,
|
|
127
|
+
args=(node,),
|
|
170
128
|
daemon=True
|
|
171
129
|
)
|
|
172
130
|
node.peer_manager_thread.start()
|
|
173
131
|
|
|
174
|
-
|
|
132
|
+
with node.peers_lock:
|
|
133
|
+
node.peers = {} # Dict[bytes,Peer]
|
|
175
134
|
|
|
176
135
|
latest_block_hex = config.get("latest_block_hash")
|
|
177
136
|
if latest_block_hex:
|
|
@@ -192,8 +151,11 @@ def communication_setup(node: "Node", config: dict):
|
|
|
192
151
|
node.logger.warning("Invalid bootstrap address %s: %s", addr, exc)
|
|
193
152
|
continue
|
|
194
153
|
|
|
195
|
-
handshake_message = Message(
|
|
196
|
-
|
|
154
|
+
handshake_message = Message(
|
|
155
|
+
handshake=True,
|
|
156
|
+
sender=node.relay_public_key,
|
|
157
|
+
content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
|
|
158
|
+
)
|
|
197
159
|
node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))
|
|
198
160
|
node.logger.info("Sent bootstrap handshake to %s:%s", host, port)
|
|
199
161
|
|
|
@@ -203,3 +165,4 @@ def communication_setup(node: "Node", config: dict):
|
|
|
203
165
|
node.outgoing_socket is not None,
|
|
204
166
|
len(bootstrap_peers),
|
|
205
167
|
)
|
|
168
|
+
node.is_connected = True
|
astreum/communication/start.py
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
def connect_to_network_and_verify(self):
|
|
2
2
|
"""Initialize communication and consensus components, then load latest block state."""
|
|
3
|
-
|
|
4
|
-
node_logger.info("Starting communication and consensus setup")
|
|
3
|
+
self.logger.info("Starting communication and consensus setup")
|
|
5
4
|
try:
|
|
6
5
|
from astreum.communication import communication_setup # type: ignore
|
|
7
6
|
communication_setup(node=self, config=self.config)
|
|
8
|
-
|
|
7
|
+
self.logger.info("Communication setup completed")
|
|
9
8
|
except Exception:
|
|
10
|
-
|
|
9
|
+
self.logger.exception("Communication setup failed")
|
|
11
10
|
|
|
12
11
|
try:
|
|
13
12
|
from astreum.consensus import consensus_setup # type: ignore
|
|
14
13
|
consensus_setup(node=self, config=self.config)
|
|
15
|
-
|
|
14
|
+
self.logger.info("Consensus setup completed")
|
|
16
15
|
except Exception:
|
|
17
|
-
|
|
16
|
+
self.logger.exception("Consensus setup failed")
|
|
18
17
|
|
|
19
18
|
# Load latest_block_hash from config
|
|
20
19
|
self.latest_block_hash = getattr(self, "latest_block_hash", None)
|
|
@@ -25,14 +24,14 @@ def connect_to_network_and_verify(self):
|
|
|
25
24
|
try:
|
|
26
25
|
from astreum.utils.bytes import hex_to_bytes
|
|
27
26
|
self.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
|
|
28
|
-
|
|
27
|
+
self.logger.debug("Loaded latest_block_hash override from config")
|
|
29
28
|
except Exception as exc:
|
|
30
|
-
|
|
29
|
+
self.logger.error("Invalid latest_block_hash in config: %s", exc)
|
|
31
30
|
|
|
32
31
|
if self.latest_block_hash and self.latest_block is None:
|
|
33
32
|
try:
|
|
34
33
|
from astreum.consensus.models.block import Block
|
|
35
34
|
self.latest_block = Block.from_atom(self, self.latest_block_hash)
|
|
36
|
-
|
|
35
|
+
self.logger.info("Loaded latest block %s from storage", self.latest_block_hash.hex())
|
|
37
36
|
except Exception as exc:
|
|
38
|
-
|
|
37
|
+
self.logger.warning("Could not load latest block from storage: %s", exc)
|