astreum 0.2.61__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 (86) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +89 -0
  4. astreum/communication/handlers/object_request.py +176 -0
  5. astreum/communication/handlers/object_response.py +115 -0
  6. astreum/communication/handlers/ping.py +34 -0
  7. astreum/communication/handlers/route_request.py +76 -0
  8. astreum/communication/handlers/route_response.py +53 -0
  9. astreum/communication/models/__init__.py +0 -0
  10. astreum/communication/models/message.py +124 -0
  11. astreum/communication/models/peer.py +51 -0
  12. astreum/{_communication → communication/models}/route.py +7 -12
  13. astreum/communication/processors/__init__.py +0 -0
  14. astreum/communication/processors/incoming.py +98 -0
  15. astreum/communication/processors/outgoing.py +20 -0
  16. astreum/communication/setup.py +166 -0
  17. astreum/communication/start.py +37 -0
  18. astreum/{_communication → communication}/util.py +7 -0
  19. astreum/consensus/__init__.py +20 -0
  20. astreum/consensus/genesis.py +66 -0
  21. astreum/consensus/models/__init__.py +0 -0
  22. astreum/consensus/models/account.py +84 -0
  23. astreum/consensus/models/accounts.py +72 -0
  24. astreum/consensus/models/block.py +364 -0
  25. astreum/{_consensus → consensus/models}/chain.py +7 -7
  26. astreum/{_consensus → consensus/models}/fork.py +8 -8
  27. astreum/consensus/models/receipt.py +98 -0
  28. astreum/{_consensus → consensus/models}/transaction.py +76 -78
  29. astreum/{_consensus → consensus}/setup.py +18 -50
  30. astreum/consensus/start.py +67 -0
  31. astreum/consensus/validator.py +95 -0
  32. astreum/{_consensus → consensus}/workers/discovery.py +19 -1
  33. astreum/consensus/workers/validation.py +307 -0
  34. astreum/{_consensus → consensus}/workers/verify.py +29 -2
  35. astreum/crypto/chacha20poly1305.py +74 -0
  36. astreum/machine/__init__.py +20 -0
  37. astreum/machine/evaluations/__init__.py +0 -0
  38. astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
  39. astreum/machine/evaluations/low_evaluation.py +281 -0
  40. astreum/machine/evaluations/script_evaluation.py +27 -0
  41. astreum/machine/models/__init__.py +0 -0
  42. astreum/machine/models/environment.py +31 -0
  43. astreum/{_lispeum → machine/models}/expression.py +36 -8
  44. astreum/machine/tokenizer.py +90 -0
  45. astreum/node.py +78 -767
  46. astreum/storage/__init__.py +7 -0
  47. astreum/storage/actions/get.py +183 -0
  48. astreum/storage/actions/set.py +178 -0
  49. astreum/{_storage → storage/models}/atom.py +55 -57
  50. astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
  51. astreum/storage/requests.py +28 -0
  52. astreum/storage/setup.py +22 -15
  53. astreum/utils/config.py +48 -0
  54. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
  55. astreum-0.3.9.dist-info/RECORD +71 -0
  56. astreum/_communication/message.py +0 -101
  57. astreum/_communication/peer.py +0 -23
  58. astreum/_communication/setup.py +0 -322
  59. astreum/_consensus/__init__.py +0 -20
  60. astreum/_consensus/account.py +0 -95
  61. astreum/_consensus/accounts.py +0 -38
  62. astreum/_consensus/block.py +0 -311
  63. astreum/_consensus/genesis.py +0 -72
  64. astreum/_consensus/receipt.py +0 -136
  65. astreum/_consensus/workers/validation.py +0 -125
  66. astreum/_lispeum/__init__.py +0 -16
  67. astreum/_lispeum/environment.py +0 -13
  68. astreum/_lispeum/low_evaluation.py +0 -123
  69. astreum/_lispeum/tokenizer.py +0 -22
  70. astreum/_node.py +0 -198
  71. astreum/_storage/__init__.py +0 -7
  72. astreum/_storage/setup.py +0 -35
  73. astreum/format.py +0 -75
  74. astreum/models/block.py +0 -441
  75. astreum/models/merkle.py +0 -205
  76. astreum/models/patricia.py +0 -393
  77. astreum/storage/object.py +0 -68
  78. astreum-0.2.61.dist-info/RECORD +0 -57
  79. /astreum/{models → communication/handlers}/__init__.py +0 -0
  80. /astreum/{_communication → communication/models}/ping.py +0 -0
  81. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  82. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  83. /astreum/{_lispeum → machine}/parser.py +0 -0
  84. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
  85. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
  86. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,124 @@
1
+ import os
2
+ from enum import IntEnum
3
+ from typing import Optional
4
+ from cryptography.hazmat.primitives import serialization
5
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
6
+ from astreum.crypto import chacha20poly1305
7
+
8
+ class MessageTopic(IntEnum):
9
+ PING = 0
10
+ OBJECT_REQUEST = 1
11
+ OBJECT_RESPONSE = 2
12
+ ROUTE_REQUEST = 3
13
+ ROUTE_RESPONSE = 4
14
+ TRANSACTION = 5
15
+
16
+
17
+ class Message:
18
+ def __init__(
19
+ self,
20
+ *,
21
+ handshake: bool = False,
22
+ sender: Optional[X25519PublicKey] = None,
23
+ topic: Optional[MessageTopic] = None,
24
+ content: Optional[bytes] = None,
25
+ body: Optional[bytes] = None,
26
+ sender_bytes: Optional[bytes] = None,
27
+ encrypted: Optional[bytes] = None,
28
+ ) -> None:
29
+ if body is not None:
30
+ if content is not None and content != b"":
31
+ raise ValueError("specify only one of 'content' or 'body'")
32
+ content = body
33
+
34
+ self.handshake = handshake
35
+ self.topic = topic
36
+ self.content = content if content is not None else b""
37
+ self.encrypted = encrypted
38
+
39
+ if self.handshake:
40
+ if sender_bytes is None and sender is None:
41
+ raise ValueError("handshake Message requires a sender public key or sender bytes")
42
+ self.topic = None
43
+ else:
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")
48
+
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
+ encoding=serialization.Encoding.Raw,
56
+ format=serialization.PublicFormat.Raw,
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
63
+ else:
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
68
+
69
+ @classmethod
70
+ def from_bytes(cls, data: bytes) -> "Message":
71
+ if len(data) < 1:
72
+ raise ValueError("Cannot parse Message: no data")
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
+
84
+ else:
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")
118
+
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:]
@@ -0,0 +1,51 @@
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, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ...node import Node
8
+
9
+ class Peer:
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)
18
+ self.timestamp = datetime.now(timezone.utc)
19
+ self.latest_block = latest_block
20
+ self.address = address
21
+ self.public_key_bytes = peer_public_key.public_bytes(
22
+ encoding=serialization.Encoding.Raw,
23
+ format=serialization.PublicFormat.Raw,
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
 
@@ -19,11 +20,11 @@ class Route:
19
20
  self.peers: Dict[bytes, Peer] = {}
20
21
 
21
22
  @staticmethod
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())
23
+ def _matching_leading_bits(a: bytes, b: bytes) -> int:
24
+ for byte_index, (ba, bb) in enumerate(zip(a, b)):
25
+ diff = ba ^ bb
26
+ if diff:
27
+ return byte_index * 8 + (8 - diff.bit_length())
27
28
  return len(a) * 8
28
29
 
29
30
  def _normalize_peer_key(self, peer_public_key: PeerKey) -> bytes:
@@ -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)
@@ -0,0 +1,166 @@
1
+ import socket, threading
2
+ from queue import Queue
3
+ from typing import Tuple, Optional
4
+ from cryptography.hazmat.primitives import serialization
5
+ from cryptography.hazmat.primitives.asymmetric import ed25519
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, Message
16
+ from .processors.incoming import (
17
+ process_incoming_messages,
18
+ populate_incoming_messages,
19
+ )
20
+ from .processors.outgoing import process_outgoing_messages
21
+ from .util import address_str_to_host_and_port
22
+ from ..utils.bytes import hex_to_bytes
23
+
24
+ def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
25
+ """DH key for relaying (always X25519)."""
26
+ if hex_key:
27
+ return X25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key))
28
+ return X25519PrivateKey.generate()
29
+
30
+ def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
31
+ """Signing key for validation (Ed25519), or None if absent."""
32
+ return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
33
+ if hex_key else None
34
+
35
+ def make_routes(
36
+ relay_pk: X25519PublicKey,
37
+ val_sk: Optional[ed25519.Ed25519PrivateKey]
38
+ ) -> Tuple[Route, Optional[Route]]:
39
+ """Peer route (DH pubkey) + optional validation route (ed pubkey)."""
40
+ peer_rt = Route(relay_pk)
41
+ val_rt = Route(val_sk.public_key()) if val_sk else None
42
+ return peer_rt, val_rt
43
+
44
+ def make_maps():
45
+ """Empty lookup maps: peers and addresses."""
46
+ return
47
+
48
+
49
+ def communication_setup(node: "Node", config: dict):
50
+ node.logger.info("Setting up node communication")
51
+ node.use_ipv6 = config.get('use_ipv6', False)
52
+ node.peers_lock = threading.RLock()
53
+
54
+ # key loading
55
+ node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
56
+ node.validation_secret_key = load_ed25519(config.get('validation_secret_key'))
57
+
58
+ # derive pubs + routes
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
+ )
64
+ node.validation_public_key = (
65
+ node.validation_secret_key.public_key().public_bytes(
66
+ encoding=serialization.Encoding.Raw,
67
+ format=serialization.PublicFormat.Raw,
68
+ )
69
+ if node.validation_secret_key
70
+ else None
71
+ )
72
+ node.peer_route, node.validation_route = make_routes(
73
+ node.relay_public_key,
74
+ node.validation_secret_key
75
+ )
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
+
82
+ # sockets + queues + threads
83
+ incoming_port = config.get('incoming_port', 7373)
84
+ fam = socket.AF_INET6 if node.use_ipv6 else socket.AF_INET
85
+ node.incoming_socket = socket.socket(fam, socket.SOCK_DGRAM)
86
+ if node.use_ipv6:
87
+ node.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
88
+ node.incoming_socket.bind(("::" if node.use_ipv6 else "0.0.0.0", incoming_port or 0))
89
+ node.incoming_port = node.incoming_socket.getsockname()[1]
90
+ node.logger.info(
91
+ "Incoming UDP socket bound to %s:%s",
92
+ "::" if node.use_ipv6 else "0.0.0.0",
93
+ node.incoming_port,
94
+ )
95
+ node.incoming_queue = Queue()
96
+ node.incoming_populate_thread = threading.Thread(
97
+ target=populate_incoming_messages,
98
+ args=(node,),
99
+ daemon=True,
100
+ )
101
+ node.incoming_process_thread = threading.Thread(
102
+ target=process_incoming_messages,
103
+ args=(node,),
104
+ daemon=True,
105
+ )
106
+ node.incoming_populate_thread.start()
107
+ node.incoming_process_thread.start()
108
+
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()
121
+
122
+ # other workers & maps
123
+ # track atom requests we initiated; guarded by atom_requests_lock on the node
124
+ node.peer_manager_thread = threading.Thread(
125
+ target=node._relay_peer_manager,
126
+ daemon=True
127
+ )
128
+ node.peer_manager_thread.start()
129
+
130
+ with node.peers_lock:
131
+ node.peers, node.addresses = {}, {} # peers: Dict[bytes,Peer], addresses: Dict[(str,int),bytes]
132
+
133
+ latest_block_hex = config.get("latest_block_hash")
134
+ if latest_block_hex:
135
+ try:
136
+ node.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
137
+ except Exception as exc:
138
+ node.logger.warning("Invalid latest_block_hash in config: %s", exc)
139
+ node.latest_block_hash = None
140
+ else:
141
+ node.latest_block_hash = None
142
+
143
+ # bootstrap pings
144
+ bootstrap_peers = config.get('bootstrap', [])
145
+ for addr in bootstrap_peers:
146
+ try:
147
+ host, port = address_str_to_host_and_port(addr) # type: ignore[arg-type]
148
+ except Exception as exc:
149
+ node.logger.warning("Invalid bootstrap address %s: %s", addr, exc)
150
+ continue
151
+
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
+ )
157
+ node.outgoing_queue.put((handshake_message.to_bytes(), (host, port)))
158
+ node.logger.info("Sent bootstrap handshake to %s:%s", host, port)
159
+
160
+ node.logger.info(
161
+ "Communication ready (incoming_port=%s, outgoing_socket_initialized=%s, bootstrap_count=%s)",
162
+ node.incoming_port,
163
+ node.outgoing_socket is not None,
164
+ len(bootstrap_peers),
165
+ )
166
+ node.is_connected = True
@@ -0,0 +1,37 @@
1
+ def connect_to_network_and_verify(self):
2
+ """Initialize communication and consensus components, then load latest block state."""
3
+ self.logger.info("Starting communication and consensus setup")
4
+ try:
5
+ from astreum.communication import communication_setup # type: ignore
6
+ communication_setup(node=self, config=self.config)
7
+ self.logger.info("Communication setup completed")
8
+ except Exception:
9
+ self.logger.exception("Communication setup failed")
10
+
11
+ try:
12
+ from astreum.consensus import consensus_setup # type: ignore
13
+ consensus_setup(node=self, config=self.config)
14
+ self.logger.info("Consensus setup completed")
15
+ except Exception:
16
+ self.logger.exception("Consensus setup failed")
17
+
18
+ # Load latest_block_hash from config
19
+ self.latest_block_hash = getattr(self, "latest_block_hash", None)
20
+ self.latest_block = getattr(self, "latest_block", None)
21
+
22
+ latest_block_hex = self.config.get("latest_block_hash")
23
+ if latest_block_hex and self.latest_block_hash is None:
24
+ try:
25
+ from astreum.utils.bytes import hex_to_bytes
26
+ self.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
27
+ self.logger.debug("Loaded latest_block_hash override from config")
28
+ except Exception as exc:
29
+ self.logger.error("Invalid latest_block_hash in config: %s", exc)
30
+
31
+ if self.latest_block_hash and self.latest_block is None:
32
+ try:
33
+ from astreum.consensus.models.block import Block
34
+ self.latest_block = Block.from_atom(self, self.latest_block_hash)
35
+ self.logger.info("Loaded latest block %s from storage", self.latest_block_hash.hex())
36
+ except Exception as 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)
@@ -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
+ ]
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, List
4
+
5
+ from .models.account import Account
6
+ from .models.accounts import Accounts
7
+ from .models.block import Block
8
+ from ..storage.models.atom import ZERO32
9
+ from ..storage.models.trie import Trie
10
+ from ..utils.integer import int_to_bytes
11
+
12
+ TREASURY_ADDRESS = b"\x01" * 32
13
+ BURN_ADDRESS = b"\x00" * 32
14
+
15
+
16
+ def create_genesis_block(
17
+ node: Any,
18
+ validator_public_key: bytes,
19
+ chain_id: int = 0,
20
+ ) -> Block:
21
+ validator_pk = bytes(validator_public_key)
22
+
23
+ if len(validator_pk) != 32:
24
+ raise ValueError("validator_public_key must be 32 bytes")
25
+
26
+ stake_trie = Trie()
27
+ stake_amount = int_to_bytes(1)
28
+ stake_trie.put(storage_node=node, key=validator_pk, value=stake_amount)
29
+ stake_root = stake_trie.root_hash or ZERO32
30
+
31
+ treasury_account = Account.create(balance=1, data_hash=stake_root, counter=0)
32
+ treasury_account.data = stake_trie
33
+ treasury_account.data_hash = stake_root
34
+ burn_account = Account.create(balance=0, data_hash=b"", counter=0)
35
+ validator_account = Account.create(balance=0, data_hash=b"", counter=0)
36
+
37
+ accounts = Accounts()
38
+ accounts.set_account(TREASURY_ADDRESS, treasury_account)
39
+ accounts.set_account(BURN_ADDRESS, burn_account)
40
+ accounts.set_account(validator_pk, validator_account)
41
+
42
+ accounts.update_trie(node)
43
+ accounts_root = accounts.root_hash
44
+ if accounts_root is None:
45
+ raise ValueError("genesis accounts trie is empty")
46
+
47
+ block = Block(
48
+ chain_id=chain_id,
49
+ previous_block_hash=ZERO32,
50
+ previous_block=None,
51
+ number=0,
52
+ timestamp=0,
53
+ accounts_hash=accounts_root,
54
+ transactions_total_fees=0,
55
+ transactions_hash=ZERO32,
56
+ receipts_hash=ZERO32,
57
+ delay_difficulty=0,
58
+ validator_public_key=validator_pk,
59
+ nonce=0,
60
+ signature=b"",
61
+ accounts=accounts,
62
+ transactions=[],
63
+ receipts=[],
64
+ )
65
+
66
+ return block
File without changes