astreum 0.2.43__py3-none-any.whl → 0.2.45__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.
Potentially problematic release.
This version of astreum might be problematic. Click here for more details.
- astreum/_communication/message.py +1 -0
- astreum/_communication/peer.py +23 -11
- astreum/_communication/route.py +40 -3
- astreum/_communication/setup.py +72 -4
- astreum/_consensus/genesis.py +12 -81
- astreum/_consensus/setup.py +50 -3
- astreum/_consensus/workers/validation.py +5 -2
- astreum/_node.py +44 -10
- astreum/utils/bytes.py +24 -0
- {astreum-0.2.43.dist-info → astreum-0.2.45.dist-info}/METADATA +1 -1
- {astreum-0.2.43.dist-info → astreum-0.2.45.dist-info}/RECORD +14 -13
- {astreum-0.2.43.dist-info → astreum-0.2.45.dist-info}/WHEEL +0 -0
- {astreum-0.2.43.dist-info → astreum-0.2.45.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.43.dist-info → astreum-0.2.45.dist-info}/top_level.txt +0 -0
astreum/_communication/peer.py
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
)
|
astreum/_communication/route.py
CHANGED
|
@@ -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,7 +16,7 @@ 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
22
|
def _matching_leading_bits(a: bytes, b: bytes) -> int:
|
|
@@ -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
|
astreum/_communication/setup.py
CHANGED
|
@@ -95,9 +95,10 @@ def process_incoming_messages(node: "Node") -> None:
|
|
|
95
95
|
peer = Peer(node.relay_secret_key, sender_key)
|
|
96
96
|
except Exception:
|
|
97
97
|
continue
|
|
98
|
-
|
|
98
|
+
peer.address = address_key
|
|
99
|
+
|
|
99
100
|
node.peers[sender_public_key_bytes] = peer
|
|
100
|
-
node.peer_route.add_peer(sender_public_key_bytes)
|
|
101
|
+
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
101
102
|
|
|
102
103
|
response = Message(handshake=True, sender=node.relay_public_key)
|
|
103
104
|
node.outgoing_queue.put((response.to_bytes(), address_key))
|
|
@@ -105,17 +106,25 @@ def process_incoming_messages(node: "Node") -> None:
|
|
|
105
106
|
|
|
106
107
|
elif old_key_bytes == sender_public_key_bytes:
|
|
107
108
|
# existing mapping with same key -> nothing to change
|
|
108
|
-
|
|
109
|
+
peer = node.peers.get(sender_public_key_bytes)
|
|
110
|
+
if peer is not None:
|
|
111
|
+
peer.address = address_key
|
|
109
112
|
|
|
110
113
|
else:
|
|
111
114
|
# address reused with a different key -> replace peer
|
|
112
115
|
node.peers.pop(old_key_bytes, None)
|
|
116
|
+
try:
|
|
117
|
+
node.peer_route.remove_peer(old_key_bytes)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
113
120
|
try:
|
|
114
121
|
peer = Peer(node.relay_secret_key, sender_key)
|
|
115
122
|
except Exception:
|
|
116
123
|
continue
|
|
117
|
-
|
|
124
|
+
peer.address = address_key
|
|
125
|
+
|
|
118
126
|
node.peers[sender_public_key_bytes] = peer
|
|
127
|
+
node.peer_route.add_peer(sender_public_key_bytes, peer)
|
|
119
128
|
|
|
120
129
|
match message.topic:
|
|
121
130
|
case MessageTopic.PING:
|
|
@@ -164,6 +173,65 @@ def process_incoming_messages(node: "Node") -> None:
|
|
|
164
173
|
if node.validation_secret_key is None:
|
|
165
174
|
continue
|
|
166
175
|
node._validation_transaction_queue.put(message.content)
|
|
176
|
+
|
|
177
|
+
case MessageTopic.STORAGE_REQUEST:
|
|
178
|
+
payload = message.content
|
|
179
|
+
if len(payload) < 32:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
atom_id = payload[:32]
|
|
183
|
+
provider_bytes = payload[32:]
|
|
184
|
+
if not provider_bytes:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
provider_str = provider_bytes.decode("utf-8")
|
|
189
|
+
except UnicodeDecodeError:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
host, port = addr[0], int(addr[1])
|
|
194
|
+
except Exception:
|
|
195
|
+
continue
|
|
196
|
+
address_key = (host, port)
|
|
197
|
+
sender_key_bytes = node.addresses.get(address_key)
|
|
198
|
+
if sender_key_bytes is None:
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
local_key_bytes = node.relay_public_key.public_bytes(
|
|
203
|
+
encoding=serialization.Encoding.Raw,
|
|
204
|
+
format=serialization.PublicFormat.Raw,
|
|
205
|
+
)
|
|
206
|
+
except Exception:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
def xor_distance(target: bytes, key: bytes) -> int:
|
|
210
|
+
return int.from_bytes(
|
|
211
|
+
bytes(a ^ b for a, b in zip(target, key)),
|
|
212
|
+
byteorder="big",
|
|
213
|
+
signed=False,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
self_distance = xor_distance(atom_id, local_key_bytes)
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
closest_peer = node.peer_route.closest_peer_for_hash(atom_id)
|
|
220
|
+
except Exception:
|
|
221
|
+
closest_peer = None
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
closest_peer is not None
|
|
225
|
+
and closest_peer.public_key_bytes != sender_key_bytes
|
|
226
|
+
):
|
|
227
|
+
closest_distance = xor_distance(atom_id, closest_peer.public_key_bytes)
|
|
228
|
+
if closest_distance < self_distance:
|
|
229
|
+
target_addr = closest_peer.address
|
|
230
|
+
if target_addr is not None and target_addr != addr:
|
|
231
|
+
node.outgoing_queue.put((message.to_bytes(), target_addr))
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
node.storage_index[atom_id] = provider_str.strip()
|
|
167
235
|
|
|
168
236
|
case _:
|
|
169
237
|
continue
|
astreum/_consensus/genesis.py
CHANGED
|
@@ -1,77 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, List
|
|
5
5
|
|
|
6
6
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
7
7
|
|
|
8
8
|
from .account import Account
|
|
9
9
|
from .block import Block
|
|
10
|
-
from .._storage.atom import
|
|
11
|
-
from .._storage.patricia import PatriciaTrie
|
|
10
|
+
from .._storage.atom import ZERO32
|
|
11
|
+
from .._storage.patricia import PatriciaTrie
|
|
12
|
+
from ..utils.integer import int_to_bytes
|
|
12
13
|
|
|
13
14
|
TREASURY_ADDRESS = b"\x01" * 32
|
|
14
15
|
BURN_ADDRESS = b"\x00" * 32
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _int_to_be_bytes(value: int) -> bytes:
|
|
18
|
-
if value < 0:
|
|
19
|
-
raise ValueError("integer fields in genesis must be non-negative")
|
|
20
|
-
if value == 0:
|
|
21
|
-
return b"\x00"
|
|
22
|
-
length = (value.bit_length() + 7) // 8
|
|
23
|
-
return value.to_bytes(length, "big")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _make_list(child_ids: List[bytes]) -> Tuple[bytes, List[Atom]]:
|
|
27
|
-
next_hash = ZERO32
|
|
28
|
-
chain: List[Atom] = []
|
|
29
|
-
for child_id in reversed(child_ids):
|
|
30
|
-
elem = Atom.from_data(data=child_id, next_hash=next_hash)
|
|
31
|
-
next_hash = elem.object_id()
|
|
32
|
-
chain.append(elem)
|
|
33
|
-
chain.reverse()
|
|
34
|
-
|
|
35
|
-
value_atom = Atom.from_data(
|
|
36
|
-
data=len(child_ids).to_bytes(8, "little"),
|
|
37
|
-
next_hash=next_hash,
|
|
38
|
-
)
|
|
39
|
-
type_atom = Atom.from_data(data=b"list", next_hash=value_atom.object_id())
|
|
40
|
-
atoms = chain + [value_atom, type_atom]
|
|
41
|
-
return type_atom.object_id(), atoms
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _store_atoms(node: Any, atoms: Iterable[Atom]) -> None:
|
|
45
|
-
setter = getattr(node, "_local_set", None)
|
|
46
|
-
if not callable(setter):
|
|
47
|
-
raise TypeError("node must expose '_local_set(object_id, atom)'")
|
|
48
|
-
for atom in atoms:
|
|
49
|
-
setter(atom.object_id(), atom)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _persist_trie(trie: PatriciaTrie, node: Any) -> None:
|
|
53
|
-
for patricia_node in trie.nodes.values():
|
|
54
|
-
_, atoms = patricia_node.to_atoms()
|
|
55
|
-
_store_atoms(node, atoms)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if not hasattr(PatriciaNode, "to_bytes"):
|
|
59
|
-
def _patricia_node_to_bytes(self: PatriciaNode) -> bytes: # type: ignore[no-redef]
|
|
60
|
-
fields = [
|
|
61
|
-
bytes([self.key_len]) + self.key,
|
|
62
|
-
self.child_0 or ZERO32,
|
|
63
|
-
self.child_1 or ZERO32,
|
|
64
|
-
self.value or b"",
|
|
65
|
-
]
|
|
66
|
-
encoded: List[bytes] = []
|
|
67
|
-
for field in fields:
|
|
68
|
-
encoded.append(len(field).to_bytes(4, "big"))
|
|
69
|
-
encoded.append(field)
|
|
70
|
-
return b"".join(encoded)
|
|
71
|
-
|
|
72
|
-
PatriciaNode.to_bytes = _patricia_node_to_bytes # type: ignore[attr-defined]
|
|
73
|
-
|
|
74
|
-
|
|
75
16
|
def create_genesis_block(node: Any, validator_public_key: bytes, validator_secret_key: bytes) -> Block:
|
|
76
17
|
validator_pk = bytes(validator_public_key)
|
|
77
18
|
|
|
@@ -80,30 +21,21 @@ def create_genesis_block(node: Any, validator_public_key: bytes, validator_secre
|
|
|
80
21
|
|
|
81
22
|
# 1. Stake trie with single validator stake of 1 (encoded on 32 bytes).
|
|
82
23
|
stake_trie = PatriciaTrie()
|
|
83
|
-
stake_amount = (1)
|
|
84
|
-
stake_trie.put(node, validator_pk, stake_amount)
|
|
85
|
-
|
|
86
|
-
stake_root = stake_trie.root_hash or ZERO32
|
|
24
|
+
stake_amount = int_to_bytes(1)
|
|
25
|
+
stake_trie.put(storage_node=node, key=validator_pk, value=stake_amount)
|
|
26
|
+
stake_root = stake_trie.root_hash
|
|
87
27
|
|
|
88
28
|
# 2. Account trie with treasury, burn, and validator accounts.
|
|
89
29
|
accounts_trie = PatriciaTrie()
|
|
90
30
|
|
|
91
31
|
treasury_account = Account.create(balance=1, data=stake_root, counter=0)
|
|
92
|
-
|
|
93
|
-
_store_atoms(node, treasury_atoms)
|
|
94
|
-
accounts_trie.put(node, TREASURY_ADDRESS, treasury_account_id)
|
|
32
|
+
accounts_trie.put(storage_node=node, key=TREASURY_ADDRESS, value=treasury_account.hash)
|
|
95
33
|
|
|
96
34
|
burn_account = Account.create(balance=0, data=b"", counter=0)
|
|
97
|
-
|
|
98
|
-
_store_atoms(node, burn_atoms)
|
|
99
|
-
accounts_trie.put(node, BURN_ADDRESS, burn_account_id)
|
|
35
|
+
accounts_trie.put(storage_node=node, key=BURN_ADDRESS, value=burn_account.hash)
|
|
100
36
|
|
|
101
37
|
validator_account = Account.create(balance=0, data=b"", counter=0)
|
|
102
|
-
|
|
103
|
-
_store_atoms(node, validator_atoms)
|
|
104
|
-
accounts_trie.put(node, validator_pk, validator_account_id)
|
|
105
|
-
|
|
106
|
-
_persist_trie(accounts_trie, node)
|
|
38
|
+
accounts_trie.put(storage_node=node, key=validator_pk, value=validator_account.hash)
|
|
107
39
|
|
|
108
40
|
accounts_root = accounts_trie.root_hash
|
|
109
41
|
if accounts_root is None:
|
|
@@ -134,8 +66,7 @@ def create_genesis_block(node: Any, validator_public_key: bytes, validator_secre
|
|
|
134
66
|
|
|
135
67
|
secret = Ed25519PrivateKey.from_private_bytes(validator_secret_key)
|
|
136
68
|
block.signature = secret.sign(block.body_hash)
|
|
137
|
-
block_hash,
|
|
138
|
-
_store_atoms(node, block_atoms)
|
|
69
|
+
block_hash, _ = block.to_atom()
|
|
139
70
|
|
|
140
71
|
block.hash = block_hash
|
|
141
72
|
return block
|
astreum/_consensus/setup.py
CHANGED
|
@@ -2,13 +2,15 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import threading
|
|
4
4
|
from queue import Queue
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any, Optional
|
|
6
6
|
|
|
7
7
|
from .workers import (
|
|
8
8
|
make_discovery_worker,
|
|
9
9
|
make_validation_worker,
|
|
10
10
|
make_verify_worker,
|
|
11
11
|
)
|
|
12
|
+
from .genesis import create_genesis_block
|
|
13
|
+
from ..utils.bytes import hex_to_bytes
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def current_validator(node: Any) -> bytes:
|
|
@@ -16,7 +18,9 @@ def current_validator(node: Any) -> bytes:
|
|
|
16
18
|
raise NotImplementedError("current_validator must be implemented by the host node")
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
def consensus_setup(node: Any) -> None:
|
|
21
|
+
def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
|
|
22
|
+
config = config or {}
|
|
23
|
+
|
|
20
24
|
# Shared state
|
|
21
25
|
node.validation_lock = getattr(node, "validation_lock", threading.RLock())
|
|
22
26
|
|
|
@@ -26,6 +30,12 @@ def consensus_setup(node: Any) -> None:
|
|
|
26
30
|
node.chains = getattr(node, "chains", {})
|
|
27
31
|
node.forks = getattr(node, "forks", {})
|
|
28
32
|
|
|
33
|
+
node.latest_block_hash = None
|
|
34
|
+
latest_block_hex = config.get("latest_block_hash")
|
|
35
|
+
if latest_block_hex is not None:
|
|
36
|
+
node.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
|
|
37
|
+
node.latest_block = None
|
|
38
|
+
|
|
29
39
|
# Pending transactions queue (hash-only entries)
|
|
30
40
|
node._validation_transaction_queue = getattr(
|
|
31
41
|
node, "_validation_transaction_queue", Queue()
|
|
@@ -64,5 +74,42 @@ def consensus_setup(node: Any) -> None:
|
|
|
64
74
|
)
|
|
65
75
|
node.consensus_discovery_thread.start()
|
|
66
76
|
node.consensus_verify_thread.start()
|
|
67
|
-
|
|
77
|
+
|
|
78
|
+
validator_secret_hex = config.get("validation_secret_key")
|
|
79
|
+
if validator_secret_hex:
|
|
80
|
+
validator_secret_bytes = hex_to_bytes(validator_secret_hex, expected_length=32)
|
|
81
|
+
try:
|
|
82
|
+
from cryptography.hazmat.primitives import serialization
|
|
83
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
84
|
+
|
|
85
|
+
validator_private = ed25519.Ed25519PrivateKey.from_private_bytes(
|
|
86
|
+
validator_secret_bytes
|
|
87
|
+
)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
raise ValueError("invalid validation_secret_key") from exc
|
|
90
|
+
|
|
91
|
+
validator_public_bytes = validator_private.public_key().public_bytes(
|
|
92
|
+
encoding=serialization.Encoding.Raw,
|
|
93
|
+
format=serialization.PublicFormat.Raw,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
node.validation_secret_key = validator_private
|
|
97
|
+
node.validation_public_key = validator_public_bytes
|
|
98
|
+
|
|
99
|
+
if node.latest_block_hash is None:
|
|
100
|
+
genesis_block = create_genesis_block(
|
|
101
|
+
node,
|
|
102
|
+
validator_public_key=validator_public_bytes,
|
|
103
|
+
validator_secret_key=validator_secret_bytes,
|
|
104
|
+
)
|
|
105
|
+
genesis_hash, genesis_atoms = genesis_block.to_atom()
|
|
106
|
+
if hasattr(node, "_local_set"):
|
|
107
|
+
for atom in genesis_atoms:
|
|
108
|
+
try:
|
|
109
|
+
node._local_set(atom.object_id(), atom)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
node.latest_block_hash = genesis_hash
|
|
113
|
+
node.latest_block = genesis_block
|
|
114
|
+
|
|
68
115
|
node.consensus_validation_thread.start()
|
|
@@ -88,7 +88,7 @@ def make_validation_worker(
|
|
|
88
88
|
new_block.timestamp = max(int(now), min_allowed)
|
|
89
89
|
|
|
90
90
|
# atomize block
|
|
91
|
-
new_block_hash,
|
|
91
|
+
new_block_hash, new_block_atoms = new_block.to_atom()
|
|
92
92
|
# put as own latest block hash
|
|
93
93
|
node.latest_block_hash = new_block_hash
|
|
94
94
|
|
|
@@ -117,6 +117,9 @@ def make_validation_worker(
|
|
|
117
117
|
except Exception:
|
|
118
118
|
pass
|
|
119
119
|
|
|
120
|
-
#
|
|
120
|
+
# upload block atoms
|
|
121
|
+
|
|
122
|
+
# upload receipt atoms
|
|
123
|
+
# upload account atoms
|
|
121
124
|
|
|
122
125
|
return _validation_worker
|
astreum/_node.py
CHANGED
|
@@ -15,21 +15,24 @@ class Node:
|
|
|
15
15
|
# Storage Setup
|
|
16
16
|
self.in_memory_storage: Dict[bytes, Atom] = {}
|
|
17
17
|
self.in_memory_storage_lock = threading.RLock()
|
|
18
|
+
self.storage_index: Dict[bytes, str] = {}
|
|
18
19
|
# Lispeum Setup
|
|
19
20
|
self.environments: Dict[uuid.UUID, Env] = {}
|
|
20
21
|
self.machine_environments_lock = threading.RLock()
|
|
21
22
|
self.low_eval = low_eval
|
|
22
23
|
# Communication and Validation Setup (import lazily to avoid heavy deps during parsing tests)
|
|
23
|
-
try:
|
|
24
|
-
from astreum._communication import communication_setup # type: ignore
|
|
25
|
-
communication_setup(node=self, config=config)
|
|
26
|
-
except Exception:
|
|
27
|
-
pass
|
|
28
|
-
try:
|
|
29
|
-
from astreum._consensus import consensus_setup # type: ignore
|
|
30
|
-
consensus_setup(node=self)
|
|
31
|
-
except Exception:
|
|
32
|
-
pass
|
|
24
|
+
try:
|
|
25
|
+
from astreum._communication import communication_setup # type: ignore
|
|
26
|
+
communication_setup(node=self, config=config)
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
try:
|
|
30
|
+
from astreum._consensus import consensus_setup # type: ignore
|
|
31
|
+
consensus_setup(node=self, config=config)
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
33
36
|
|
|
34
37
|
# ---- Env helpers ----
|
|
35
38
|
def env_get(self, env_id: uuid.UUID, key: bytes) -> Optional[Expr]:
|
|
@@ -68,3 +71,34 @@ class Node:
|
|
|
68
71
|
if atom is not None:
|
|
69
72
|
return atom
|
|
70
73
|
return self._network_get(key)
|
|
74
|
+
|
|
75
|
+
def _network_set(self, atom: Atom) -> None:
|
|
76
|
+
"""Advertise an atom to the closest known peer so they can fetch it from us."""
|
|
77
|
+
try:
|
|
78
|
+
from src.astreum._communication.message import Message, MessageTopic
|
|
79
|
+
except Exception:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
atom_id = atom.object_id()
|
|
83
|
+
try:
|
|
84
|
+
closest_peer = self.peer_route.closest_peer_for_hash(atom_id)
|
|
85
|
+
except Exception:
|
|
86
|
+
return
|
|
87
|
+
if closest_peer is None or closest_peer.address is None:
|
|
88
|
+
return
|
|
89
|
+
target_addr = closest_peer.address
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
provider_ip, provider_port = self.incoming_socket.getsockname()[:2]
|
|
93
|
+
except Exception:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
provider_str = f"{provider_ip}:{int(provider_port)}"
|
|
97
|
+
try:
|
|
98
|
+
provider_bytes = provider_str.encode("utf-8")
|
|
99
|
+
except Exception:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
payload = atom_id + provider_bytes
|
|
103
|
+
message = Message(topic=MessageTopic.STORAGE_REQUEST, content=payload)
|
|
104
|
+
self.outgoing_queue.put((message.to_bytes(), target_addr))
|
astreum/utils/bytes.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def hex_to_bytes(value: str, *, expected_length: Optional[int] = None) -> bytes:
|
|
5
|
+
"""Convert a 0x-prefixed hex string into raw bytes."""
|
|
6
|
+
if not isinstance(value, str):
|
|
7
|
+
raise TypeError("hex value must be provided as a string")
|
|
8
|
+
|
|
9
|
+
if not value.startswith(("0x", "0X")):
|
|
10
|
+
raise ValueError("hex value must start with '0x'")
|
|
11
|
+
|
|
12
|
+
hex_digits = value[2:]
|
|
13
|
+
if len(hex_digits) % 2:
|
|
14
|
+
raise ValueError("hex value must have an even number of digits")
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
result = bytes.fromhex(hex_digits)
|
|
18
|
+
except ValueError as exc:
|
|
19
|
+
raise ValueError("hex value contains non-hexadecimal characters") from exc
|
|
20
|
+
|
|
21
|
+
if expected_length is not None and len(result) != expected_length:
|
|
22
|
+
raise ValueError(f"hex value must decode to exactly {expected_length} bytes")
|
|
23
|
+
|
|
24
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.45
|
|
4
4
|
Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
|
|
5
5
|
Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
|
|
6
6
|
Project-URL: Homepage, https://github.com/astreum/lib
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
astreum/__init__.py,sha256=9tzA27B_eG5wRF1SAWJIV7xTmCcR1QFc123b_cvFOa4,345
|
|
2
|
-
astreum/_node.py,sha256=
|
|
2
|
+
astreum/_node.py,sha256=KC_TSaOq7zmOE8baS4gNuoQAI8NxEMsz5rlH29nLN20,3819
|
|
3
3
|
astreum/format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
|
|
4
4
|
astreum/node.py,sha256=MmlK3jaANTMB3ZAxR8IaSc82OS9meJmVawYIVURADbg,39689
|
|
5
5
|
astreum/_communication/__init__.py,sha256=XJui0yOcfAur4HKt-8sSRlwB-MSU1rchkuOAY-nKDOE,207
|
|
6
|
-
astreum/_communication/message.py,sha256=
|
|
7
|
-
astreum/_communication/peer.py,sha256=
|
|
6
|
+
astreum/_communication/message.py,sha256=Wl1IITj7eY9_q0IOT4J7c5gsjS1bF51CH7GcSSuu5OM,3327
|
|
7
|
+
astreum/_communication/peer.py,sha256=CbqkyCwhFCiC2spd1-KjNdeVGNjjt2ECVs8uHot-ETI,875
|
|
8
8
|
astreum/_communication/ping.py,sha256=u_DQTZJsbMdYiDDqjdZDsLaN5na2m9WZjVeEM3zq9_Y,955
|
|
9
|
-
astreum/_communication/route.py,sha256=
|
|
10
|
-
astreum/_communication/setup.py,sha256=
|
|
9
|
+
astreum/_communication/route.py,sha256=Dqw8utefBeesZ311Nyizb7PO-g5dl26AoIUavttojHA,4037
|
|
10
|
+
astreum/_communication/setup.py,sha256=EN2GyVfGlVtkSHMhZWwJN33y-Ycfdvq9tjr38Myrmqg,11739
|
|
11
11
|
astreum/_communication/util.py,sha256=bJ3td3naDzmCelAJQpLwiDMoRBkijQl9YLROjsWyOrI,1256
|
|
12
12
|
astreum/_consensus/__init__.py,sha256=gOCpvnIeO17CGjUGr0odaKNvGEggmDRXfT5IuyrYtcM,376
|
|
13
13
|
astreum/_consensus/account.py,sha256=ClMB1e07ky5Wf7BwR4yhEjTSaWK2wiCmpvzio9AsZAY,3295
|
|
@@ -15,13 +15,13 @@ astreum/_consensus/accounts.py,sha256=zGq2BCMJPtD_lzcj4jqMzB2Foc3ptxXSPhc9zypB1T
|
|
|
15
15
|
astreum/_consensus/block.py,sha256=HybdVcV1D9f1wUE9XeFRnCGF9HH8YI1bgo-y1h-RS5o,12342
|
|
16
16
|
astreum/_consensus/chain.py,sha256=WwUeLrdg7uj--ZsUxse6xFlzO2QeQQggyKSL49KhfU0,2768
|
|
17
17
|
astreum/_consensus/fork.py,sha256=dK5DtT9hWCj_rQs6MS1c1bcGBbkgVTBPIOudYbqS9vw,3825
|
|
18
|
-
astreum/_consensus/genesis.py,sha256=
|
|
18
|
+
astreum/_consensus/genesis.py,sha256=YOzEEcpH5lQUdRNz1P8cfeddh-IJzjTbOiVKrEPHXPg,2506
|
|
19
19
|
astreum/_consensus/receipt.py,sha256=GPLspqVJHnyBr1cKmBoClsJyeEUecYmg3_acz4L4rRo,6031
|
|
20
|
-
astreum/_consensus/setup.py,sha256=
|
|
20
|
+
astreum/_consensus/setup.py,sha256=KHW2KzHRgCMLWaJNV2utosKdk7nxjI5aF9ie7jctgcU,4257
|
|
21
21
|
astreum/_consensus/transaction.py,sha256=q9r0nQzB_67dV-9cIThCzpiJkTojP--8QENPOKxN5yw,7481
|
|
22
22
|
astreum/_consensus/workers/__init__.py,sha256=bS5FjbevbIR5FHbVGnT4Jli17VIld_5auemRw4CaHFU,278
|
|
23
23
|
astreum/_consensus/workers/discovery.py,sha256=X1yjKGjLSApMJ9mgWbnc7N21ALD09khDf-in-M45Mis,1683
|
|
24
|
-
astreum/_consensus/workers/validation.py,sha256=
|
|
24
|
+
astreum/_consensus/workers/validation.py,sha256=ftlwTDocwrvCk_BAicrOJgm3deZHgwjN_gn5r-gWE_0,4900
|
|
25
25
|
astreum/_consensus/workers/verify.py,sha256=wNkfh5qMARew79B-yyGL2UiExaSoGpOIq0fXUkDzpdw,1925
|
|
26
26
|
astreum/_lispeum/__init__.py,sha256=LAy2Z-gBBQlByBHRGUKaaQrOB7QzFEEyGRtInmwTpfU,304
|
|
27
27
|
astreum/_lispeum/environment.py,sha256=pJ0rjp9GoQxHhDiPIVei0jP7dZ_Pznso2O_tpp94-Ik,328
|
|
@@ -46,9 +46,10 @@ astreum/models/patricia.py,sha256=ohmXrcaz7Ae561tyC4u4iPOkQPkKr8N0IWJek4upFIg,13
|
|
|
46
46
|
astreum/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
47
|
astreum/storage/object.py,sha256=knFlvw_tpcC4twSu1DGNpHX31wlANN8E5dgEqIfU--Q,2041
|
|
48
48
|
astreum/storage/setup.py,sha256=1-9ztEFI_BvRDvAA0lAn4mFya8iq65THTArlj--M3Hg,626
|
|
49
|
+
astreum/utils/bytes.py,sha256=9QTWC2JCdwWLB5R2mPtmjPro0IUzE58DL3uEul4AheE,846
|
|
49
50
|
astreum/utils/integer.py,sha256=iQt-klWOYVghu_NOT341MmHbOle4FDT3by4PNKNXscg,736
|
|
50
|
-
astreum-0.2.
|
|
51
|
-
astreum-0.2.
|
|
52
|
-
astreum-0.2.
|
|
53
|
-
astreum-0.2.
|
|
54
|
-
astreum-0.2.
|
|
51
|
+
astreum-0.2.45.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
|
|
52
|
+
astreum-0.2.45.dist-info/METADATA,sha256=poHQQVqPXQLxXZ-j_4EAtw5R6QA2s_Ps9URrpFvmWDg,6181
|
|
53
|
+
astreum-0.2.45.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
54
|
+
astreum-0.2.45.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
|
|
55
|
+
astreum-0.2.45.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|