astreum 0.2.41__py3-none-any.whl → 0.3.1__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 (82) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +83 -0
  4. astreum/communication/handlers/ping.py +48 -0
  5. astreum/communication/handlers/storage_request.py +81 -0
  6. astreum/communication/models/__init__.py +0 -0
  7. astreum/{_communication → communication/models}/message.py +1 -0
  8. astreum/communication/models/peer.py +23 -0
  9. astreum/{_communication → communication/models}/route.py +45 -8
  10. astreum/{_communication → communication}/setup.py +46 -95
  11. astreum/communication/start.py +38 -0
  12. astreum/consensus/__init__.py +20 -0
  13. astreum/consensus/genesis.py +66 -0
  14. astreum/consensus/models/__init__.py +0 -0
  15. astreum/consensus/models/account.py +84 -0
  16. astreum/consensus/models/accounts.py +72 -0
  17. astreum/consensus/models/block.py +364 -0
  18. astreum/{_consensus → consensus/models}/chain.py +7 -7
  19. astreum/{_consensus → consensus/models}/fork.py +8 -8
  20. astreum/consensus/models/receipt.py +98 -0
  21. astreum/consensus/models/transaction.py +213 -0
  22. astreum/{_consensus → consensus}/setup.py +26 -11
  23. astreum/consensus/start.py +68 -0
  24. astreum/consensus/validator.py +95 -0
  25. astreum/{_consensus → consensus}/workers/discovery.py +20 -1
  26. astreum/consensus/workers/validation.py +291 -0
  27. astreum/{_consensus → consensus}/workers/verify.py +32 -3
  28. astreum/machine/__init__.py +20 -0
  29. astreum/machine/evaluations/__init__.py +0 -0
  30. astreum/machine/evaluations/high_evaluation.py +237 -0
  31. astreum/machine/evaluations/low_evaluation.py +281 -0
  32. astreum/machine/evaluations/script_evaluation.py +27 -0
  33. astreum/machine/models/__init__.py +0 -0
  34. astreum/machine/models/environment.py +31 -0
  35. astreum/machine/models/expression.py +218 -0
  36. astreum/{_lispeum → machine}/parser.py +26 -31
  37. astreum/machine/tokenizer.py +90 -0
  38. astreum/node.py +73 -781
  39. astreum/storage/__init__.py +7 -0
  40. astreum/storage/actions/get.py +69 -0
  41. astreum/storage/actions/set.py +132 -0
  42. astreum/storage/models/atom.py +107 -0
  43. astreum/{_storage/patricia.py → storage/models/trie.py} +236 -177
  44. astreum/storage/setup.py +44 -15
  45. astreum/utils/bytes.py +24 -0
  46. astreum/utils/integer.py +25 -0
  47. astreum/utils/logging.py +219 -0
  48. astreum-0.3.1.dist-info/METADATA +160 -0
  49. astreum-0.3.1.dist-info/RECORD +62 -0
  50. astreum/_communication/peer.py +0 -11
  51. astreum/_consensus/__init__.py +0 -20
  52. astreum/_consensus/account.py +0 -170
  53. astreum/_consensus/accounts.py +0 -67
  54. astreum/_consensus/block.py +0 -328
  55. astreum/_consensus/genesis.py +0 -141
  56. astreum/_consensus/receipt.py +0 -177
  57. astreum/_consensus/transaction.py +0 -192
  58. astreum/_consensus/workers/validation.py +0 -122
  59. astreum/_lispeum/__init__.py +0 -16
  60. astreum/_lispeum/environment.py +0 -13
  61. astreum/_lispeum/expression.py +0 -37
  62. astreum/_lispeum/high_evaluation.py +0 -177
  63. astreum/_lispeum/low_evaluation.py +0 -123
  64. astreum/_lispeum/tokenizer.py +0 -22
  65. astreum/_node.py +0 -58
  66. astreum/_storage/__init__.py +0 -5
  67. astreum/_storage/atom.py +0 -117
  68. astreum/format.py +0 -75
  69. astreum/models/block.py +0 -441
  70. astreum/models/merkle.py +0 -205
  71. astreum/models/patricia.py +0 -393
  72. astreum/storage/object.py +0 -68
  73. astreum-0.2.41.dist-info/METADATA +0 -146
  74. astreum-0.2.41.dist-info/RECORD +0 -53
  75. /astreum/{models → communication/handlers}/__init__.py +0 -0
  76. /astreum/{_communication → communication/models}/ping.py +0 -0
  77. /astreum/{_communication → communication}/util.py +0 -0
  78. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  79. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  80. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
  81. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
  82. {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/top_level.txt +0 -0
astreum/__init__.py CHANGED
@@ -1,9 +1,18 @@
1
- """Lightweight package initializer to avoid circular imports during tests.
1
+
2
+ from astreum.consensus import Account, Accounts, Block, Chain, Fork, Receipt, Transaction
3
+ from astreum.machine import Env, Expr
4
+ from astreum.node import Node
2
5
 
3
- Exports are intentionally minimal; import submodules directly as needed:
4
- - Node, Expr, Env, tokenize, parse -> from astreum._node or astreum.lispeum
5
- - Validation types -> from astreum._validation
6
- - Storage types -> from astreum._storage
7
- """
8
6
 
9
- __all__: list[str] = []
7
+ __all__: list[str] = [
8
+ "Node",
9
+ "Env",
10
+ "Expr",
11
+ "Block",
12
+ "Chain",
13
+ "Fork",
14
+ "Receipt",
15
+ "Transaction",
16
+ "Account",
17
+ "Accounts",
18
+ ]
@@ -1,6 +1,6 @@
1
- from .message import Message
2
- from .peer import Peer
3
- from .route import Route
1
+ from .models.message import Message
2
+ from .models.peer import Peer
3
+ from .models.route import Route
4
4
  from .setup import communication_setup
5
5
 
6
6
  __all__ = [
@@ -0,0 +1,83 @@
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
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import TYPE_CHECKING, Sequence
5
+
6
+ from ..models.ping import Ping
7
+
8
+ if TYPE_CHECKING:
9
+ from .... import Node
10
+
11
+
12
+ def handle_ping(node: "Node", addr: Sequence[object], payload: bytes) -> None:
13
+ """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
+ try:
30
+ ping = Ping.from_bytes(payload)
31
+ except Exception as exc:
32
+ logger.warning("Error decoding ping: %s", exc)
33
+ return
34
+
35
+ peer.timestamp = datetime.now(timezone.utc)
36
+ peer.latest_block = ping.latest_block
37
+
38
+ validation_route = node.validation_route
39
+ if validation_route is None:
40
+ return
41
+
42
+ try:
43
+ if ping.is_validator:
44
+ validation_route.add_peer(sender_public_key_bytes)
45
+ else:
46
+ validation_route.remove_peer(sender_public_key_bytes)
47
+ except Exception:
48
+ pass
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from cryptography.hazmat.primitives import serialization
6
+
7
+ if TYPE_CHECKING:
8
+ from .... import Node
9
+ from ..models.message import Message
10
+
11
+
12
+ def handle_storage_request(node: "Node", addr: Sequence[object], message: "Message") -> None:
13
+ """Process incoming storage request payloads, forwarding if needed."""
14
+ logger = node.logger
15
+ payload = message.content
16
+ if len(payload) < 32:
17
+ return
18
+
19
+ atom_id = payload[:32]
20
+ provider_bytes = payload[32:]
21
+ if not provider_bytes:
22
+ return
23
+
24
+ try:
25
+ provider_str = provider_bytes.decode("utf-8")
26
+ except UnicodeDecodeError:
27
+ return
28
+
29
+ try:
30
+ host, port = addr[0], int(addr[1])
31
+ except Exception:
32
+ return
33
+ address_key = (host, port)
34
+ sender_key_bytes = node.addresses.get(address_key)
35
+ if sender_key_bytes is None:
36
+ return
37
+
38
+ try:
39
+ local_key_bytes = node.relay_public_key.public_bytes(
40
+ encoding=serialization.Encoding.Raw,
41
+ format=serialization.PublicFormat.Raw,
42
+ )
43
+ except Exception:
44
+ return
45
+
46
+ def xor_distance(target: bytes, key: bytes) -> int:
47
+ return int.from_bytes(
48
+ bytes(a ^ b for a, b in zip(target, key)),
49
+ byteorder="big",
50
+ signed=False,
51
+ )
52
+
53
+ self_distance = xor_distance(atom_id, local_key_bytes)
54
+
55
+ try:
56
+ closest_peer = node.peer_route.closest_peer_for_hash(atom_id)
57
+ except Exception:
58
+ closest_peer = None
59
+
60
+ if closest_peer is not None and closest_peer.public_key_bytes != sender_key_bytes:
61
+ closest_distance = xor_distance(atom_id, closest_peer.public_key_bytes)
62
+ if closest_distance < self_distance:
63
+ target_addr = closest_peer.address
64
+ if target_addr is not None and target_addr != addr:
65
+ try:
66
+ node.outgoing_queue.put((message.to_bytes(), target_addr))
67
+ except Exception:
68
+ return
69
+ logger.debug(
70
+ "Forwarded storage request for %s to %s",
71
+ atom_id.hex(),
72
+ target_addr,
73
+ )
74
+ return
75
+
76
+ node.storage_index[atom_id] = provider_str.strip()
77
+ logger.debug(
78
+ "Stored provider %s for atom %s",
79
+ provider_str.strip(),
80
+ atom_id.hex(),
81
+ )
File without changes
@@ -10,6 +10,7 @@ class MessageTopic(IntEnum):
10
10
  ROUTE_REQUEST = 3
11
11
  ROUTE_RESPONSE = 4
12
12
  TRANSACTION = 5
13
+ STORAGE_REQUEST = 6
13
14
 
14
15
 
15
16
  class Message:
@@ -0,0 +1,23 @@
1
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
2
+ from cryptography.hazmat.primitives import serialization
3
+ from datetime import datetime, timezone
4
+ from typing import Optional, Tuple
5
+
6
+ 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)
16
+ 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(
21
+ encoding=serialization.Encoding.Raw,
22
+ format=serialization.PublicFormat.Raw,
23
+ )
@@ -1,6 +1,7 @@
1
- from typing import Dict, List, Union
1
+ 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
+ from .peer import Peer
4
5
 
5
6
  PeerKey = Union[X25519PublicKey, bytes, bytearray]
6
7
 
@@ -15,14 +16,14 @@ class Route:
15
16
  self.buckets: Dict[int, List[bytes]] = {
16
17
  i: [] for i in range(len(self.relay_public_key_bytes) * 8)
17
18
  }
18
- self.peers = {}
19
+ self.peers: Dict[bytes, Peer] = {}
19
20
 
20
21
  @staticmethod
21
- def _matching_leading_bits(a: bytes, b: bytes) -> int:
22
- for byte_index, (ba, bb) in enumerate(zip(a, b)):
23
- diff = ba ^ bb
24
- if diff:
25
- return byte_index * 8 + (8 - diff.bit_length())
22
+ def _matching_leading_bits(a: bytes, b: bytes) -> int:
23
+ for byte_index, (ba, bb) in enumerate(zip(a, b)):
24
+ diff = ba ^ bb
25
+ if diff:
26
+ return byte_index * 8 + (8 - diff.bit_length())
26
27
  return len(a) * 8
27
28
 
28
29
  def _normalize_peer_key(self, peer_public_key: PeerKey) -> bytes:
@@ -38,13 +39,21 @@ class Route:
38
39
  return key_bytes
39
40
  raise TypeError("peer_public_key must be raw bytes or X25519PublicKey")
40
41
 
41
- def add_peer(self, peer_public_key: PeerKey):
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
+ def add_peer(self, peer_public_key: PeerKey, peer: Optional[Peer] = None):
42
49
  peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
43
50
  bucket_idx = self._matching_leading_bits(self.relay_public_key_bytes, peer_public_key_bytes)
44
51
  if len(self.buckets[bucket_idx]) < self.bucket_size:
45
52
  bucket = self.buckets[bucket_idx]
46
53
  if peer_public_key_bytes not in bucket:
47
54
  bucket.append(peer_public_key_bytes)
55
+ if peer is not None:
56
+ self.peers[peer_public_key_bytes] = peer
48
57
 
49
58
  def remove_peer(self, peer_public_key: PeerKey):
50
59
  peer_public_key_bytes = self._normalize_peer_key(peer_public_key)
@@ -56,3 +65,31 @@ class Route:
56
65
  bucket.remove(peer_public_key_bytes)
57
66
  except ValueError:
58
67
  pass
68
+ self.peers.pop(peer_public_key_bytes, None)
69
+
70
+ def closest_peer_for_hash(self, target_hash: bytes) -> Optional[Peer]:
71
+ """Return the peer with the minimal XOR distance to ``target_hash``."""
72
+ if not isinstance(target_hash, (bytes, bytearray)):
73
+ raise TypeError("target_hash must be bytes-like")
74
+
75
+ target = bytes(target_hash)
76
+ if len(target) != len(self.relay_public_key_bytes):
77
+ raise ValueError("target_hash must match peer key length (32 bytes)")
78
+
79
+ closest_key: Optional[bytes] = None
80
+ closest_distance: Optional[int] = None
81
+
82
+ for bucket in self.buckets.values():
83
+ for peer_key in bucket:
84
+ try:
85
+ distance = self._xor_distance(target, peer_key)
86
+ except ValueError:
87
+ continue
88
+ if closest_distance is None or distance < closest_distance:
89
+ closest_distance = distance
90
+ closest_key = peer_key
91
+
92
+ if closest_key is None:
93
+ return None
94
+ peer = self.peers.get(closest_key)
95
+ return peer
@@ -1,9 +1,8 @@
1
1
  import socket, threading
2
- from datetime import datetime, timezone
3
2
  from queue import Queue
4
3
  from typing import Tuple, Optional
5
- from cryptography.hazmat.primitives.asymmetric import ed25519
6
4
  from cryptography.hazmat.primitives import serialization
5
+ from cryptography.hazmat.primitives.asymmetric import ed25519
7
6
  from cryptography.hazmat.primitives.asymmetric.x25519 import (
8
7
  X25519PrivateKey,
9
8
  X25519PublicKey,
@@ -14,14 +13,18 @@ if TYPE_CHECKING:
14
13
  from .. import Node
15
14
 
16
15
  from . import Route, Message
17
- from .message import MessageTopic
18
- from .peer import Peer
19
- from .ping import Ping
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
20
20
  from .util import address_str_to_host_and_port
21
+ from ..utils.bytes import hex_to_bytes
21
22
 
22
23
  def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
23
24
  """DH key for relaying (always X25519)."""
24
- return
25
+ if hex_key:
26
+ return X25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key))
27
+ return X25519PrivateKey.generate()
25
28
 
26
29
  def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
27
30
  """Signing key for validation (Ed25519), or None if absent."""
@@ -54,104 +57,27 @@ def make_maps():
54
57
 
55
58
  def process_incoming_messages(node: "Node") -> None:
56
59
  """Process incoming messages (placeholder)."""
60
+ node_logger = node.logger
57
61
  while True:
58
62
  try:
59
63
  data, addr = node.incoming_queue.get()
60
64
  except Exception as exc:
61
- print(f"Error taking from incoming queue: {exc}")
65
+ node_logger.exception("Error taking from incoming queue")
62
66
  continue
63
67
 
64
68
  try:
65
69
  message = Message.from_bytes(data)
66
70
  except Exception as exc:
67
- print(f"Error decoding message: {exc}")
71
+ node_logger.warning("Error decoding message: %s", exc)
68
72
  continue
69
73
 
70
74
  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))
75
+ if handle_handshake(node, addr, message):
104
76
  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
77
 
120
78
  match message.topic:
121
79
  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
80
+ handle_ping(node, addr, message.content)
155
81
  case MessageTopic.OBJECT_REQUEST:
156
82
  pass
157
83
  case MessageTopic.OBJECT_RESPONSE:
@@ -164,6 +90,9 @@ def process_incoming_messages(node: "Node") -> None:
164
90
  if node.validation_secret_key is None:
165
91
  continue
166
92
  node._validation_transaction_queue.put(message.content)
93
+
94
+ case MessageTopic.STORAGE_REQUEST:
95
+ handle_storage_request(node, addr, message)
167
96
 
168
97
  case _:
169
98
  continue
@@ -171,14 +100,16 @@ def process_incoming_messages(node: "Node") -> None:
171
100
 
172
101
  def populate_incoming_messages(node: "Node") -> None:
173
102
  """Receive UDP packets and feed the incoming queue (placeholder)."""
103
+ node_logger = node.logger
174
104
  while True:
175
105
  try:
176
106
  data, addr = node.incoming_socket.recvfrom(4096)
177
107
  node.incoming_queue.put((data, addr))
178
108
  except Exception as exc:
179
- print(f"Error populating incoming queue: {exc}")
109
+ node_logger.warning("Error populating incoming queue: %s", exc)
180
110
 
181
111
  def communication_setup(node: "Node", config: dict):
112
+ node.logger.info("Setting up node communication")
182
113
  node.use_ipv6 = config.get('use_ipv6', False)
183
114
 
184
115
  # key loading
@@ -208,6 +139,11 @@ def communication_setup(node: "Node", config: dict):
208
139
  node.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
209
140
  node.incoming_socket.bind(("::" if node.use_ipv6 else "0.0.0.0", incoming_port or 0))
210
141
  node.incoming_port = node.incoming_socket.getsockname()[1]
142
+ node.logger.info(
143
+ "Incoming UDP socket bound to %s:%s",
144
+ "::" if node.use_ipv6 else "0.0.0.0",
145
+ node.incoming_port,
146
+ )
211
147
  node.incoming_queue = Queue()
212
148
  node.incoming_populate_thread = threading.Thread(
213
149
  target=populate_incoming_messages,
@@ -236,19 +172,34 @@ def communication_setup(node: "Node", config: dict):
236
172
  node.peer_manager_thread.start()
237
173
 
238
174
  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)
175
+
176
+ latest_block_hex = config.get("latest_block_hash")
177
+ if latest_block_hex:
178
+ try:
179
+ node.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
180
+ except Exception as exc:
181
+ node.logger.warning("Invalid latest_block_hash in config: %s", exc)
182
+ node.latest_block_hash = None
242
183
  else:
243
- node.latest_block_hash = bytes(latest_hash)
184
+ node.latest_block_hash = None
244
185
 
245
186
  # bootstrap pings
246
- for addr in config.get('bootstrap', []):
187
+ bootstrap_peers = config.get('bootstrap', [])
188
+ for addr in bootstrap_peers:
247
189
  try:
248
190
  host, port = address_str_to_host_and_port(addr) # type: ignore[arg-type]
249
- except Exception:
191
+ except Exception as exc:
192
+ node.logger.warning("Invalid bootstrap address %s: %s", addr, exc)
250
193
  continue
251
194
 
252
195
  handshake_message = Message(handshake=True, sender=node.relay_public_key)
253
196
 
254
197
  node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))
198
+ node.logger.info("Sent bootstrap handshake to %s:%s", host, port)
199
+
200
+ node.logger.info(
201
+ "Communication ready (incoming_port=%s, outgoing_socket_initialized=%s, bootstrap_count=%s)",
202
+ node.incoming_port,
203
+ node.outgoing_socket is not None,
204
+ len(bootstrap_peers),
205
+ )
@@ -0,0 +1,38 @@
1
+ def connect_to_network_and_verify(self):
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")
5
+ try:
6
+ from astreum.communication import communication_setup # type: ignore
7
+ communication_setup(node=self, config=self.config)
8
+ node_logger.info("Communication setup completed")
9
+ except Exception:
10
+ node_logger.exception("Communication setup failed")
11
+
12
+ try:
13
+ from astreum.consensus import consensus_setup # type: ignore
14
+ consensus_setup(node=self, config=self.config)
15
+ node_logger.info("Consensus setup completed")
16
+ except Exception:
17
+ node_logger.exception("Consensus setup failed")
18
+
19
+ # Load latest_block_hash from config
20
+ self.latest_block_hash = getattr(self, "latest_block_hash", None)
21
+ self.latest_block = getattr(self, "latest_block", None)
22
+
23
+ latest_block_hex = self.config.get("latest_block_hash")
24
+ if latest_block_hex and self.latest_block_hash is None:
25
+ try:
26
+ from astreum.utils.bytes import hex_to_bytes
27
+ self.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
28
+ node_logger.debug("Loaded latest_block_hash override from config")
29
+ except Exception as exc:
30
+ node_logger.error("Invalid latest_block_hash in config: %s", exc)
31
+
32
+ if self.latest_block_hash and self.latest_block is None:
33
+ try:
34
+ from astreum.consensus.models.block import Block
35
+ 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())
37
+ except Exception as exc:
38
+ node_logger.warning("Could not load latest block from storage: %s", exc)
@@ -0,0 +1,20 @@
1
+ from .models.account import Account
2
+ from .models.accounts import Accounts
3
+ from .models.block import Block
4
+ from .models.chain import Chain
5
+ from .models.fork import Fork
6
+ from .models.receipt import Receipt
7
+ from .models.transaction import Transaction
8
+ from .setup import consensus_setup
9
+
10
+
11
+ __all__ = [
12
+ "Block",
13
+ "Chain",
14
+ "Fork",
15
+ "Receipt",
16
+ "Transaction",
17
+ "Account",
18
+ "Accounts",
19
+ "consensus_setup",
20
+ ]