astreum 0.2.61__py3-none-any.whl → 0.3.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. astreum/__init__.py +16 -7
  2. astreum/{_communication → communication}/__init__.py +3 -3
  3. astreum/communication/handlers/handshake.py +89 -0
  4. astreum/communication/handlers/object_request.py +176 -0
  5. astreum/communication/handlers/object_response.py +115 -0
  6. astreum/communication/handlers/ping.py +34 -0
  7. astreum/communication/handlers/route_request.py +76 -0
  8. astreum/communication/handlers/route_response.py +53 -0
  9. astreum/communication/models/__init__.py +0 -0
  10. astreum/communication/models/message.py +124 -0
  11. astreum/communication/models/peer.py +51 -0
  12. astreum/{_communication → communication/models}/route.py +7 -12
  13. astreum/communication/processors/__init__.py +0 -0
  14. astreum/communication/processors/incoming.py +98 -0
  15. astreum/communication/processors/outgoing.py +20 -0
  16. astreum/communication/setup.py +166 -0
  17. astreum/communication/start.py +37 -0
  18. astreum/{_communication → communication}/util.py +7 -0
  19. astreum/consensus/__init__.py +20 -0
  20. astreum/consensus/genesis.py +66 -0
  21. astreum/consensus/models/__init__.py +0 -0
  22. astreum/consensus/models/account.py +84 -0
  23. astreum/consensus/models/accounts.py +72 -0
  24. astreum/consensus/models/block.py +364 -0
  25. astreum/{_consensus → consensus/models}/chain.py +7 -7
  26. astreum/{_consensus → consensus/models}/fork.py +8 -8
  27. astreum/consensus/models/receipt.py +98 -0
  28. astreum/{_consensus → consensus/models}/transaction.py +76 -78
  29. astreum/{_consensus → consensus}/setup.py +18 -50
  30. astreum/consensus/start.py +67 -0
  31. astreum/consensus/validator.py +95 -0
  32. astreum/{_consensus → consensus}/workers/discovery.py +19 -1
  33. astreum/consensus/workers/validation.py +307 -0
  34. astreum/{_consensus → consensus}/workers/verify.py +29 -2
  35. astreum/crypto/chacha20poly1305.py +74 -0
  36. astreum/machine/__init__.py +20 -0
  37. astreum/machine/evaluations/__init__.py +0 -0
  38. astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
  39. astreum/machine/evaluations/low_evaluation.py +281 -0
  40. astreum/machine/evaluations/script_evaluation.py +27 -0
  41. astreum/machine/models/__init__.py +0 -0
  42. astreum/machine/models/environment.py +31 -0
  43. astreum/{_lispeum → machine/models}/expression.py +36 -8
  44. astreum/machine/tokenizer.py +90 -0
  45. astreum/node.py +78 -767
  46. astreum/storage/__init__.py +7 -0
  47. astreum/storage/actions/get.py +183 -0
  48. astreum/storage/actions/set.py +178 -0
  49. astreum/{_storage → storage/models}/atom.py +55 -57
  50. astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
  51. astreum/storage/requests.py +28 -0
  52. astreum/storage/setup.py +22 -15
  53. astreum/utils/config.py +48 -0
  54. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
  55. astreum-0.3.9.dist-info/RECORD +71 -0
  56. astreum/_communication/message.py +0 -101
  57. astreum/_communication/peer.py +0 -23
  58. astreum/_communication/setup.py +0 -322
  59. astreum/_consensus/__init__.py +0 -20
  60. astreum/_consensus/account.py +0 -95
  61. astreum/_consensus/accounts.py +0 -38
  62. astreum/_consensus/block.py +0 -311
  63. astreum/_consensus/genesis.py +0 -72
  64. astreum/_consensus/receipt.py +0 -136
  65. astreum/_consensus/workers/validation.py +0 -125
  66. astreum/_lispeum/__init__.py +0 -16
  67. astreum/_lispeum/environment.py +0 -13
  68. astreum/_lispeum/low_evaluation.py +0 -123
  69. astreum/_lispeum/tokenizer.py +0 -22
  70. astreum/_node.py +0 -198
  71. astreum/_storage/__init__.py +0 -7
  72. astreum/_storage/setup.py +0 -35
  73. astreum/format.py +0 -75
  74. astreum/models/block.py +0 -441
  75. astreum/models/merkle.py +0 -205
  76. astreum/models/patricia.py +0 -393
  77. astreum/storage/object.py +0 -68
  78. astreum-0.2.61.dist-info/RECORD +0 -57
  79. /astreum/{models → communication/handlers}/__init__.py +0 -0
  80. /astreum/{_communication → communication/models}/ping.py +0 -0
  81. /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
  82. /astreum/{_lispeum → machine/models}/meter.py +0 -0
  83. /astreum/{_lispeum → machine}/parser.py +0 -0
  84. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
  85. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
  86. {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,307 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from queue import Empty
5
+ from typing import Any, Callable
6
+
7
+ from ..models.account import Account
8
+ from ..models.accounts import Accounts
9
+ from ..models.block import Block
10
+ from ..models.transaction import apply_transaction
11
+ from ..validator import current_validator
12
+ from ...storage.models.atom import bytes_list_to_atoms
13
+ from ...communication.models.message import Message, MessageTopic
14
+ from ...communication.models.ping import Ping
15
+
16
+
17
+ def make_validation_worker(
18
+ node: Any,
19
+ ) -> Callable[[], None]:
20
+ """Build the validation worker bound to the given node."""
21
+
22
+ def _validation_worker() -> None:
23
+ node.logger.info("Validation worker started")
24
+ stop = node._validation_stop_event
25
+
26
+ def _award_validator_reward(block: Block, reward_amount: int) -> None:
27
+ """Credit the validator account with the provided reward."""
28
+ if reward_amount <= 0:
29
+ return
30
+ accounts = getattr(block, "accounts", None)
31
+ validator_key = getattr(block, "validator_public_key", None)
32
+ if accounts is None or not validator_key:
33
+ node.logger.debug(
34
+ "Skipping validator reward; accounts snapshot or key missing"
35
+ )
36
+ return
37
+ try:
38
+ validator_account = accounts.get_account(
39
+ address=validator_key, node=node
40
+ )
41
+ except Exception:
42
+ node.logger.exception("Unable to load validator account for reward")
43
+ return
44
+ if validator_account is None:
45
+ validator_account = Account.create()
46
+ validator_account.balance += reward_amount
47
+ accounts.set_account(validator_key, validator_account)
48
+
49
+ while not stop.is_set():
50
+ validation_public_key = getattr(node, "validation_public_key", None)
51
+ if not validation_public_key:
52
+ node.logger.debug("Validation public key unavailable; sleeping")
53
+ time.sleep(0.5)
54
+ continue
55
+
56
+ latest_block_hash = getattr(node, "latest_block_hash", None)
57
+ if not isinstance(latest_block_hash, (bytes, bytearray)):
58
+ node.logger.warning("Missing latest_block_hash; retrying")
59
+ time.sleep(0.5)
60
+ continue
61
+
62
+ node.logger.debug(
63
+ "Querying current validator for block %s",
64
+ latest_block_hash.hex()
65
+ if isinstance(latest_block_hash, (bytes, bytearray))
66
+ else latest_block_hash,
67
+ )
68
+ try:
69
+ scheduled_validator, _ = current_validator(node, latest_block_hash)
70
+ except Exception as exc:
71
+ node.logger.exception("Unable to determine current validator: %s", exc)
72
+ time.sleep(0.5)
73
+ continue
74
+
75
+ if scheduled_validator != validation_public_key:
76
+ expected_hex = (
77
+ scheduled_validator.hex()
78
+ if isinstance(scheduled_validator, (bytes, bytearray))
79
+ else scheduled_validator
80
+ )
81
+ node.logger.debug("Current validator mismatch; expected %s", expected_hex)
82
+ time.sleep(0.5)
83
+ continue
84
+
85
+ try:
86
+ previous_block = Block.from_atom(node, latest_block_hash)
87
+ except Exception:
88
+ node.logger.exception("Unable to load previous block for validation")
89
+ time.sleep(0.5)
90
+ continue
91
+
92
+ try:
93
+ current_hash = node._validation_transaction_queue.get_nowait()
94
+ queue_empty = False
95
+ except Empty:
96
+ current_hash = None
97
+ queue_empty = True
98
+ node.logger.debug(
99
+ "No pending validation transactions; generating empty block"
100
+ )
101
+
102
+ try:
103
+ accounts_snapshot = Accounts(root_hash=previous_block.accounts_hash)
104
+ except Exception:
105
+ accounts_snapshot = None
106
+ node.logger.warning("Unable to initialise accounts snapshot for block")
107
+
108
+ new_block = Block(
109
+ chain_id=getattr(node, "chain", 0),
110
+ previous_block_hash=latest_block_hash,
111
+ previous_block=previous_block,
112
+ number=(previous_block.number or 0) + 1,
113
+ timestamp=None,
114
+ accounts_hash=previous_block.accounts_hash,
115
+ transactions_total_fees=0,
116
+ transactions_hash=None,
117
+ receipts_hash=None,
118
+ delay_difficulty=None,
119
+ validator_public_key=validation_public_key,
120
+ nonce=0,
121
+ signature=None,
122
+ accounts=accounts_snapshot,
123
+ transactions=[],
124
+ receipts=[],
125
+ )
126
+ node.logger.debug(
127
+ "Creating block #%s extending %s",
128
+ new_block.number,
129
+ (
130
+ node.latest_block_hash.hex()
131
+ if isinstance(node.latest_block_hash, (bytes, bytearray))
132
+ else node.latest_block_hash
133
+ ),
134
+ )
135
+
136
+ # we may want to add a timer to process part of the txs only on a slow computer
137
+ total_fees = 0
138
+ while current_hash is not None:
139
+ try:
140
+ total_fees += apply_transaction(node, new_block, current_hash)
141
+ except NotImplementedError:
142
+ tx_hex = (
143
+ current_hash.hex()
144
+ if isinstance(current_hash, (bytes, bytearray))
145
+ else current_hash
146
+ )
147
+ node.logger.warning("Transaction %s unsupported; re-queued", tx_hex)
148
+ node._validation_transaction_queue.put(current_hash)
149
+ time.sleep(0.5)
150
+ break
151
+ except Exception:
152
+ tx_hex = (
153
+ current_hash.hex()
154
+ if isinstance(current_hash, (bytes, bytearray))
155
+ else current_hash
156
+ )
157
+ node.logger.exception("Failed applying transaction %s", tx_hex)
158
+
159
+ try:
160
+ current_hash = node._validation_transaction_queue.get_nowait()
161
+ except Empty:
162
+ current_hash = None
163
+
164
+ new_block.transactions_total_fees = total_fees
165
+ reward_amount = total_fees if total_fees > 0 else 1
166
+ if total_fees == 0 and queue_empty:
167
+ node.logger.debug("Awarding base validator reward of 1 aster")
168
+ elif total_fees > 0:
169
+ node.logger.debug(
170
+ "Collected %d aster in transaction fees for this block", total_fees
171
+ )
172
+ _award_validator_reward(new_block, reward_amount)
173
+
174
+ # create an atom list of transactions, save the list head hash as the block's transactions_hash
175
+ transactions = new_block.transactions or []
176
+ tx_hashes = [bytes(tx.hash) for tx in transactions if tx.hash]
177
+ head_hash, _ = bytes_list_to_atoms(tx_hashes)
178
+ new_block.transactions_hash = head_hash
179
+ node.logger.debug("Block includes %d transactions", len(transactions))
180
+
181
+ receipts = new_block.receipts or []
182
+ receipt_atoms = []
183
+ receipt_hashes = []
184
+ for rcpt in receipts:
185
+ receipt_id, atoms = rcpt.to_atom()
186
+ receipt_atoms.extend(atoms)
187
+ receipt_hashes.append(bytes(receipt_id))
188
+ receipts_head, _ = bytes_list_to_atoms(receipt_hashes)
189
+ new_block.receipts_hash = receipts_head
190
+ node.logger.debug("Block includes %d receipts", len(receipts))
191
+
192
+ account_atoms = []
193
+ if new_block.accounts is not None:
194
+ try:
195
+ account_atoms = new_block.accounts.update_trie(node)
196
+ new_block.accounts_hash = new_block.accounts.root_hash
197
+ node.logger.debug(
198
+ "Updated trie for %d cached accounts",
199
+ len(new_block.accounts._cache),
200
+ )
201
+ except Exception:
202
+ node.logger.exception("Failed to update accounts trie for block")
203
+
204
+ now = time.time()
205
+ min_allowed = new_block.previous_block.timestamp + 1
206
+ new_block.timestamp = max(int(now), min_allowed)
207
+
208
+ new_block.delay_difficulty = Block.calculate_delay_difficulty(
209
+ previous_timestamp=previous_block.timestamp,
210
+ current_timestamp=new_block.timestamp,
211
+ previous_difficulty=previous_block.delay_difficulty,
212
+ )
213
+
214
+ try:
215
+ new_block.generate_nonce(difficulty=previous_block.delay_difficulty)
216
+ node.logger.debug(
217
+ "Found nonce %s for block #%s at difficulty %s",
218
+ new_block.nonce,
219
+ new_block.number,
220
+ new_block.delay_difficulty,
221
+ )
222
+ except Exception:
223
+ node.logger.exception("Failed while searching for block nonce")
224
+ time.sleep(0.5)
225
+ continue
226
+
227
+ # atomize block
228
+ new_block_hash, new_block_atoms = new_block.to_atom()
229
+ # put as own latest block hash
230
+ node.latest_block_hash = new_block_hash
231
+ node.latest_block = new_block
232
+ node.logger.info(
233
+ "Validated block #%s with hash %s (%d atoms)",
234
+ new_block.number,
235
+ new_block_hash.hex(),
236
+ len(new_block_atoms),
237
+ )
238
+
239
+ # ping peers in the validation route to update their records
240
+ if node.validation_route and node.outgoing_queue and node.peers:
241
+ route_peers = {
242
+ peer_key
243
+ for bucket in getattr(node.validation_route, "buckets", {}).values()
244
+ for peer_key in bucket
245
+ }
246
+ if route_peers:
247
+ ping_payload = Ping(
248
+ is_validator=True,
249
+ latest_block=new_block_hash,
250
+ ).to_bytes()
251
+
252
+ for peer_key in route_peers:
253
+ peer_hex = (
254
+ peer_key.hex()
255
+ if isinstance(peer_key, (bytes, bytearray))
256
+ else peer_key
257
+ )
258
+ peer = node.get_peer(peer_key)
259
+ if peer is None:
260
+ node.logger.debug(
261
+ "Skipping validator ping to peer %s; peer not found",
262
+ peer_hex,
263
+ )
264
+ continue
265
+ address = getattr(peer, "address", None)
266
+ if not address:
267
+ node.logger.debug(
268
+ "Skipping validator ping to %s; address missing",
269
+ peer_hex,
270
+ )
271
+ continue
272
+ try:
273
+ ping_msg = Message(
274
+ topic=MessageTopic.PING,
275
+ content=ping_payload,
276
+ sender=node.relay_public_key,
277
+ )
278
+ ping_msg.encrypt(peer.shared_key_bytes)
279
+ node.outgoing_queue.put((ping_msg.to_bytes(), address))
280
+ node.logger.debug(
281
+ "Queued validator ping to %s (%s)",
282
+ address,
283
+ peer_key.hex()
284
+ if isinstance(peer_key, (bytes, bytearray))
285
+ else peer_key,
286
+ )
287
+ except Exception:
288
+ node.logger.exception("Failed queueing validator ping to %s", address)
289
+
290
+ # upload block atoms
291
+ for block_atom in new_block_atoms:
292
+ atom_id = block_atom.object_id()
293
+ node._hot_storage_set(key=atom_id, value=block_atom)
294
+
295
+ # upload receipt atoms
296
+ for receipt_atom in receipt_atoms:
297
+ atom_id = receipt_atom.object_id()
298
+ node._hot_storage_set(key=atom_id, value=receipt_atom)
299
+
300
+ # upload account atoms
301
+ for account_atom in account_atoms:
302
+ atom_id = account_atom.object_id()
303
+ node._hot_storage_set(key=atom_id, value=account_atom)
304
+
305
+ node.logger.info("Validation worker stopped")
306
+
307
+ return _validation_worker
@@ -4,13 +4,18 @@ import time
4
4
  from queue import Empty
5
5
  from typing import Any, Set
6
6
 
7
- from ..fork import Fork
7
+ from ..models.fork import Fork
8
8
 
9
9
 
10
10
  def _process_peers_latest_block(
11
11
  node: Any, latest_block_hash: bytes, peer_ids: Set[Any]
12
12
  ) -> None:
13
13
  """Assign peers to the fork that matches their reported head."""
14
+ node.logger.debug(
15
+ "Processing %d peers reporting block %s",
16
+ len(peer_ids),
17
+ latest_block_hash.hex() if isinstance(latest_block_hash, (bytes, bytearray)) else latest_block_hash,
18
+ )
14
19
  new_fork = Fork(head=latest_block_hash)
15
20
 
16
21
  current_fork_heads = {
@@ -22,6 +27,11 @@ def _process_peers_latest_block(
22
27
  if new_fork.validated_upto and new_fork.validated_upto in node.forks:
23
28
  ref = node.forks[new_fork.validated_upto]
24
29
  if getattr(ref, "malicious_block_hash", None):
30
+ node.logger.warning(
31
+ "Skipping fork from block %s referencing malicious fork %s",
32
+ latest_block_hash.hex() if isinstance(latest_block_hash, (bytes, bytearray)) else latest_block_hash,
33
+ new_fork.validated_upto.hex() if isinstance(new_fork.validated_upto, (bytes, bytearray)) else new_fork.validated_upto,
34
+ )
25
35
  return
26
36
  new_fork.root = ref.root
27
37
  new_fork.validated_upto = ref.validated_upto
@@ -34,12 +44,19 @@ def _process_peers_latest_block(
34
44
  fork.remove_peer(peer_id)
35
45
 
36
46
  node.forks[latest_block_hash] = new_fork
47
+ node.logger.debug(
48
+ "Fork %s now has %d peers (total forks %d)",
49
+ latest_block_hash.hex() if isinstance(latest_block_hash, (bytes, bytearray)) else latest_block_hash,
50
+ len(new_fork.peers),
51
+ len(node.forks),
52
+ )
37
53
 
38
54
 
39
55
  def make_verify_worker(node: Any):
40
56
  """Build the verify worker bound to the given node."""
41
57
 
42
58
  def _verify_worker() -> None:
59
+ node.logger.info("Verify worker started")
43
60
  stop = node._validation_stop_event
44
61
  while not stop.is_set():
45
62
  batch: list[tuple[bytes, Set[Any]]] = []
@@ -51,13 +68,23 @@ def make_verify_worker(node: Any):
51
68
  pass
52
69
 
53
70
  if not batch:
71
+ node.logger.debug("Verify queue empty; sleeping")
54
72
  time.sleep(0.1)
55
73
  continue
56
74
 
57
75
  for latest_b, peers in batch:
58
76
  try:
59
77
  _process_peers_latest_block(node, latest_b, peers)
78
+ node.logger.debug(
79
+ "Updated forks from block %s for %d peers",
80
+ latest_b.hex() if isinstance(latest_b, (bytes, bytearray)) else latest_b,
81
+ len(peers),
82
+ )
60
83
  except Exception:
61
- pass
84
+ latest_hex = (
85
+ latest_b.hex() if isinstance(latest_b, (bytes, bytearray)) else latest_b
86
+ )
87
+ node.logger.exception("Failed processing verification batch for %s", latest_hex)
88
+ node.logger.info("Verify worker stopped")
62
89
 
63
90
  return _verify_worker
@@ -0,0 +1,74 @@
1
+ import os
2
+ from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
3
+
4
+ def encrypt(key: bytes, nonce: bytes, plaintext: bytes, aad: bytes = b"") -> bytes:
5
+ """
6
+ Encrypt data using the ChaCha20-Poly1305 AEAD construction.
7
+
8
+ This function provides both confidentiality and integrity. The returned
9
+ value contains the ciphertext with the Poly1305 authentication tag
10
+ appended. The same key, nonce, and associated authenticated data (AAD)
11
+ must be provided during decryption.
12
+
13
+ Parameters
14
+ ----------
15
+ key : bytes
16
+ A 32-byte (256-bit) secret key.
17
+ nonce : bytes
18
+ A 12-byte nonce. Must be unique per key; nonce reuse with the same key
19
+ breaks security.
20
+ plaintext : bytes
21
+ The data to be encrypted.
22
+ aad : bytes, optional
23
+ Associated authenticated data that is not encrypted but is included
24
+ in authentication (e.g. headers or metadata). Defaults to empty.
25
+
26
+ Returns
27
+ -------
28
+ bytes
29
+ The encrypted output consisting of ciphertext followed by the
30
+ authentication tag.
31
+
32
+ Raises
33
+ ------
34
+ ValueError
35
+ If the key or nonce length is invalid.
36
+ """
37
+ aead = ChaCha20Poly1305(key)
38
+ return aead.encrypt(nonce, plaintext, aad)
39
+
40
+
41
+ def decrypt(key: bytes, nonce: bytes, ciphertext: bytes, aad: bytes = b"") -> bytes:
42
+ """
43
+ Decrypt data encrypted with ChaCha20-Poly1305 and verify its authenticity.
44
+
45
+ This function verifies the Poly1305 authentication tag before returning
46
+ the plaintext. If the ciphertext, nonce, key, or associated authenticated
47
+ data (AAD) has been altered, decryption fails.
48
+
49
+ Parameters
50
+ ----------
51
+ key : bytes
52
+ The same 32-byte (256-bit) secret key used for encryption.
53
+ nonce : bytes
54
+ The same 12-byte nonce used for encryption.
55
+ ciphertext : bytes
56
+ The encrypted data including the appended authentication tag.
57
+ aad : bytes, optional
58
+ The associated authenticated data used during encryption. Must match
59
+ exactly. Defaults to empty.
60
+
61
+ Returns
62
+ -------
63
+ bytes
64
+ The original decrypted plaintext.
65
+
66
+ Raises
67
+ ------
68
+ cryptography.exceptions.InvalidTag
69
+ If authentication fails due to tampering or incorrect inputs.
70
+ ValueError
71
+ If the key or nonce length is invalid.
72
+ """
73
+ aead = ChaCha20Poly1305(key)
74
+ return aead.decrypt(nonce, ciphertext, aad)
@@ -0,0 +1,20 @@
1
+ from .models.expression import Expr
2
+ from .models.environment import Env
3
+ from .evaluations.low_evaluation import low_eval
4
+ from .models.meter import Meter
5
+ from .parser import parse, ParseError
6
+ from .tokenizer import tokenize
7
+ from .evaluations.high_evaluation import high_eval
8
+ from .evaluations.script_evaluation import script_eval
9
+
10
+ __all__ = [
11
+ "Env",
12
+ "Expr",
13
+ "low_eval",
14
+ "Meter",
15
+ "parse",
16
+ "tokenize",
17
+ "high_eval",
18
+ "ParseError",
19
+ "script_eval",
20
+ ]
File without changes