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.
Files changed (34) hide show
  1. astreum/communication/handlers/handshake.py +89 -83
  2. astreum/communication/handlers/object_request.py +176 -0
  3. astreum/communication/handlers/object_response.py +115 -0
  4. astreum/communication/handlers/ping.py +6 -20
  5. astreum/communication/handlers/route_request.py +76 -0
  6. astreum/communication/handlers/route_response.py +53 -0
  7. astreum/communication/models/message.py +81 -58
  8. astreum/communication/models/peer.py +42 -14
  9. astreum/communication/models/route.py +2 -7
  10. astreum/communication/processors/__init__.py +0 -0
  11. astreum/communication/processors/incoming.py +98 -0
  12. astreum/communication/processors/outgoing.py +20 -0
  13. astreum/communication/setup.py +36 -75
  14. astreum/communication/start.py +9 -10
  15. astreum/communication/util.py +7 -0
  16. astreum/consensus/start.py +9 -10
  17. astreum/consensus/workers/discovery.py +6 -7
  18. astreum/consensus/workers/validation.py +307 -291
  19. astreum/consensus/workers/verify.py +8 -10
  20. astreum/crypto/chacha20poly1305.py +74 -0
  21. astreum/machine/evaluations/high_evaluation.py +237 -237
  22. astreum/machine/evaluations/low_evaluation.py +18 -18
  23. astreum/node.py +25 -6
  24. astreum/storage/actions/get.py +183 -69
  25. astreum/storage/actions/set.py +66 -20
  26. astreum/storage/requests.py +28 -0
  27. astreum/storage/setup.py +3 -25
  28. astreum/utils/config.py +48 -0
  29. {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/METADATA +3 -3
  30. {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/RECORD +33 -24
  31. astreum/communication/handlers/storage_request.py +0 -81
  32. {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
  33. {astreum-0.3.1.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
  34. {astreum-0.3.1.dist-info → astreum-0.3.9.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 = b"",
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 or b""
36
+ self.content = content if content is not None else b""
37
+ self.encrypted = encrypted
41
38
 
42
39
  if self.handshake:
43
- if self.sender is None:
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
- def to_bytes(self):
52
- if self.handshake:
53
- # handshake byte (1) + raw public key bytes
54
- return bytes([1]) + self.sender.public_bytes(
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 + topic + content
60
- return bytes([0, self.topic.value]) + self.content
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
- flag = data[0]
67
- # create empty instance
68
- msg = cls.__new__(cls)
69
-
70
- if flag == 1:
71
- # handshake message: the rest is the peer’s public key
72
- key_bytes = data[1:]
73
- if not key_bytes:
74
- raise ValueError("Handshake message missing sender public key bytes")
75
- try:
76
- sender = X25519PublicKey.from_public_bytes(key_bytes)
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
- raise ValueError(f"Invalid handshake flag: {flag}")
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
- return msg
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
- shared_key: bytes
8
- timestamp: datetime
9
- latest_block: bytes
10
- address: Optional[Tuple[str, int]]
11
- public_key: X25519PublicKey
12
- public_key_bytes: bytes
13
-
14
- def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
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 = b""
18
- self.address = None
19
- self.public_key = peer_pub_key
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 = self._xor_distance(target, peer_key)
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)
@@ -13,10 +13,11 @@ if TYPE_CHECKING:
13
13
  from .. import Node
14
14
 
15
15
  from . import Route, Message
16
- from .handlers.handshake import handle_handshake
17
- from .handlers.ping import handle_ping
18
- from .handlers.storage_request import handle_storage_request
19
- from .models.message import MessageTopic
16
+ from .processors.incoming import (
17
+ process_incoming_messages,
18
+ populate_incoming_messages,
19
+ )
20
+ from .processors.outgoing import process_outgoing_messages
20
21
  from .util import address_str_to_host_and_port
21
22
  from ..utils.bytes import hex_to_bytes
22
23
 
@@ -40,77 +41,15 @@ def make_routes(
40
41
  val_rt = Route(val_sk.public_key()) if val_sk else None
41
42
  return peer_rt, val_rt
42
43
 
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
44
  def make_maps():
54
45
  """Empty lookup maps: peers and addresses."""
55
46
  return
56
47
 
57
48
 
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
49
  def communication_setup(node: "Node", config: dict):
112
50
  node.logger.info("Setting up node communication")
113
51
  node.use_ipv6 = config.get('use_ipv6', False)
52
+ node.peers_lock = threading.RLock()
114
53
 
115
54
  # key loading
116
55
  node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
@@ -118,6 +57,10 @@ def communication_setup(node: "Node", config: dict):
118
57
 
119
58
  # derive pubs + routes
120
59
  node.relay_public_key = node.relay_secret_key.public_key()
60
+ node.relay_public_key_bytes = node.relay_public_key.public_bytes(
61
+ encoding=serialization.Encoding.Raw,
62
+ format=serialization.PublicFormat.Raw,
63
+ )
121
64
  node.validation_public_key = (
122
65
  node.validation_secret_key.public_key().public_bytes(
123
66
  encoding=serialization.Encoding.Raw,
@@ -131,6 +74,11 @@ def communication_setup(node: "Node", config: dict):
131
74
  node.validation_secret_key
132
75
  )
133
76
 
77
+ # connection state & atom request tracking
78
+ node.is_connected = False
79
+ node.atom_requests = set()
80
+ node.atom_requests_lock = threading.RLock()
81
+
134
82
  # sockets + queues + threads
135
83
  incoming_port = config.get('incoming_port', 7373)
136
84
  fam = socket.AF_INET6 if node.use_ipv6 else socket.AF_INET
@@ -158,20 +106,29 @@ def communication_setup(node: "Node", config: dict):
158
106
  node.incoming_populate_thread.start()
159
107
  node.incoming_process_thread.start()
160
108
 
161
- (node.outgoing_socket,
162
- node.outgoing_queue,
163
- node.outgoing_thread
164
- ) = setup_outgoing(node.use_ipv6)
109
+ node.outgoing_socket = socket.socket(
110
+ socket.AF_INET6 if node.use_ipv6 else socket.AF_INET,
111
+ socket.SOCK_DGRAM,
112
+ )
113
+ node.outgoing_queue = Queue()
114
+
115
+ node.outgoing_thread = threading.Thread(
116
+ target=process_outgoing_messages,
117
+ args=(node,),
118
+ daemon=True,
119
+ )
120
+ node.outgoing_thread.start()
165
121
 
166
122
  # other workers & maps
167
- node.object_request_queue = Queue()
123
+ # track atom requests we initiated; guarded by atom_requests_lock on the node
168
124
  node.peer_manager_thread = threading.Thread(
169
125
  target=node._relay_peer_manager,
170
126
  daemon=True
171
127
  )
172
128
  node.peer_manager_thread.start()
173
129
 
174
- node.peers, node.addresses = {}, {} # peers: Dict[bytes,Peer], addresses: Dict[(str,int),bytes]
130
+ with node.peers_lock:
131
+ node.peers, node.addresses = {}, {} # peers: Dict[bytes,Peer], addresses: Dict[(str,int),bytes]
175
132
 
176
133
  latest_block_hex = config.get("latest_block_hash")
177
134
  if latest_block_hex:
@@ -192,8 +149,11 @@ def communication_setup(node: "Node", config: dict):
192
149
  node.logger.warning("Invalid bootstrap address %s: %s", addr, exc)
193
150
  continue
194
151
 
195
- handshake_message = Message(handshake=True, sender=node.relay_public_key)
196
-
152
+ handshake_message = Message(
153
+ handshake=True,
154
+ sender=node.relay_public_key,
155
+ content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
156
+ )
197
157
  node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))
198
158
  node.logger.info("Sent bootstrap handshake to %s:%s", host, port)
199
159
 
@@ -203,3 +163,4 @@ def communication_setup(node: "Node", config: dict):
203
163
  node.outgoing_socket is not None,
204
164
  len(bootstrap_peers),
205
165
  )
166
+ node.is_connected = True
@@ -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
- node_logger = self.logger
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
- node_logger.info("Communication setup completed")
7
+ self.logger.info("Communication setup completed")
9
8
  except Exception:
10
- node_logger.exception("Communication setup failed")
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
- node_logger.info("Consensus setup completed")
14
+ self.logger.info("Consensus setup completed")
16
15
  except Exception:
17
- node_logger.exception("Consensus setup failed")
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
- node_logger.debug("Loaded latest_block_hash override from config")
27
+ self.logger.debug("Loaded latest_block_hash override from config")
29
28
  except Exception as exc:
30
- node_logger.error("Invalid latest_block_hash in config: %s", exc)
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
- node_logger.info("Loaded latest block %s from storage", self.latest_block_hash.hex())
35
+ self.logger.info("Loaded latest block %s from storage", self.latest_block_hash.hex())
37
36
  except Exception as exc:
38
- node_logger.warning("Could not load latest block from storage: %s", exc)
37
+ self.logger.warning("Could not load latest block from storage: %s", exc)
@@ -40,3 +40,10 @@ def address_str_to_host_and_port(address: str) -> Tuple[str, int]:
40
40
  raise ValueError(f"port out of range: {port}")
41
41
 
42
42
  return host, port
43
+
44
+
45
+ def xor_distance(a: bytes, b: bytes) -> int:
46
+ """Return the unsigned integer XOR distance between two equal-length identifiers."""
47
+ if len(a) != len(b):
48
+ raise ValueError("xor distance requires operands of equal length")
49
+ return int.from_bytes(bytes(x ^ y for x, y in zip(a, b)), "big", signed=False)