astreum 0.3.9__py3-none-any.whl → 0.3.16__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 CHANGED
@@ -1,6 +1,6 @@
1
-
1
+
2
2
  from astreum.consensus import Account, Accounts, Block, Chain, Fork, Receipt, Transaction
3
- from astreum.machine import Env, Expr
3
+ from astreum.machine import Env, Expr, parse, tokenize
4
4
  from astreum.node import Node
5
5
 
6
6
 
@@ -15,4 +15,6 @@ __all__: list[str] = [
15
15
  "Transaction",
16
16
  "Account",
17
17
  "Accounts",
18
+ "parse",
19
+ "tokenize",
18
20
  ]
@@ -31,45 +31,11 @@ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) ->
31
31
  return True
32
32
  peer_address = (host, port)
33
33
 
34
- old_key_bytes = node.addresses.get(peer_address)
35
- node.addresses[peer_address] = sender_public_key_bytes
36
-
37
- if old_key_bytes is None:
38
- try:
39
- peer = Peer(
40
- node_secret_key=node.relay_secret_key,
41
- peer_public_key=sender_key,
42
- address=peer_address,
43
- )
44
- except Exception:
45
- return True
46
-
47
- node.add_peer(sender_public_key_bytes, peer)
48
- node.peer_route.add_peer(sender_public_key_bytes, peer)
49
-
50
- node.logger.info(
51
- "Handshake accepted from %s:%s; peer added",
52
- peer_address[0],
53
- peer_address[1],
54
- )
55
- response = Message(
56
- handshake=True,
57
- sender=node.relay_public_key,
58
- content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
59
- )
60
- node.outgoing_queue.put((response.to_bytes(), peer_address))
61
- return True
62
-
63
- if old_key_bytes == sender_public_key_bytes:
64
- peer = node.get_peer(sender_public_key_bytes)
65
- if peer is not None:
66
- peer.address = peer_address
34
+ existing_peer = node.get_peer(sender_public_key_bytes)
35
+ if existing_peer is not None:
36
+ existing_peer.address = peer_address
67
37
  return False
68
38
 
69
- try:
70
- node.peer_route.remove_peer(old_key_bytes)
71
- except Exception:
72
- pass
73
39
  try:
74
40
  peer = Peer(
75
41
  node_secret_key=node.relay_secret_key,
@@ -79,11 +45,18 @@ def handle_handshake(node: "Node", addr: Sequence[object], message: Message) ->
79
45
  except Exception:
80
46
  return True
81
47
 
82
- node.replace_peer(old_key_bytes, sender_public_key_bytes, peer)
48
+ node.add_peer(sender_public_key_bytes, peer)
83
49
  node.peer_route.add_peer(sender_public_key_bytes, peer)
50
+
84
51
  node.logger.info(
85
- "Peer at %s:%s replaced due to key change",
52
+ "Handshake accepted from %s:%s; peer added",
86
53
  peer_address[0],
87
54
  peer_address[1],
88
55
  )
89
- return False
56
+ response = Message(
57
+ handshake=True,
58
+ sender=node.relay_public_key,
59
+ content=int(node.config["incoming_port"]).to_bytes(2, "big", signed=False),
60
+ )
61
+ node.outgoing_queue.put((response.to_bytes(), peer_address))
62
+ return True
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from datetime import datetime, timedelta, timezone
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from .. import Node
9
+
10
+
11
+ def manage_peer(node: "Node") -> None:
12
+ """Continuously evict peers whose timestamps exceed the configured timeout."""
13
+ node.logger.info(
14
+ "Peer manager started (timeout=%3ds, interval=%3ds)",
15
+ node.config["peer_timeout"],
16
+ node.config["peer_timeout_interval"],
17
+ )
18
+ while True:
19
+ timeout_seconds = node.config["peer_timeout"]
20
+ interval_seconds = node.config["peer_timeout_interval"]
21
+ try:
22
+ peers = getattr(node, "peers", None)
23
+ peer_route = getattr(node, "peer_route", None)
24
+ if not isinstance(peers, dict) or peer_route is None:
25
+ time.sleep(interval_seconds)
26
+ continue
27
+
28
+ cutoff = datetime.now(timezone.utc) - timedelta(seconds=timeout_seconds)
29
+ stale_keys = []
30
+ with node.peers_lock:
31
+ for peer_key, peer in list(peers.items()):
32
+ if peer.timestamp < cutoff:
33
+ stale_keys.append(peer_key)
34
+
35
+ removed_count = 0
36
+ for peer_key in stale_keys:
37
+ removed = node.remove_peer(peer_key)
38
+ if removed is None:
39
+ continue
40
+ removed_count += 1
41
+ try:
42
+ peer_route.remove_peer(peer_key)
43
+ except Exception:
44
+ node.logger.debug(
45
+ "Unable to remove peer %s from route",
46
+ peer_key.hex(),
47
+ )
48
+ node.logger.debug(
49
+ "Evicted stale peer %s last seen at %s",
50
+ peer_key.hex(),
51
+ getattr(removed, "timestamp", None),
52
+ )
53
+
54
+ if removed_count:
55
+ node.logger.info("Peer manager removed %s stale peer(s)", removed_count)
56
+ except Exception:
57
+ node.logger.exception("Peer manager iteration failed")
58
+
59
+ time.sleep(interval_seconds)
@@ -18,6 +18,7 @@ from .processors.incoming import (
18
18
  populate_incoming_messages,
19
19
  )
20
20
  from .processors.outgoing import process_outgoing_messages
21
+ from .processors.peer import manage_peer
21
22
  from .util import address_str_to_host_and_port
22
23
  from ..utils.bytes import hex_to_bytes
23
24
 
@@ -122,13 +123,14 @@ def communication_setup(node: "Node", config: dict):
122
123
  # other workers & maps
123
124
  # track atom requests we initiated; guarded by atom_requests_lock on the node
124
125
  node.peer_manager_thread = threading.Thread(
125
- target=node._relay_peer_manager,
126
+ target=manage_peer,
127
+ args=(node,),
126
128
  daemon=True
127
129
  )
128
130
  node.peer_manager_thread.start()
129
131
 
130
132
  with node.peers_lock:
131
- node.peers, node.addresses = {}, {} # peers: Dict[bytes,Peer], addresses: Dict[(str,int),bytes]
133
+ node.peers = {} # Dict[bytes,Peer]
132
134
 
133
135
  latest_block_hex = config.get("latest_block_hash")
134
136
  if latest_block_hex:
@@ -11,15 +11,19 @@ from ..storage.models.atom import ZERO32
11
11
  from ..utils.integer import bytes_to_int, int_to_bytes
12
12
 
13
13
 
14
+ SLOT_DURATION_SECONDS = 2
15
+
16
+
14
17
  def current_validator(
15
18
  node: Any,
16
19
  block_hash: bytes,
17
20
  target_time: Optional[int] = None,
18
21
  ) -> Tuple[bytes, Accounts]:
19
22
  """
20
- Determine the validator for the requested target_time, halving stakes each second
21
- between the referenced block and the target time. Returns the validator key and
22
- the updated accounts snapshot reflecting stake and balance adjustments.
23
+ Determine the validator for the requested target_time, halving stakes once per
24
+ slot (currently 2 seconds) between the referenced block and the target time.
25
+ Returns the validator key and the updated accounts snapshot reflecting stake and
26
+ balance adjustments.
23
27
  """
24
28
 
25
29
  block = Block.from_atom(node, block_hash)
@@ -74,6 +78,8 @@ def current_validator(
74
78
  if current_amount <= 0:
75
79
  raise ValueError("validator stake must be positive")
76
80
  new_amount = current_amount // 2
81
+ if new_amount < 1:
82
+ new_amount = 1
77
83
  returned_amount = current_amount - new_amount
78
84
  stakes[validator_key] = new_amount
79
85
  stake_trie.put(node, validator_key, int_to_bytes(new_amount))
@@ -86,10 +92,13 @@ def current_validator(
86
92
  accounts.set_account(validator_key, validator_account)
87
93
  accounts.set_account(TREASURY_ADDRESS, treasury_account)
88
94
 
89
- iteration_target = block_timestamp + 1
90
- while True:
95
+ delta = target_timestamp - block_timestamp
96
+ slots_to_process = max(1, (delta + SLOT_DURATION_SECONDS - 1) // SLOT_DURATION_SECONDS)
97
+
98
+ selected_validator = pick_validator()
99
+ halve_stake(selected_validator)
100
+ for _ in range(1, slots_to_process):
91
101
  selected_validator = pick_validator()
92
102
  halve_stake(selected_validator)
93
- if iteration_target == target_timestamp:
94
- return selected_validator, accounts
95
- iteration_target += 1
103
+
104
+ return selected_validator, accounts
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import math
3
4
  import time
4
5
  from queue import Empty
5
6
  from typing import Any, Callable
@@ -203,7 +204,9 @@ def make_validation_worker(
203
204
 
204
205
  now = time.time()
205
206
  min_allowed = new_block.previous_block.timestamp + 1
206
- new_block.timestamp = max(int(now), min_allowed)
207
+ nonce_time_seconds = node.nonce_time_ms / 1000.0
208
+ expected_blocktime = now + nonce_time_seconds
209
+ new_block.timestamp = max(int(math.ceil(expected_blocktime)), min_allowed)
207
210
 
208
211
  new_block.delay_difficulty = Block.calculate_delay_difficulty(
209
212
  previous_timestamp=previous_block.timestamp,
@@ -212,7 +215,10 @@ def make_validation_worker(
212
215
  )
213
216
 
214
217
  try:
218
+ nonce_started = time.perf_counter()
215
219
  new_block.generate_nonce(difficulty=previous_block.delay_difficulty)
220
+ elapsed_ms = int((time.perf_counter() - nonce_started) * 1000)
221
+ setattr(node, "nonce_time_ms", elapsed_ms)
216
222
  node.logger.debug(
217
223
  "Found nonce %s for block #%s at difficulty %s",
218
224
  new_block.nonce,
@@ -223,18 +229,39 @@ def make_validation_worker(
223
229
  node.logger.exception("Failed while searching for block nonce")
224
230
  time.sleep(0.5)
225
231
  continue
232
+
233
+ # wait until the block timestamp is reached before propagating
234
+ now = time.time()
235
+ if now > (new_block.timestamp + 2):
236
+ node.logger.warning(
237
+ "Skipping block #%s propagation; timestamp %s already elapsed (now=%s)",
238
+ new_block.number,
239
+ new_block.timestamp,
240
+ now,
241
+ )
242
+ continue
226
243
 
244
+ spread_delay = new_block.timestamp - now
245
+ if spread_delay > 0:
246
+ node.logger.debug(
247
+ "Delaying distribution for %.3fs to reach block timestamp %s",
248
+ spread_delay,
249
+ new_block.timestamp,
250
+ )
251
+ time.sleep(spread_delay)
252
+
227
253
  # atomize block
228
254
  new_block_hash, new_block_atoms = new_block.to_atom()
229
255
  # put as own latest block hash
230
256
  node.latest_block_hash = new_block_hash
231
257
  node.latest_block = new_block
232
258
  node.logger.info(
233
- "Validated block #%s with hash %s (%d atoms)",
259
+ "Created block #%s with hash %s (%d atoms)",
234
260
  new_block.number,
235
261
  new_block_hash.hex(),
236
262
  len(new_block_atoms),
237
263
  )
264
+
238
265
 
239
266
  # ping peers in the validation route to update their records
240
267
  if node.validation_route and node.outgoing_queue and node.peers:
astreum/node.py CHANGED
@@ -54,7 +54,10 @@ class Node:
54
54
  self.environments: Dict[uuid.UUID, Env] = {}
55
55
  self.machine_environments_lock = threading.RLock()
56
56
  self.is_connected = False
57
-
57
+ self.latest_block_hash = None
58
+ self.latest_block = None
59
+ self.nonce_time_ms = 0 # rolling measurement of last nonce search duration
60
+
58
61
  connect = connect_to_network_and_verify
59
62
  validate = process_blocks_and_transactions
60
63
 
astreum/utils/config.py CHANGED
@@ -1,9 +1,11 @@
1
-
1
+
2
2
  from pathlib import Path
3
3
  from typing import Dict
4
4
 
5
5
  DEFAULT_HOT_STORAGE_LIMIT = 1 << 30 # 1 GiB
6
6
  DEFAULT_COLD_STORAGE_LIMIT = 10 << 30 # 10 GiB
7
+ DEFAULT_PEER_TIMEOUT_SECONDS = 15 * 60 # 15 minutes
8
+ DEFAULT_PEER_TIMEOUT_INTERVAL_SECONDS = 10 # 10 seconds
7
9
 
8
10
 
9
11
  def config_setup(config: Dict = {}):
@@ -45,4 +47,30 @@ def config_setup(config: Dict = {}):
45
47
  else:
46
48
  config["cold_storage_path"] = None
47
49
 
50
+ peer_timeout_raw = config.get("peer_timeout", DEFAULT_PEER_TIMEOUT_SECONDS)
51
+ try:
52
+ peer_timeout = int(peer_timeout_raw)
53
+ except (TypeError, ValueError) as exc:
54
+ raise ValueError(
55
+ f"peer_timeout must be an integer: {peer_timeout_raw!r}"
56
+ ) from exc
57
+
58
+ if peer_timeout <= 0:
59
+ raise ValueError("peer_timeout must be a positive integer")
60
+
61
+ config["peer_timeout"] = peer_timeout
62
+
63
+ interval_raw = config.get("peer_timeout_interval", DEFAULT_PEER_TIMEOUT_INTERVAL_SECONDS)
64
+ try:
65
+ interval = int(interval_raw)
66
+ except (TypeError, ValueError) as exc:
67
+ raise ValueError(
68
+ f"peer_timeout_interval must be an integer: {interval_raw!r}"
69
+ ) from exc
70
+
71
+ if interval <= 0:
72
+ raise ValueError("peer_timeout_interval must be a positive integer")
73
+
74
+ config["peer_timeout_interval"] = interval
75
+
48
76
  return config
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.3.9
3
+ Version: 0.3.16
4
4
  Summary: Python library to interact with the Astreum blockchain and its virtual machine.
5
5
  Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
6
6
  Project-URL: Homepage, https://github.com/astreum/lib-py
@@ -1,11 +1,11 @@
1
- astreum/__init__.py,sha256=GkEW_ReYore8_0nEOvPnZLUa3lO7CgMWu6LeEjrGXEk,325
2
- astreum/node.py,sha256=naKGkn97M3Khux3r8mZ81wsiAI6JFr4uhly4X_BspmU,2689
1
+ astreum/__init__.py,sha256=ohPOPq9IdKln63LvbLR6HwWjMnvInelwlW-FXRFXa2M,370
2
+ astreum/node.py,sha256=Rl4SdsA5olkgY33Q_d8XpKQfECKvqfTN_Y2FkCx6uE0,2852
3
3
  astreum/communication/__init__.py,sha256=wNxzsAk8Fol9cGMPuVvY4etrrMqn3SjZq1dE82kFrxw,228
4
- astreum/communication/setup.py,sha256=We43HOZG0v18TZhEdkc8MWCvI9YvHw8s4fmauHEXDUc,5887
4
+ astreum/communication/setup.py,sha256=tDf4koYu04u_iOCeuvYyTaJaxQm6-yIGkedXCoWjT8w,5876
5
5
  astreum/communication/start.py,sha256=wxL1cgChebhnaeEaY9flS6qybo_cFW2-tcRvjLxC8Hw,1823
6
6
  astreum/communication/util.py,sha256=fS3u3giOOXmvN0_reb0oIaXsFESHAWz-bbAuzdzdGQ4,1575
7
7
  astreum/communication/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- astreum/communication/handlers/handshake.py,sha256=79Y5ei8RHg53WIwzP1eO4dpDYdKvhr1qrbWMECf9D-E,2781
8
+ astreum/communication/handlers/handshake.py,sha256=LZLW06tufhiLAW0k1yC8n6wvTPT5VaUAvncHl8fm5n8,1910
9
9
  astreum/communication/handlers/object_request.py,sha256=n_ThJomdYXm6HPfkLMmCqRxO58xPCUOHXf1ddmG-OC8,7163
10
10
  astreum/communication/handlers/object_response.py,sha256=X5MfYxd_b4SfKq3129Fi29ZfLynWNRyhGuoISiMHy20,3959
11
11
  astreum/communication/handlers/ping.py,sha256=2fVynfVIsbWHtf7lpM6fTYWmeG0I1WSU3tmgCh9di7A,916
@@ -19,11 +19,12 @@ astreum/communication/models/route.py,sha256=NdmnI1J1wFs2pkm6W0Kv-29JeqGiHcKlw4-
19
19
  astreum/communication/processors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  astreum/communication/processors/incoming.py,sha256=10l6Az-Ul_2BvYpTXgPiL9bL0te1q3GB3aET0JednKE,3325
21
21
  astreum/communication/processors/outgoing.py,sha256=09nAeTzvo3jGWl3SLgIQr8vfmO_IvdDqSG7_ZKThYPk,593
22
+ astreum/communication/processors/peer.py,sha256=1P_-F1stJtXCPI3vlDtLcsQpQRTQQl1dNyJBDeFRGP4,2129
22
23
  astreum/consensus/__init__.py,sha256=VZR_NyGSD5VvZp3toD2zpdYwFDLBIcckeVZXFPlruuU,425
23
24
  astreum/consensus/genesis.py,sha256=RI9AzQFmDTgNFuiiTmW2dDiGcURIUGmThdRpxWrUOBk,1962
24
25
  astreum/consensus/setup.py,sha256=lrEapfpJXKqw4iwST11-tqPAI2VW2h3H6Ue4JDAtrP4,3142
25
26
  astreum/consensus/start.py,sha256=DM45Pw6JL5rew-KpcspINguH43ZUHr4v99tXjYqaEkE,2551
26
- astreum/consensus/validator.py,sha256=cqcmw1WEB8DkznNX_Mn8tmE956rVSNCPv1FicdL8EAQ,3647
27
+ astreum/consensus/validator.py,sha256=Jj9_ndZ358yAqVzQpLyjr4lkBjdCscAYoqAi-Ja3Qoo,3871
27
28
  astreum/consensus/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
29
  astreum/consensus/models/account.py,sha256=3QcT59QUZynysLSbiywidFYVzYJ3LR6qia7JwXOwn4I,2690
29
30
  astreum/consensus/models/accounts.py,sha256=iUMs6LvmMea-gxd6-ujkFjqhWmuW1cl9XTWGXQkpLys,2388
@@ -34,7 +35,7 @@ astreum/consensus/models/receipt.py,sha256=KjKKjYp_LnP2zkX1FLIwD_4hqKV1b2TPfp43t
34
35
  astreum/consensus/models/transaction.py,sha256=AYa1Q-BaYW3mkOv1e3WbvDFEsYamKMiFrja-eO2zU_Y,7475
35
36
  astreum/consensus/workers/__init__.py,sha256=bS5FjbevbIR5FHbVGnT4Jli17VIld_5auemRw4CaHFU,278
36
37
  astreum/consensus/workers/discovery.py,sha256=u6HyxamMVJjYnPFPa_U95I2pN9UzHRQ-LOa7YYZT808,2453
37
- astreum/consensus/workers/validation.py,sha256=uBkZVduHtwz8hUwDf_gFDiY8TfFaoYj40jx-s-EzuEU,13358
38
+ astreum/consensus/workers/validation.py,sha256=geHcKaTUIbCnYvAHj9xTbiSMQTpvH6kJ4O5b5sCFUwk,14540
38
39
  astreum/consensus/workers/verify.py,sha256=tBBrAHH8Xcg3uopmQSjT6iuZd1s-9FkLnJ_JgeW5HdU,3423
39
40
  astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
41
  astreum/crypto/chacha20poly1305.py,sha256=01VtLx_bdJC86ifQeTA494ZdKbPM2MswDTLmAs9bl8c,2479
@@ -61,11 +62,11 @@ astreum/storage/actions/set.py,sha256=8MvlZS3MFvLc-apDb6mucxt1JBxw82lxMVoa0sTvdo
61
62
  astreum/storage/models/atom.py,sha256=FY_bgtoju59Yo7TL1DTFTr9_pRMNBuH6-u59D6bz2fc,3163
62
63
  astreum/storage/models/trie.py,sha256=Bn3ssPGI7YGS4iUH5ESvpG1NE6Ljx2Xo7wkEpQhjKUY,17587
63
64
  astreum/utils/bytes.py,sha256=9QTWC2JCdwWLB5R2mPtmjPro0IUzE58DL3uEul4AheE,846
64
- astreum/utils/config.py,sha256=jiobdNFiF44BMmoifAG3feq57ZPIhjUG4FG71cn-kgY,1527
65
+ astreum/utils/config.py,sha256=MASHeLYzaPHG8Z6vLUd14vRH9JByfAL05tvgZWKrhNM,2517
65
66
  astreum/utils/integer.py,sha256=iQt-klWOYVghu_NOT341MmHbOle4FDT3by4PNKNXscg,736
66
67
  astreum/utils/logging.py,sha256=mRDtWSCj8vKt58WGKLNSkK9Oa0graNVSoS8URby4Q9g,6684
67
- astreum-0.3.9.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
68
- astreum-0.3.9.dist-info/METADATA,sha256=vlvV1wwS4GiKwUQhpEg3FqJ_I_5m4efioshBzwhs4bk,7766
69
- astreum-0.3.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
- astreum-0.3.9.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
71
- astreum-0.3.9.dist-info/RECORD,,
68
+ astreum-0.3.16.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
69
+ astreum-0.3.16.dist-info/METADATA,sha256=X6_Ea1a8-C-7At3YvK1OjeUsuqh0zmE1SoeUjJaB7qk,7767
70
+ astreum-0.3.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
71
+ astreum-0.3.16.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
72
+ astreum-0.3.16.dist-info/RECORD,,