astreum 0.2.61__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}/route.py +5 -5
- astreum/communication/setup.py +205 -0
- 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 → consensus/models}/transaction.py +76 -78
- astreum/{_consensus → consensus}/setup.py +18 -50
- 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 +31 -2
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/{_lispeum → machine/evaluations}/high_evaluation.py +16 -15
- 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 +73 -781
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +69 -0
- astreum/storage/actions/set.py +132 -0
- astreum/{_storage → storage/models}/atom.py +55 -57
- astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
- astreum/storage/setup.py +44 -15
- {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/METADATA +25 -24
- astreum-0.3.1.dist-info/RECORD +62 -0
- 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}/message.py +0 -0
- /astreum/{_communication → communication/models}/peer.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/{_lispeum → machine}/parser.py +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/WHEEL +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.61.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
|
|
@@ -19,11 +19,11 @@ class Route:
|
|
|
19
19
|
self.peers: Dict[bytes, Peer] = {}
|
|
20
20
|
|
|
21
21
|
@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())
|
|
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())
|
|
27
27
|
return len(a) * 8
|
|
28
28
|
|
|
29
29
|
def _normalize_peer_key(self, peer_public_key: PeerKey) -> bytes:
|
|
@@ -0,0 +1,205 @@
|
|
|
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 .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
|
+
from .util import address_str_to_host_and_port
|
|
21
|
+
from ..utils.bytes import hex_to_bytes
|
|
22
|
+
|
|
23
|
+
def load_x25519(hex_key: Optional[str]) -> X25519PrivateKey:
|
|
24
|
+
"""DH key for relaying (always X25519)."""
|
|
25
|
+
if hex_key:
|
|
26
|
+
return X25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key))
|
|
27
|
+
return X25519PrivateKey.generate()
|
|
28
|
+
|
|
29
|
+
def load_ed25519(hex_key: Optional[str]) -> Optional[ed25519.Ed25519PrivateKey]:
|
|
30
|
+
"""Signing key for validation (Ed25519), or None if absent."""
|
|
31
|
+
return ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(hex_key)) \
|
|
32
|
+
if hex_key else None
|
|
33
|
+
|
|
34
|
+
def make_routes(
|
|
35
|
+
relay_pk: X25519PublicKey,
|
|
36
|
+
val_sk: Optional[ed25519.Ed25519PrivateKey]
|
|
37
|
+
) -> Tuple[Route, Optional[Route]]:
|
|
38
|
+
"""Peer route (DH pubkey) + optional validation route (ed pubkey)."""
|
|
39
|
+
peer_rt = Route(relay_pk)
|
|
40
|
+
val_rt = Route(val_sk.public_key()) if val_sk else None
|
|
41
|
+
return peer_rt, val_rt
|
|
42
|
+
|
|
43
|
+
def setup_outgoing(
|
|
44
|
+
use_ipv6: bool
|
|
45
|
+
) -> Tuple[socket.socket, Queue, threading.Thread]:
|
|
46
|
+
fam = socket.AF_INET6 if use_ipv6 else socket.AF_INET
|
|
47
|
+
sock = socket.socket(fam, socket.SOCK_DGRAM)
|
|
48
|
+
q = Queue()
|
|
49
|
+
thr = threading.Thread(target=lambda: None, daemon=True)
|
|
50
|
+
thr.start()
|
|
51
|
+
return sock, q, thr
|
|
52
|
+
|
|
53
|
+
def make_maps():
|
|
54
|
+
"""Empty lookup maps: peers and addresses."""
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def process_incoming_messages(node: "Node") -> None:
|
|
59
|
+
"""Process incoming messages (placeholder)."""
|
|
60
|
+
node_logger = node.logger
|
|
61
|
+
while True:
|
|
62
|
+
try:
|
|
63
|
+
data, addr = node.incoming_queue.get()
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
node_logger.exception("Error taking from incoming queue")
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
message = Message.from_bytes(data)
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
node_logger.warning("Error decoding message: %s", exc)
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if message.handshake:
|
|
75
|
+
if handle_handshake(node, addr, message):
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
match message.topic:
|
|
79
|
+
case MessageTopic.PING:
|
|
80
|
+
handle_ping(node, addr, message.content)
|
|
81
|
+
case MessageTopic.OBJECT_REQUEST:
|
|
82
|
+
pass
|
|
83
|
+
case MessageTopic.OBJECT_RESPONSE:
|
|
84
|
+
pass
|
|
85
|
+
case MessageTopic.ROUTE_REQUEST:
|
|
86
|
+
pass
|
|
87
|
+
case MessageTopic.ROUTE_RESPONSE:
|
|
88
|
+
pass
|
|
89
|
+
case MessageTopic.TRANSACTION:
|
|
90
|
+
if node.validation_secret_key is None:
|
|
91
|
+
continue
|
|
92
|
+
node._validation_transaction_queue.put(message.content)
|
|
93
|
+
|
|
94
|
+
case MessageTopic.STORAGE_REQUEST:
|
|
95
|
+
handle_storage_request(node, addr, message)
|
|
96
|
+
|
|
97
|
+
case _:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def populate_incoming_messages(node: "Node") -> None:
|
|
102
|
+
"""Receive UDP packets and feed the incoming queue (placeholder)."""
|
|
103
|
+
node_logger = node.logger
|
|
104
|
+
while True:
|
|
105
|
+
try:
|
|
106
|
+
data, addr = node.incoming_socket.recvfrom(4096)
|
|
107
|
+
node.incoming_queue.put((data, addr))
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
node_logger.warning("Error populating incoming queue: %s", exc)
|
|
110
|
+
|
|
111
|
+
def communication_setup(node: "Node", config: dict):
|
|
112
|
+
node.logger.info("Setting up node communication")
|
|
113
|
+
node.use_ipv6 = config.get('use_ipv6', False)
|
|
114
|
+
|
|
115
|
+
# key loading
|
|
116
|
+
node.relay_secret_key = load_x25519(config.get('relay_secret_key'))
|
|
117
|
+
node.validation_secret_key = load_ed25519(config.get('validation_secret_key'))
|
|
118
|
+
|
|
119
|
+
# derive pubs + routes
|
|
120
|
+
node.relay_public_key = node.relay_secret_key.public_key()
|
|
121
|
+
node.validation_public_key = (
|
|
122
|
+
node.validation_secret_key.public_key().public_bytes(
|
|
123
|
+
encoding=serialization.Encoding.Raw,
|
|
124
|
+
format=serialization.PublicFormat.Raw,
|
|
125
|
+
)
|
|
126
|
+
if node.validation_secret_key
|
|
127
|
+
else None
|
|
128
|
+
)
|
|
129
|
+
node.peer_route, node.validation_route = make_routes(
|
|
130
|
+
node.relay_public_key,
|
|
131
|
+
node.validation_secret_key
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# sockets + queues + threads
|
|
135
|
+
incoming_port = config.get('incoming_port', 7373)
|
|
136
|
+
fam = socket.AF_INET6 if node.use_ipv6 else socket.AF_INET
|
|
137
|
+
node.incoming_socket = socket.socket(fam, socket.SOCK_DGRAM)
|
|
138
|
+
if node.use_ipv6:
|
|
139
|
+
node.incoming_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
140
|
+
node.incoming_socket.bind(("::" if node.use_ipv6 else "0.0.0.0", incoming_port or 0))
|
|
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
|
+
)
|
|
147
|
+
node.incoming_queue = Queue()
|
|
148
|
+
node.incoming_populate_thread = threading.Thread(
|
|
149
|
+
target=populate_incoming_messages,
|
|
150
|
+
args=(node,),
|
|
151
|
+
daemon=True,
|
|
152
|
+
)
|
|
153
|
+
node.incoming_process_thread = threading.Thread(
|
|
154
|
+
target=process_incoming_messages,
|
|
155
|
+
args=(node,),
|
|
156
|
+
daemon=True,
|
|
157
|
+
)
|
|
158
|
+
node.incoming_populate_thread.start()
|
|
159
|
+
node.incoming_process_thread.start()
|
|
160
|
+
|
|
161
|
+
(node.outgoing_socket,
|
|
162
|
+
node.outgoing_queue,
|
|
163
|
+
node.outgoing_thread
|
|
164
|
+
) = setup_outgoing(node.use_ipv6)
|
|
165
|
+
|
|
166
|
+
# other workers & maps
|
|
167
|
+
node.object_request_queue = Queue()
|
|
168
|
+
node.peer_manager_thread = threading.Thread(
|
|
169
|
+
target=node._relay_peer_manager,
|
|
170
|
+
daemon=True
|
|
171
|
+
)
|
|
172
|
+
node.peer_manager_thread.start()
|
|
173
|
+
|
|
174
|
+
node.peers, node.addresses = {}, {} # peers: Dict[bytes,Peer], addresses: Dict[(str,int),bytes]
|
|
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
|
|
183
|
+
else:
|
|
184
|
+
node.latest_block_hash = None
|
|
185
|
+
|
|
186
|
+
# bootstrap pings
|
|
187
|
+
bootstrap_peers = config.get('bootstrap', [])
|
|
188
|
+
for addr in bootstrap_peers:
|
|
189
|
+
try:
|
|
190
|
+
host, port = address_str_to_host_and_port(addr) # type: ignore[arg-type]
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
node.logger.warning("Invalid bootstrap address %s: %s", addr, exc)
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
handshake_message = Message(handshake=True, sender=node.relay_public_key)
|
|
196
|
+
|
|
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
|
+
]
|
|
@@ -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
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, List, Tuple
|
|
5
|
+
|
|
6
|
+
from ...storage.models.atom import Atom, ZERO32, AtomKind
|
|
7
|
+
from ...storage.models.trie import Trie
|
|
8
|
+
from ...utils.integer import bytes_to_int, int_to_bytes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Account:
|
|
13
|
+
balance: int
|
|
14
|
+
code_hash: bytes
|
|
15
|
+
counter: int
|
|
16
|
+
data_hash: bytes
|
|
17
|
+
data: Trie
|
|
18
|
+
atom_hash: bytes = ZERO32
|
|
19
|
+
atoms: List[Atom] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def create(cls, balance: int = 0, data_hash: bytes = ZERO32, code_hash: bytes = ZERO32, counter: int = 0) -> "Account":
|
|
23
|
+
account = cls(
|
|
24
|
+
balance=int(balance),
|
|
25
|
+
code_hash=bytes(code_hash),
|
|
26
|
+
counter=int(counter),
|
|
27
|
+
data_hash=bytes(data_hash),
|
|
28
|
+
data=Trie(root_hash=bytes(data_hash)),
|
|
29
|
+
)
|
|
30
|
+
atom_hash, atoms = account.to_atom()
|
|
31
|
+
account.atom_hash = atom_hash
|
|
32
|
+
account.atoms = atoms
|
|
33
|
+
return account
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_atom(cls, node: Any, root_id: bytes) -> "Account":
|
|
37
|
+
|
|
38
|
+
account_atoms = node.get_atom_list_from_storage(root_hash=root_id)
|
|
39
|
+
|
|
40
|
+
if account_atoms is None or len(account_atoms) != 5:
|
|
41
|
+
raise ValueError("malformed account atom list")
|
|
42
|
+
|
|
43
|
+
type_atom, balance_atom, code_atom, counter_atom, data_atom = account_atoms
|
|
44
|
+
|
|
45
|
+
if type_atom.data != b"account":
|
|
46
|
+
raise ValueError("not an account (type mismatch)")
|
|
47
|
+
|
|
48
|
+
account = cls.create(
|
|
49
|
+
balance=bytes_to_int(balance_atom.data),
|
|
50
|
+
data_hash=data_atom.data,
|
|
51
|
+
counter=bytes_to_int(counter_atom.data),
|
|
52
|
+
code_hash=code_atom.data,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return account
|
|
56
|
+
|
|
57
|
+
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
58
|
+
data_atom = Atom(
|
|
59
|
+
data=bytes(self.data_hash),
|
|
60
|
+
kind=AtomKind.LIST,
|
|
61
|
+
)
|
|
62
|
+
counter_atom = Atom(
|
|
63
|
+
data=int_to_bytes(self.counter),
|
|
64
|
+
next_id=data_atom.object_id(),
|
|
65
|
+
kind=AtomKind.BYTES,
|
|
66
|
+
)
|
|
67
|
+
code_atom = Atom(
|
|
68
|
+
data=bytes(self.code_hash),
|
|
69
|
+
next_id=counter_atom.object_id(),
|
|
70
|
+
kind=AtomKind.LIST,
|
|
71
|
+
)
|
|
72
|
+
balance_atom = Atom(
|
|
73
|
+
data=int_to_bytes(self.balance),
|
|
74
|
+
next_id=code_atom.object_id(),
|
|
75
|
+
kind=AtomKind.BYTES,
|
|
76
|
+
)
|
|
77
|
+
type_atom = Atom(
|
|
78
|
+
data=b"account",
|
|
79
|
+
next_id=balance_atom.object_id(),
|
|
80
|
+
kind=AtomKind.SYMBOL,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
atoms = [data_atom, counter_atom, code_atom, balance_atom, type_atom]
|
|
84
|
+
return type_atom.object_id(), list(atoms)
|