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.
- astreum/__init__.py +16 -7
- astreum/{_communication → communication}/__init__.py +3 -3
- astreum/communication/handlers/handshake.py +89 -0
- astreum/communication/handlers/object_request.py +176 -0
- astreum/communication/handlers/object_response.py +115 -0
- astreum/communication/handlers/ping.py +34 -0
- astreum/communication/handlers/route_request.py +76 -0
- astreum/communication/handlers/route_response.py +53 -0
- astreum/communication/models/__init__.py +0 -0
- astreum/communication/models/message.py +124 -0
- astreum/communication/models/peer.py +51 -0
- astreum/{_communication → communication/models}/route.py +7 -12
- astreum/communication/processors/__init__.py +0 -0
- astreum/communication/processors/incoming.py +98 -0
- astreum/communication/processors/outgoing.py +20 -0
- astreum/communication/setup.py +166 -0
- astreum/communication/start.py +37 -0
- astreum/{_communication → communication}/util.py +7 -0
- astreum/consensus/__init__.py +20 -0
- astreum/consensus/genesis.py +66 -0
- astreum/consensus/models/__init__.py +0 -0
- astreum/consensus/models/account.py +84 -0
- astreum/consensus/models/accounts.py +72 -0
- astreum/consensus/models/block.py +364 -0
- astreum/{_consensus → consensus/models}/chain.py +7 -7
- astreum/{_consensus → consensus/models}/fork.py +8 -8
- astreum/consensus/models/receipt.py +98 -0
- astreum/{_consensus → consensus/models}/transaction.py +76 -78
- astreum/{_consensus → consensus}/setup.py +18 -50
- astreum/consensus/start.py +67 -0
- astreum/consensus/validator.py +95 -0
- astreum/{_consensus → consensus}/workers/discovery.py +19 -1
- astreum/consensus/workers/validation.py +307 -0
- astreum/{_consensus → consensus}/workers/verify.py +29 -2
- astreum/crypto/chacha20poly1305.py +74 -0
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
- astreum/machine/evaluations/low_evaluation.py +281 -0
- astreum/machine/evaluations/script_evaluation.py +27 -0
- astreum/machine/models/__init__.py +0 -0
- astreum/machine/models/environment.py +31 -0
- astreum/{_lispeum → machine/models}/expression.py +36 -8
- astreum/machine/tokenizer.py +90 -0
- astreum/node.py +78 -767
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +183 -0
- astreum/storage/actions/set.py +178 -0
- astreum/{_storage → storage/models}/atom.py +55 -57
- astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
- astreum/storage/requests.py +28 -0
- astreum/storage/setup.py +22 -15
- astreum/utils/config.py +48 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
- astreum-0.3.9.dist-info/RECORD +71 -0
- astreum/_communication/message.py +0 -101
- astreum/_communication/peer.py +0 -23
- astreum/_communication/setup.py +0 -322
- astreum/_consensus/__init__.py +0 -20
- astreum/_consensus/account.py +0 -95
- astreum/_consensus/accounts.py +0 -38
- astreum/_consensus/block.py +0 -311
- astreum/_consensus/genesis.py +0 -72
- astreum/_consensus/receipt.py +0 -136
- astreum/_consensus/workers/validation.py +0 -125
- astreum/_lispeum/__init__.py +0 -16
- astreum/_lispeum/environment.py +0 -13
- astreum/_lispeum/low_evaluation.py +0 -123
- astreum/_lispeum/tokenizer.py +0 -22
- astreum/_node.py +0 -198
- astreum/_storage/__init__.py +0 -7
- astreum/_storage/setup.py +0 -35
- astreum/format.py +0 -75
- astreum/models/block.py +0 -441
- astreum/models/merkle.py +0 -205
- astreum/models/patricia.py +0 -393
- astreum/storage/object.py +0 -68
- astreum-0.2.61.dist-info/RECORD +0 -57
- /astreum/{models → communication/handlers}/__init__.py +0 -0
- /astreum/{_communication → communication/models}/ping.py +0 -0
- /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
- /astreum/{_lispeum → machine/models}/meter.py +0 -0
- /astreum/{_lispeum → machine}/parser.py +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {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 =
|
|
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
|