astreum 0.2.39__py3-none-any.whl → 0.2.40__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.

Potentially problematic release.


This version of astreum might be problematic. Click here for more details.

@@ -1,8 +1,10 @@
1
+ from .message import Message
1
2
  from .peer import Peer
2
3
  from .route import Route
3
4
  from .setup import communication_setup
4
5
 
5
6
  __all__ = [
7
+ "Message",
6
8
  "Peer",
7
9
  "Route",
8
10
  "communication_setup",
@@ -1,64 +1,100 @@
1
- from enum import IntEnum
2
- from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
3
-
4
- class MessageTopic(IntEnum):
5
- PING = 0
6
- OBJECT_REQUEST = 1
7
- OBJECT_RESPONSE = 2
8
- ROUTE_REQUEST = 3
9
- ROUTE_RESPONSE = 4
10
-
11
- class Message:
12
- handshake: bool
13
- sender: X25519PublicKey
14
-
15
- topic: MessageTopic
16
- content: bytes
17
-
18
- def to_bytes(self):
19
- if self.handshake:
20
- # handshake byte (1) + raw public key bytes
21
- return bytes([1]) + self.sender.public_bytes(
22
- encoding=serialization.Encoding.Raw,
23
- format=serialization.PublicFormat.Raw
24
- )
25
- else:
26
- # normal message: 0 + topic + content
27
- return bytes([0, self.topic.value]) + self.content
28
-
29
- @classmethod
30
- def from_bytes(cls, data: bytes) -> "Message":
31
- if len(data) < 1:
32
- raise ValueError("Cannot parse Message: no data")
33
- flag = data[0]
34
- # create empty instance
35
- msg = cls.__new__(cls)
36
-
37
- if flag == 1:
38
- # handshake message: the rest is the peer’s public key
39
- key_bytes = data[1:]
40
- try:
41
- sender = X25519PublicKey.from_public_bytes(key_bytes)
42
- except ValueError:
43
- raise ValueError("Invalid public key bytes")
44
- msg.handshake = True
45
- msg.sender = sender
46
- msg.topic = None
47
- msg.content = b''
48
- elif flag == 0:
49
- # normal message: next byte is topic, rest is content
50
- if len(data) < 2:
51
- raise ValueError("Cannot parse Message: missing topic byte")
52
- topic_val = data[1]
53
- try:
54
- topic = MessageTopic(topic_val)
55
- except ValueError:
56
- raise ValueError(f"Unknown MessageTopic: {topic_val}")
57
- msg.handshake = False
58
- msg.sender = None
59
- msg.topic = topic
60
- msg.content = data[2:]
61
- else:
62
- raise ValueError(f"Invalid handshake flag: {flag}")
63
-
64
- return msg
1
+ from enum import IntEnum
2
+ from typing import Optional
3
+ from cryptography.hazmat.primitives import serialization
4
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
5
+
6
+ class MessageTopic(IntEnum):
7
+ PING = 0
8
+ OBJECT_REQUEST = 1
9
+ OBJECT_RESPONSE = 2
10
+ ROUTE_REQUEST = 3
11
+ ROUTE_RESPONSE = 4
12
+ TRANSACTION = 5
13
+
14
+
15
+ class Message:
16
+ handshake: bool
17
+ sender: Optional[X25519PublicKey]
18
+
19
+ topic: Optional[MessageTopic]
20
+ content: bytes
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ handshake: bool = False,
26
+ sender: Optional[X25519PublicKey] = None,
27
+ topic: Optional[MessageTopic] = None,
28
+ content: bytes = b"",
29
+ body: Optional[bytes] = None,
30
+ ) -> None:
31
+ if body is not None:
32
+ if content and content != b"":
33
+ raise ValueError("specify only one of 'content' or 'body'")
34
+ content = body
35
+
36
+ self.handshake = handshake
37
+ self.sender = sender
38
+ self.topic = topic
39
+ self.content = content or b""
40
+
41
+ if self.handshake:
42
+ if self.sender is None:
43
+ raise ValueError("handshake Message requires a sender public key")
44
+ self.topic = None
45
+ self.content = b""
46
+ else:
47
+ if self.topic is None:
48
+ raise ValueError("non-handshake Message requires a topic")
49
+
50
+ def to_bytes(self):
51
+ if self.handshake:
52
+ # handshake byte (1) + raw public key bytes
53
+ return bytes([1]) + self.sender.public_bytes(
54
+ encoding=serialization.Encoding.Raw,
55
+ format=serialization.PublicFormat.Raw
56
+ )
57
+ else:
58
+ # normal message: 0 + topic + content
59
+ return bytes([0, self.topic.value]) + self.content
60
+
61
+ @classmethod
62
+ def from_bytes(cls, data: bytes) -> "Message":
63
+ if len(data) < 1:
64
+ raise ValueError("Cannot parse Message: no data")
65
+ flag = data[0]
66
+ # create empty instance
67
+ msg = cls.__new__(cls)
68
+
69
+ if flag == 1:
70
+ # handshake message: the rest is the peer’s public key
71
+ key_bytes = data[1:]
72
+ if not key_bytes:
73
+ raise ValueError("Handshake message missing sender public key bytes")
74
+ try:
75
+ sender = X25519PublicKey.from_public_bytes(key_bytes)
76
+ except ValueError:
77
+ raise ValueError("Invalid public key bytes")
78
+ if sender is None:
79
+ raise ValueError("Handshake message missing sender public key")
80
+ msg.handshake = True
81
+ msg.sender = sender
82
+ msg.topic = None
83
+ msg.content = b''
84
+ elif flag == 0:
85
+ # normal message: next byte is topic, rest is content
86
+ if len(data) < 2:
87
+ raise ValueError("Cannot parse Message: missing topic byte")
88
+ topic_val = data[1]
89
+ try:
90
+ topic = MessageTopic(topic_val)
91
+ except ValueError:
92
+ raise ValueError(f"Unknown MessageTopic: {topic_val}")
93
+ msg.handshake = False
94
+ msg.sender = None
95
+ msg.topic = topic
96
+ msg.content = data[2:]
97
+ else:
98
+ raise ValueError(f"Invalid handshake flag: {flag}")
99
+
100
+ return msg
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ class PingFormatError(ValueError):
7
+ """Raised when ping payload bytes are invalid."""
8
+
9
+
10
+ @dataclass
11
+ class Ping:
12
+ is_validator: bool
13
+ latest_block: bytes
14
+
15
+ PAYLOAD_SIZE = 33
16
+
17
+ def __post_init__(self) -> None:
18
+ lb = bytes(self.latest_block or b"")
19
+ if len(lb) != 32:
20
+ raise ValueError("latest_block must be exactly 32 bytes")
21
+ self.latest_block = lb
22
+
23
+ def to_bytes(self) -> bytes:
24
+ return (b"\x01" if self.is_validator else b"\x00") + self.latest_block
25
+
26
+ @classmethod
27
+ def from_bytes(cls, data: bytes) -> "Ping":
28
+ if len(data) != cls.PAYLOAD_SIZE:
29
+ raise PingFormatError("ping payload must be exactly 33 bytes")
30
+ flag = data[0]
31
+ if flag not in (0, 1):
32
+ raise PingFormatError("ping validator flag must be 0 or 1")
33
+ return cls(is_validator=bool(flag), latest_block=data[1:])
@@ -1,25 +1,58 @@
1
- from typing import Dict, List
2
- from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
3
-
4
- class Route:
5
- def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
6
- self.relay_public_key_bytes = relay_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
7
- self.bucket_size = bucket_size
8
- self.buckets: Dict[int, List[X25519PublicKey]] = {
9
- i: [] for i in range(len(self.relay_public_key_bytes) * 8)
10
- }
11
- self.peers = {}
12
-
13
- @staticmethod
1
+ from typing import Dict, List, Union
2
+ from cryptography.hazmat.primitives import serialization
3
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
4
+
5
+ PeerKey = Union[X25519PublicKey, bytes, bytearray]
6
+
7
+
8
+ class Route:
9
+ def __init__(self, relay_public_key: X25519PublicKey, bucket_size: int = 16):
10
+ self.relay_public_key_bytes = relay_public_key.public_bytes(
11
+ encoding=serialization.Encoding.Raw,
12
+ format=serialization.PublicFormat.Raw,
13
+ )
14
+ self.bucket_size = bucket_size
15
+ self.buckets: Dict[int, List[bytes]] = {
16
+ i: [] for i in range(len(self.relay_public_key_bytes) * 8)
17
+ }
18
+ self.peers = {}
19
+
20
+ @staticmethod
14
21
  def _matching_leading_bits(a: bytes, b: bytes) -> int:
15
22
  for byte_index, (ba, bb) in enumerate(zip(a, b)):
16
23
  diff = ba ^ bb
17
24
  if diff:
18
25
  return byte_index * 8 + (8 - diff.bit_length())
19
- return len(a) * 8
20
-
21
- def add_peer(self, peer_public_key: X25519PublicKey):
22
- peer_public_key_bytes = peer_public_key.public_bytes(encoding=Encoding.Raw, format=PublicFormat.Raw)
23
- bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
24
- if len(self.buckets[bucket_idx]) < self.bucket_size:
25
- self.buckets[bucket_idx].append(peer_public_key)
26
+ return len(a) * 8
27
+
28
+ def _normalize_peer_key(self, peer_public_key: PeerKey) -> bytes:
29
+ if isinstance(peer_public_key, X25519PublicKey):
30
+ return peer_public_key.public_bytes(
31
+ encoding=serialization.Encoding.Raw,
32
+ format=serialization.PublicFormat.Raw,
33
+ )
34
+ if isinstance(peer_public_key, (bytes, bytearray)):
35
+ key_bytes = bytes(peer_public_key)
36
+ if len(key_bytes) != len(self.relay_public_key_bytes):
37
+ raise ValueError("peer key must be raw 32-byte public key")
38
+ return key_bytes
39
+ raise TypeError("peer_public_key must be raw bytes or X25519PublicKey")
40
+
41
+ def add_peer(self, peer_public_key: PeerKey):
42
+ peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
43
+ bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
44
+ if len(self.buckets[bucket_idx]) < self.bucket_size:
45
+ bucket = self.buckets[bucket_idx]
46
+ if peer_public_key_bytes not in bucket:
47
+ bucket.append(peer_public_key_bytes)
48
+
49
+ def remove_peer(self, peer_public_key: PeerKey):
50
+ peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
51
+ bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
52
+ bucket = self.buckets.get(bucket_idx)
53
+ if not bucket:
54
+ return
55
+ try:
56
+ bucket.remove(peer_public_key_bytes)
57
+ except ValueError:
58
+ pass
@@ -1,73 +1,188 @@
1
- import socket, threading
2
- from queue import Queue
3
- from typing import Tuple, Optional
1
+ import socket, threading
2
+ from datetime import datetime, timezone
3
+ from queue import Queue
4
+ from typing import Tuple, Optional
4
5
  from cryptography.hazmat.primitives.asymmetric import ed25519
5
6
  from cryptography.hazmat.primitives import serialization
6
- from cryptography.hazmat.primitives.asymmetric.x25519 import (
7
- X25519PrivateKey,
8
- X25519PublicKey,
9
- )
10
-
11
- from typing import TYPE_CHECKING
12
- if TYPE_CHECKING:
13
- from .. import Node
14
-
15
- from .import Route
16
-
17
- def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
18
- """DH key for relaying (always X25519)."""
19
- return
20
-
21
- def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
22
- """Signing key for validation (Ed25519), or None if absent."""
23
- return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
24
- if hex_key else None
25
-
26
- def make_routes(
27
- relay_pk: X25519PublicKey,
28
- val_sk: Optional[ed25519.Ed25519PrivateKey]
29
- ) -> Tuple[Route, Optional[Route]]:
30
- """Peer route (DH pubkey) + optional validation route (ed pubkey)."""
31
- peer_rt = Route(relay_pk)
32
- val_rt = Route(val_sk.public_key()) if val_sk else None
33
- return peer_rt, val_rt
34
-
35
- def setup_udp(
36
- bind_port: int,
37
- use_ipv6: bool
38
- ) -> Tuple[socket.socket, int, Queue, threading.Thread, threading.Thread]:
39
- fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
40
- sock = socket.socket(fam, socket.SOCK_DGRAM)
41
- if use_ipv6:
42
- sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
43
- sock.bind(("::" if use_ipv6 else "0.0.0.0", bind_port or 0))
44
- port = sock.getsockname()[1]
45
-
46
- q = Queue()
47
- pop = threading.Thread(target=lambda: None, daemon=True)
48
- proc = threading.Thread(target=lambda: None, daemon=True)
49
- pop.start(); proc.start()
50
- return sock, port, q, pop, proc
51
-
52
- def setup_outgoing(
53
- use_ipv6: bool
54
- ) -> Tuple[socket.socket, Queue, threading.Thread]:
55
- fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
56
- sock = socket.socket(fam, socket.SOCK_DGRAM)
57
- q = Queue()
58
- thr = threading.Thread(target=lambda: None, daemon=True)
59
- thr.start()
60
- return sock, q, thr
61
-
62
- def make_maps():
63
- """Empty lookup maps: peers and addresses."""
64
- return
65
-
66
- def communication_setup(node: "Node", config: dict):
67
- node.use_ipv6 = config.get('use_ipv6', False)
68
-
69
- # key loading
70
- node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
7
+ from cryptography.hazmat.primitives.asymmetric.x25519 import (
8
+ X25519PrivateKey,
9
+ X25519PublicKey,
10
+ )
11
+
12
+ from typing import TYPE_CHECKING
13
+ if TYPE_CHECKING:
14
+ from .. import Node
15
+
16
+ from . import Route, Message
17
+ from .message import MessageTopic
18
+ from .peer import Peer
19
+ from .ping import Ping
20
+ from .util import address_str_to_host_and_port
21
+
22
+ def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
23
+ """DH key for relaying (always X25519)."""
24
+ return
25
+
26
+ def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
27
+ """Signing key for validation (Ed25519), or None if absent."""
28
+ return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
29
+ if hex_key else None
30
+
31
+ def make_routes(
32
+ relay_pk: X25519PublicKey,
33
+ val_sk: Optional[ed25519.Ed25519PrivateKey]
34
+ ) -> Tuple[Route, Optional[Route]]:
35
+ """Peer route (DH pubkey) + optional validation route (ed pubkey)."""
36
+ peer_rt = Route(relay_pk)
37
+ val_rt = Route(val_sk.public_key()) if val_sk else None
38
+ return peer_rt, val_rt
39
+
40
+ def setup_outgoing(
41
+ use_ipv6: bool
42
+ ) -> Tuple[socket.socket, Queue, threading.Thread]:
43
+ fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
44
+ sock = socket.socket(fam, socket.SOCK_DGRAM)
45
+ q = Queue()
46
+ thr = threading.Thread(target=lambda: None, daemon=True)
47
+ thr.start()
48
+ return sock, q, thr
49
+
50
+ def make_maps():
51
+ """Empty lookup maps: peers and addresses."""
52
+ return
53
+
54
+
55
+ def process_incoming_messages(node: "Node") -> None:
56
+ """Process incoming messages (placeholder)."""
57
+ while True:
58
+ try:
59
+ data, addr = node.incoming_queue.get()
60
+ except Exception as exc:
61
+ print(f"Error taking from incoming queue: {exc}")
62
+ continue
63
+
64
+ try:
65
+ message = Message.from_bytes(data)
66
+ except Exception as exc:
67
+ print(f"Error decoding message: {exc}")
68
+ continue
69
+
70
+ if message.handshake:
71
+ sender_key = message.sender
72
+
73
+ try:
74
+ sender_public_key_bytes = sender_key.public_bytes(
75
+ encoding=serialization.Encoding.Raw,
76
+ format=serialization.PublicFormat.Raw,
77
+ )
78
+ except Exception as exc:
79
+ print(f"Error extracting sender key bytes: {exc}")
80
+ continue
81
+
82
+ # Normalize remote address (IPv6 tuples may be 4 elements)
83
+ try:
84
+ host, port = addr[0], int(addr[1])
85
+ except Exception:
86
+ continue
87
+ address_key = (host, port)
88
+
89
+ old_key_bytes = node.addresses.get(address_key)
90
+ node.addresses[address_key] = sender_public_key_bytes
91
+
92
+ if old_key_bytes is None:
93
+ # brand-new address -> brand-new peer
94
+ try:
95
+ peer = Peer(node.relay_secret_key, sender_key)
96
+ except Exception:
97
+ continue
98
+
99
+ node.peers[sender_public_key_bytes] = peer
100
+ node.peer_route.add_peer(sender_public_key_bytes)
101
+
102
+ response = Message(handshake=True, sender=node.relay_public_key)
103
+ node.outgoing_queue.put((response.to_bytes(), address_key))
104
+ continue
105
+
106
+ elif old_key_bytes == sender_public_key_bytes:
107
+ # existing mapping with same key -> nothing to change
108
+ pass
109
+
110
+ else:
111
+ # address reused with a different key -> replace peer
112
+ node.peers.pop(old_key_bytes, None)
113
+ try:
114
+ peer = Peer(node.relay_secret_key, sender_key)
115
+ except Exception:
116
+ continue
117
+
118
+ node.peers[sender_public_key_bytes] = peer
119
+
120
+ match message.topic:
121
+ case MessageTopic.PING:
122
+ try:
123
+ host, port = addr[0], int(addr[1])
124
+ except Exception:
125
+ continue
126
+ address_key = (host, port)
127
+ sender_public_key_bytes = node.addresses.get(address_key)
128
+ if sender_public_key_bytes is None:
129
+ continue
130
+ peer = node.peers.get(sender_public_key_bytes)
131
+ if peer is None:
132
+ continue
133
+ try:
134
+ ping = Ping.from_bytes(message.content)
135
+ except Exception as exc:
136
+ print(f"Error decoding ping: {exc}")
137
+ continue
138
+
139
+ peer.timestamp = datetime.now(timezone.utc)
140
+ peer.latest_block = ping.latest_block
141
+
142
+ validation_route = node.validation_route
143
+ if validation_route is None:
144
+ continue
145
+ if ping.is_validator:
146
+ try:
147
+ validation_route.add_peer(sender_public_key_bytes)
148
+ except Exception:
149
+ pass
150
+ else:
151
+ try:
152
+ validation_route.remove_peer(sender_public_key_bytes)
153
+ except Exception:
154
+ pass
155
+ case MessageTopic.OBJECT_REQUEST:
156
+ pass
157
+ case MessageTopic.OBJECT_RESPONSE:
158
+ pass
159
+ case MessageTopic.ROUTE_REQUEST:
160
+ pass
161
+ case MessageTopic.ROUTE_RESPONSE:
162
+ pass
163
+ case MessageTopic.TRANSACTION:
164
+ if node.validation_secret_key is None:
165
+ continue
166
+ node._validation_transaction_queue.put(message.content)
167
+
168
+ case _:
169
+ continue
170
+
171
+
172
+ def populate_incoming_messages(node: "Node") -> None:
173
+ """Receive UDP packets and feed the incoming queue (placeholder)."""
174
+ while True:
175
+ try:
176
+ data, addr = node.incoming_socket.recvfrom(4096)
177
+ node.incoming_queue.put((data, addr))
178
+ except Exception as exc:
179
+ print(f"Error populating incoming queue: {exc}")
180
+
181
+ def communication_setup(node: "Node", config: dict):
182
+ node.use_ipv6 = config.get('use_ipv6', False)
183
+
184
+ # key loading
185
+ node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
71
186
  node.validation_secret_key = load_ed25519(config.get('validation_secret_key'))
72
187
 
73
188
  # derive pubs + routes
@@ -80,34 +195,60 @@ def communication_setup(node: "Node", config: dict):
80
195
  if node.validation_secret_key
81
196
  else None
82
197
  )
83
- node.peer_route, node.validation_route = make_routes(
84
- node.relay_public_key,
85
- node.validation_secret_key
86
- )
87
-
88
- # sockets + queues + threads
89
- (node.incoming_socket,
90
- node.incoming_port,
91
- node.incoming_queue,
92
- node.incoming_populate_thread,
93
- node.incoming_process_thread
94
- ) = setup_udp(config.get('incoming_port', 7373), node.use_ipv6)
95
-
96
- (node.outgoing_socket,
97
- node.outgoing_queue,
98
- node.outgoing_thread
99
- ) = setup_outgoing(node.use_ipv6)
100
-
101
- # other workers & maps
102
- node.object_request_queue = Queue()
103
- node.peer_manager_thread = threading.Thread(
104
- target=node._relay_peer_manager,
105
- daemon=True
106
- )
107
- node.peer_manager_thread.start()
108
-
109
- node.peers, node.addresses = {}, {} # peers: Dict[X25519PublicKey,Peer], addresses: Dict[(str,int),X25519PublicKey]
110
-
111
- # bootstrap pings
112
- for addr in config.get('bootstrap', []):
113
- node._send_ping(addr)
198
+ node.peer_route, node.validation_route = make_routes(
199
+ node.relay_public_key,
200
+ node.validation_secret_key
201
+ )
202
+
203
+ # sockets + queues + threads
204
+ incoming_port = config.get('incoming_port', 7373)
205
+ fam = socket.AF_INET6 if node.use_ipv6 else socket.AF_INET
206
+ node.incoming_socket = socket.socket(fam, socket.SOCK_DGRAM)
207
+ if node.use_ipv6:
208
+ node.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
209
+ node.incoming_socket.bind(("::" if node.use_ipv6 else "0.0.0.0", incoming_port or 0))
210
+ node.incoming_port = node.incoming_socket.getsockname()[1]
211
+ node.incoming_queue = Queue()
212
+ node.incoming_populate_thread = threading.Thread(
213
+ target=populate_incoming_messages,
214
+ args=(node,),
215
+ daemon=True,
216
+ )
217
+ node.incoming_process_thread = threading.Thread(
218
+ target=process_incoming_messages,
219
+ args=(node,),
220
+ daemon=True,
221
+ )
222
+ node.incoming_populate_thread.start()
223
+ node.incoming_process_thread.start()
224
+
225
+ (node.outgoing_socket,
226
+ node.outgoing_queue,
227
+ node.outgoing_thread
228
+ ) = setup_outgoing(node.use_ipv6)
229
+
230
+ # other workers & maps
231
+ node.object_request_queue = Queue()
232
+ node.peer_manager_thread = threading.Thread(
233
+ target=node._relay_peer_manager,
234
+ daemon=True
235
+ )
236
+ node.peer_manager_thread.start()
237
+
238
+ node.peers, node.addresses = {}, {} # peers: Dict[bytes,Peer], addresses: Dict[(str,int),bytes]
239
+ latest_hash = getattr(node, "latest_block_hash", None)
240
+ if not isinstance(latest_hash, (bytes, bytearray)) or len(latest_hash) != 32:
241
+ node.latest_block_hash = bytes(32)
242
+ else:
243
+ node.latest_block_hash = bytes(latest_hash)
244
+
245
+ # bootstrap pings
246
+ for addr in config.get('bootstrap', []):
247
+ try:
248
+ host, port = address_str_to_host_and_port(addr) # type: ignore[arg-type]
249
+ except Exception:
250
+ continue
251
+
252
+ handshake_message = Message(handshake=True, sender=node.relay_public_key)
253
+
254
+ node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))