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.
- astreum/__init__.py +16 -7
- astreum/{_communication → communication}/__init__.py +3 -3
- astreum/communication/handlers/handshake.py +83 -0
- astreum/communication/handlers/ping.py +48 -0
- astreum/communication/handlers/storage_request.py +81 -0
- astreum/communication/models/__init__.py +0 -0
- astreum/{_communication → communication/models}/message.py +1 -0
- astreum/communication/models/peer.py +23 -0
- astreum/{_communication → communication/models}/route.py +45 -8
- astreum/{_communication → communication}/setup.py +46 -95
- astreum/communication/start.py +38 -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/models/transaction.py +213 -0
- astreum/{_consensus → consensus}/setup.py +26 -11
- astreum/consensus/start.py +68 -0
- astreum/consensus/validator.py +95 -0
- astreum/{_consensus → consensus}/workers/discovery.py +20 -1
- astreum/consensus/workers/validation.py +291 -0
- astreum/{_consensus → consensus}/workers/verify.py +32 -3
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/machine/evaluations/high_evaluation.py +237 -0
- 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/machine/models/expression.py +218 -0
- astreum/{_lispeum → machine}/parser.py +26 -31
- astreum/machine/tokenizer.py +90 -0
- astreum/node.py +73 -781
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +69 -0
- astreum/storage/actions/set.py +132 -0
- astreum/storage/models/atom.py +107 -0
- astreum/{_storage/patricia.py → storage/models/trie.py} +236 -177
- astreum/storage/setup.py +44 -15
- astreum/utils/bytes.py +24 -0
- astreum/utils/integer.py +25 -0
- astreum/utils/logging.py +219 -0
- astreum-0.3.1.dist-info/METADATA +160 -0
- astreum-0.3.1.dist-info/RECORD +62 -0
- astreum/_communication/peer.py +0 -11
- astreum/_consensus/__init__.py +0 -20
- astreum/_consensus/account.py +0 -170
- astreum/_consensus/accounts.py +0 -67
- astreum/_consensus/block.py +0 -328
- astreum/_consensus/genesis.py +0 -141
- astreum/_consensus/receipt.py +0 -177
- astreum/_consensus/transaction.py +0 -192
- astreum/_consensus/workers/validation.py +0 -122
- astreum/_lispeum/__init__.py +0 -16
- astreum/_lispeum/environment.py +0 -13
- astreum/_lispeum/expression.py +0 -37
- astreum/_lispeum/high_evaluation.py +0 -177
- astreum/_lispeum/low_evaluation.py +0 -123
- astreum/_lispeum/tokenizer.py +0 -22
- astreum/_node.py +0 -58
- astreum/_storage/__init__.py +0 -5
- astreum/_storage/atom.py +0 -117
- 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.41.dist-info/METADATA +0 -146
- astreum-0.2.41.dist-info/RECORD +0 -53
- /astreum/{models → communication/handlers}/__init__.py +0 -0
- /astreum/{_communication → communication/models}/ping.py +0 -0
- /astreum/{_communication → communication}/util.py +0 -0
- /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
- /astreum/{_lispeum → machine/models}/meter.py +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
- {astreum-0.2.41.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
+
]
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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 .
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
node_logger.warning("Error decoding message: %s", exc)
|
|
68
72
|
continue
|
|
69
73
|
|
|
70
74
|
if message.handshake:
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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 =
|
|
184
|
+
node.latest_block_hash = None
|
|
244
185
|
|
|
245
186
|
# bootstrap pings
|
|
246
|
-
|
|
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
|
+
]
|