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,98 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, List, Optional, Tuple
4
+
5
+ from ...storage.models.atom import Atom, AtomKind, ZERO32
6
+
7
+ STATUS_SUCCESS = 0
8
+ STATUS_FAILED = 1
9
+
10
+
11
+ def _int_to_be_bytes(value: Optional[int]) -> bytes:
12
+ if value is None:
13
+ return b""
14
+ value = int(value)
15
+ if value == 0:
16
+ return b"\x00"
17
+ size = (value.bit_length() + 7) // 8
18
+ return value.to_bytes(size, "big")
19
+
20
+
21
+ def _be_bytes_to_int(data: Optional[bytes]) -> int:
22
+ if not data:
23
+ return 0
24
+ return int.from_bytes(data, "big")
25
+
26
+
27
+ class Receipt:
28
+ def __init__(
29
+ self,
30
+ transaction_hash: bytes,
31
+ cost: int,
32
+ status: int,
33
+ logs_hash: bytes = ZERO32,
34
+ ) -> None:
35
+ self.transaction_hash = bytes(transaction_hash)
36
+ self.cost = int(cost)
37
+ self.logs_hash = bytes(logs_hash)
38
+ self.status = int(status)
39
+ self.atom_hash = ZERO32
40
+ self.atoms: List[Atom] = []
41
+
42
+ def to_atom(self) -> Tuple[bytes, List[Atom]]:
43
+ if self.status not in (STATUS_SUCCESS, STATUS_FAILED):
44
+ raise ValueError("unsupported receipt status")
45
+
46
+ detail_specs = [
47
+ (bytes(self.transaction_hash), AtomKind.LIST),
48
+ (_int_to_be_bytes(self.status), AtomKind.BYTES),
49
+ (_int_to_be_bytes(self.cost), AtomKind.BYTES),
50
+ (bytes(self.logs_hash), AtomKind.LIST),
51
+ ]
52
+
53
+ detail_atoms: List[Atom] = []
54
+ next_hash = ZERO32
55
+ for payload, kind in reversed(detail_specs):
56
+ atom = Atom(data=payload, next_id=next_hash, kind=kind)
57
+ detail_atoms.append(atom)
58
+ next_hash = atom.object_id()
59
+ detail_atoms.reverse()
60
+
61
+ type_atom = Atom(data=b"receipt", next_id=next_hash, kind=AtomKind.SYMBOL)
62
+
63
+ atoms = detail_atoms + [type_atom]
64
+ receipt_id = type_atom.object_id()
65
+ return receipt_id, atoms
66
+
67
+ @classmethod
68
+ def from_atom(cls, node: Any, receipt_id: bytes) -> Receipt:
69
+ atom_chain = node.get_atom_list_from_storage(receipt_id)
70
+ if atom_chain is None or len(atom_chain) != 5:
71
+ raise ValueError("malformed receipt atom chain")
72
+
73
+ type_atom, tx_atom, status_atom, cost_atom, logs_atom = atom_chain
74
+ if type_atom.kind is not AtomKind.SYMBOL or type_atom.data != b"receipt":
75
+ raise ValueError("not a receipt (type atom)")
76
+ if tx_atom.kind is not AtomKind.LIST:
77
+ raise ValueError("receipt transaction hash must be list-kind")
78
+ if status_atom.kind is not AtomKind.BYTES or cost_atom.kind is not AtomKind.BYTES or logs_atom.kind is not AtomKind.LIST:
79
+ raise ValueError("receipt detail atoms must be bytes-kind")
80
+
81
+ transaction_hash_bytes = tx_atom.data
82
+ status_bytes = status_atom.data
83
+ cost_bytes = cost_atom.data
84
+ logs_bytes = logs_atom.data
85
+
86
+ status_value = _be_bytes_to_int(status_bytes)
87
+ if status_value not in (STATUS_SUCCESS, STATUS_FAILED):
88
+ raise ValueError("unsupported receipt status")
89
+
90
+ receipt = cls(
91
+ transaction_hash=transaction_hash_bytes,
92
+ cost=_be_bytes_to_int(cost_bytes),
93
+ logs_hash=logs_bytes,
94
+ status=status_value,
95
+ )
96
+ receipt.atom_hash = bytes(receipt_id)
97
+ receipt.atoms = atom_chain
98
+ return receipt
@@ -3,14 +3,15 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Any, List, Optional, Tuple
5
5
 
6
- from .._storage.atom import Atom, AtomKind, ZERO32
7
- from ..utils.integer import bytes_to_int, int_to_bytes
6
+ from ...storage.models.atom import Atom, AtomKind, ZERO32
7
+ from ...utils.integer import bytes_to_int, int_to_bytes
8
8
  from .account import Account
9
- from .genesis import TREASURY_ADDRESS
9
+ from ..genesis import TREASURY_ADDRESS
10
10
  from .receipt import STATUS_FAILED, Receipt, STATUS_SUCCESS
11
11
 
12
12
  @dataclass
13
13
  class Transaction:
14
+ chain_id: int
14
15
  amount: int
15
16
  counter: int
16
17
  data: bytes = b""
@@ -21,42 +22,40 @@ class Transaction:
21
22
 
22
23
  def to_atom(self) -> Tuple[bytes, List[Atom]]:
23
24
  """Serialise the transaction, returning (object_id, atoms)."""
24
- body_child_ids: List[bytes] = []
25
+ detail_payloads: List[bytes] = []
25
26
  acc: List[Atom] = []
26
27
 
27
28
  def emit(payload: bytes) -> None:
28
- atom = Atom.from_data(data=payload, kind=AtomKind.BYTES)
29
- body_child_ids.append(atom.object_id())
30
- acc.append(atom)
29
+ detail_payloads.append(payload)
31
30
 
31
+ emit(int_to_bytes(self.chain_id))
32
32
  emit(int_to_bytes(self.amount))
33
33
  emit(int_to_bytes(self.counter))
34
34
  emit(bytes(self.data))
35
35
  emit(bytes(self.recipient))
36
36
  emit(bytes(self.sender))
37
37
 
38
- # Build the linked list of body entry references.
39
- body_atoms: List[Atom] = []
40
38
  body_head = ZERO32
41
- for child_id in reversed(body_child_ids):
42
- node = Atom.from_data(data=child_id, next_hash=body_head, kind=AtomKind.BYTES)
43
- body_head = node.object_id()
44
- body_atoms.append(node)
45
- body_atoms.reverse()
46
- acc.extend(body_atoms)
47
-
48
- body_list_atom = Atom.from_data(data=body_head, kind=AtomKind.LIST)
39
+ detail_atoms: List[Atom] = []
40
+ for payload in reversed(detail_payloads):
41
+ atom = Atom(data=payload, next_id=body_head, kind=AtomKind.BYTES)
42
+ detail_atoms.append(atom)
43
+ body_head = atom.object_id()
44
+ detail_atoms.reverse()
45
+ acc.extend(detail_atoms)
46
+
47
+ body_list_atom = Atom(data=body_head, kind=AtomKind.LIST)
49
48
  acc.append(body_list_atom)
50
49
  body_list_id = body_list_atom.object_id()
51
50
 
52
- signature_atom = Atom.from_data(
51
+ signature_atom = Atom(
53
52
  data=bytes(self.signature),
54
- next_hash=body_list_id,
53
+ next_id=body_list_id,
55
54
  kind=AtomKind.BYTES,
56
55
  )
57
- type_atom = Atom.from_data(
56
+ type_atom = Atom(
58
57
  data=b"transaction",
59
- next_hash=signature_atom.object_id(),
58
+ next_id=signature_atom.object_id(),
60
59
  kind=AtomKind.SYMBOL,
61
60
  )
62
61
 
@@ -103,49 +102,38 @@ class Transaction:
103
102
  raise ValueError(f"malformed {context}")
104
103
  return atom
105
104
 
106
- def _read_list(head_id: Optional[bytes], context: str) -> List[bytes]:
107
- entries: List[bytes] = []
108
- current = head_id if head_id and head_id != ZERO32 else None
109
- while current:
110
- node = storage_get(current)
111
- if node is None:
112
- raise ValueError(f"missing list node while decoding {context}")
113
- node_kind = _atom_kind(node)
114
- if node_kind is not AtomKind.BYTES:
115
- raise ValueError(f"invalid list node while decoding {context}")
116
- if len(node.data) != len(ZERO32):
117
- raise ValueError(f"malformed list entry while decoding {context}")
118
- entries.append(node.data)
119
- nxt = node.next
120
- current = nxt if nxt and nxt != ZERO32 else None
121
- return entries
122
-
123
- def _read_detail_bytes(entry_id: Optional[bytes]) -> bytes:
124
- if not entry_id or entry_id == ZERO32:
125
- return b""
126
- detail_atom = storage_get(entry_id)
127
- return detail_atom.data if detail_atom is not None else b""
128
-
129
105
  type_atom = _require_atom(transaction_id, "transaction type atom", AtomKind.SYMBOL)
130
106
  if type_atom.data != b"transaction":
131
107
  raise ValueError("not a transaction (type atom payload)")
132
108
 
133
- signature_atom = _require_atom(type_atom.next, "transaction signature atom", AtomKind.BYTES)
134
- body_list_atom = _require_atom(signature_atom.next, "transaction body list atom", AtomKind.LIST)
135
- if body_list_atom.next and body_list_atom.next != ZERO32:
109
+ signature_atom = _require_atom(type_atom.next_id, "transaction signature atom", AtomKind.BYTES)
110
+ body_list_atom = _require_atom(signature_atom.next_id, "transaction body list atom", AtomKind.LIST)
111
+ if body_list_atom.next_id and body_list_atom.next_id != ZERO32:
136
112
  raise ValueError("malformed transaction (body list tail)")
137
113
 
138
- body_entry_ids = _read_list(body_list_atom.data, "transaction body")
139
- if len(body_entry_ids) < 5:
140
- body_entry_ids.extend([ZERO32] * (5 - len(body_entry_ids)))
141
-
142
- amount_bytes = _read_detail_bytes(body_entry_ids[0])
143
- counter_bytes = _read_detail_bytes(body_entry_ids[1])
144
- data_bytes = _read_detail_bytes(body_entry_ids[2])
145
- recipient_bytes = _read_detail_bytes(body_entry_ids[3])
146
- sender_bytes = _read_detail_bytes(body_entry_ids[4])
114
+ detail_atoms = node.get_atom_list_from_storage(body_list_atom.data)
115
+ if detail_atoms is None:
116
+ raise ValueError("missing transaction body list nodes")
117
+ if len(detail_atoms) != 6:
118
+ raise ValueError("transaction body must contain exactly 6 detail entries")
119
+
120
+ detail_values: List[bytes] = []
121
+ for detail_atom in detail_atoms:
122
+ if detail_atom.kind is not AtomKind.BYTES:
123
+ raise ValueError("transaction detail atoms must be bytes")
124
+ detail_values.append(detail_atom.data)
125
+
126
+ (
127
+ chain_id_bytes,
128
+ amount_bytes,
129
+ counter_bytes,
130
+ data_bytes,
131
+ recipient_bytes,
132
+ sender_bytes,
133
+ ) = detail_values
147
134
 
148
135
  return cls(
136
+ chain_id=bytes_to_int(chain_id_bytes),
149
137
  amount=bytes_to_int(amount_bytes),
150
138
  counter=bytes_to_int(counter_bytes),
151
139
  data=data_bytes,
@@ -156,33 +144,41 @@ class Transaction:
156
144
  )
157
145
 
158
146
 
159
- def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None:
160
- """Apply transaction to the candidate block. Override downstream."""
147
+ def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> int:
148
+ """Apply transaction to the candidate block and return the collected fee."""
161
149
  transaction = Transaction.from_atom(node, transaction_hash)
162
150
 
163
- accounts = block.accounts
151
+ block_chain = getattr(block, "chain_id", None)
152
+ if block_chain is not None and transaction.chain_id != block_chain:
153
+ return 0
164
154
 
165
- sender_account = accounts.get_account(address=transaction.sender, node=node)
155
+ accounts = getattr(block, "accounts", None)
156
+ if accounts is None:
157
+ raise ValueError("block missing accounts snapshot for transaction application")
166
158
 
159
+ sender_account = accounts.get_account(address=transaction.sender, node=node)
167
160
  if sender_account is None:
168
- return
169
-
170
- tx_cost = 1 + transaction.amount
161
+ return 0
162
+
163
+ tx_fee = 1
164
+ tx_cost = tx_fee + transaction.amount
171
165
 
172
166
  if sender_account.balance < tx_cost:
173
167
  low_sender_balance_receipt = Receipt(
174
- transaction_hash=transaction_hash,
168
+ transaction_hash=bytes(transaction_hash),
175
169
  cost=0,
176
- logs=b"low sender balance",
177
- status=STATUS_FAILED
170
+ status=STATUS_FAILED,
178
171
  )
179
- low_sender_balance_receipt.atomize()
180
- block.receipts.append(receipt)
172
+ low_sender_balance_receipt.to_atom()
173
+ if block.receipts is None:
174
+ block.receipts = []
175
+ block.receipts.append(low_sender_balance_receipt)
176
+ if block.transactions is None:
177
+ block.transactions = []
181
178
  block.transactions.append(transaction)
182
- return
179
+ return 0
183
180
 
184
181
  recipient_account = accounts.get_account(address=transaction.recipient, node=node)
185
-
186
182
  if recipient_account is None:
187
183
  recipient_account = Account.create()
188
184
 
@@ -198,18 +194,20 @@ def apply_transaction(node: Any, block: object, transaction_hash: bytes) -> None
198
194
  recipient_account.balance += transaction.amount
199
195
 
200
196
  sender_account.balance -= tx_cost
197
+ accounts.set_account(transaction.sender, sender_account)
198
+ accounts.set_account(transaction.recipient, recipient_account)
201
199
 
202
- block.accounts.set_account(address=sender_account)
203
-
204
- block.accounts.set_account(address=recipient_account)
205
-
206
- block.transactions.append(transaction_hash)
200
+ if block.transactions is None:
201
+ block.transactions = []
202
+ block.transactions.append(transaction)
207
203
 
208
204
  receipt = Receipt(
209
205
  transaction_hash=bytes(transaction_hash),
210
- cost=0,
211
- logs=b"",
206
+ cost=tx_fee,
212
207
  status=STATUS_SUCCESS,
213
208
  )
214
- receipt.atomize()
209
+ receipt.to_atom()
210
+ if block.receipts is None:
211
+ block.receipts = []
215
212
  block.receipts.append(receipt)
213
+ return tx_fee
@@ -4,22 +4,18 @@ import threading
4
4
  from queue import Queue
5
5
  from typing import Any, Optional
6
6
 
7
+ from .validator import current_validator # re-exported for compatibility
7
8
  from .workers import (
8
9
  make_discovery_worker,
9
10
  make_validation_worker,
10
11
  make_verify_worker,
11
12
  )
12
- from .genesis import create_genesis_block
13
13
  from ..utils.bytes import hex_to_bytes
14
14
 
15
15
 
16
- def current_validator(node: Any) -> bytes:
17
- """Return the current validator identifier. Override downstream."""
18
- raise NotImplementedError("current_validator must be implemented by the host node")
19
-
20
-
21
16
  def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
22
17
  config = config or {}
18
+ node.logger.info("Setting up node consensus")
23
19
 
24
20
  # Shared state
25
21
  node.validation_lock = getattr(node, "validation_lock", threading.RLock())
@@ -29,12 +25,22 @@ def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
29
25
  # - forks: Dict[head, Fork]
30
26
  node.chains = getattr(node, "chains", {})
31
27
  node.forks = getattr(node, "forks", {})
28
+ node.logger.info(
29
+ "Consensus maps initialized (chains=%s, forks=%s)",
30
+ len(node.chains),
31
+ len(node.forks),
32
+ )
32
33
 
33
- node.latest_block_hash = None
34
34
  latest_block_hex = config.get("latest_block_hash")
35
35
  if latest_block_hex is not None:
36
36
  node.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
37
- node.latest_block = None
37
+
38
+ node.latest_block_hash = getattr(node, "latest_block_hash", None)
39
+ node.latest_block = getattr(node, "latest_block", None)
40
+ node.logger.info(
41
+ "Consensus latest_block_hash preset: %s",
42
+ node.latest_block_hash.hex() if isinstance(node.latest_block_hash, bytes) else node.latest_block_hash,
43
+ )
38
44
 
39
45
  # Pending transactions queue (hash-only entries)
40
46
  node._validation_transaction_queue = getattr(
@@ -57,9 +63,7 @@ def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
57
63
  node.enqueue_transaction_hash = enqueue_transaction_hash
58
64
 
59
65
  verify_worker = make_verify_worker(node)
60
- validation_worker = make_validation_worker(
61
- node, current_validator=current_validator
62
- )
66
+ validation_worker = make_validation_worker(node)
63
67
 
64
68
  # Start workers as daemons
65
69
  discovery_worker = make_discovery_worker(node)
@@ -73,43 +77,7 @@ def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
73
77
  target=validation_worker, daemon=True, name="consensus-validation"
74
78
  )
75
79
  node.consensus_discovery_thread.start()
80
+ node.logger.info("Started consensus discovery thread (%s)", node.consensus_discovery_thread.name)
76
81
  node.consensus_verify_thread.start()
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
-
115
- node.consensus_validation_thread.start()
82
+ node.logger.info("Started consensus verify thread (%s)", node.consensus_verify_thread.name)
83
+ node.logger.info("Consensus setup ready")
@@ -0,0 +1,67 @@
1
+ from cryptography.hazmat.primitives import serialization
2
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
3
+
4
+ from astreum.consensus.genesis import create_genesis_block
5
+
6
+
7
+ def process_blocks_and_transactions(self, validator_secret_key: Ed25519PrivateKey):
8
+ """Initialize validator keys, ensure genesis exists, then start validation thread."""
9
+ self.logger.info(
10
+ "Initializing block and transaction processing for chain %s",
11
+ self.config["chain"],
12
+ )
13
+
14
+ self.validation_secret_key = validator_secret_key
15
+ validator_public_key_obj = self.validation_secret_key.public_key()
16
+ validator_public_key_bytes = validator_public_key_obj.public_bytes(
17
+ encoding=serialization.Encoding.Raw,
18
+ format=serialization.PublicFormat.Raw,
19
+ )
20
+ self.validation_public_key = validator_public_key_bytes
21
+ self.logger.debug(
22
+ "Derived validator public key %s", validator_public_key_bytes.hex()
23
+ )
24
+
25
+ if self.latest_block_hash is None:
26
+ genesis_block = create_genesis_block(
27
+ self,
28
+ validator_public_key=validator_public_key_bytes,
29
+ chain_id=self.config["chain_id"],
30
+ )
31
+ account_atoms = genesis_block.accounts.update_trie(self) if genesis_block.accounts else []
32
+
33
+ genesis_hash, genesis_atoms = genesis_block.to_atom()
34
+ self.logger.debug(
35
+ "Genesis block created with %s atoms (%s account atoms)",
36
+ len(genesis_atoms),
37
+ len(account_atoms),
38
+ )
39
+
40
+ for atom in account_atoms + genesis_atoms:
41
+ try:
42
+ self._hot_storage_set(key=atom.object_id(), value=atom)
43
+ except Exception as exc:
44
+ self.logger.warning(
45
+ "Unable to persist genesis atom %s: %s",
46
+ atom.object_id(),
47
+ exc,
48
+ )
49
+
50
+ self.latest_block_hash = genesis_hash
51
+ self.latest_block = genesis_block
52
+ self.logger.info("Genesis block stored with hash %s", genesis_hash.hex())
53
+ else:
54
+ self.logger.debug(
55
+ "latest_block_hash already set to %s; skipping genesis creation",
56
+ self.latest_block_hash.hex()
57
+ if isinstance(self.latest_block_hash, (bytes, bytearray))
58
+ else self.latest_block_hash,
59
+ )
60
+
61
+ self.logger.info(
62
+ "Starting consensus validation thread (%s)",
63
+ self.consensus_validation_thread.name,
64
+ )
65
+ self.consensus_validation_thread.start()
66
+
67
+ # ping all peers to announce validation capability
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from typing import Any, Dict, Optional, Tuple
5
+
6
+ from .genesis import TREASURY_ADDRESS
7
+ from .models.account import Account
8
+ from .models.accounts import Accounts
9
+ from .models.block import Block
10
+ from ..storage.models.atom import ZERO32
11
+ from ..utils.integer import bytes_to_int, int_to_bytes
12
+
13
+
14
+ def current_validator(
15
+ node: Any,
16
+ block_hash: bytes,
17
+ target_time: Optional[int] = None,
18
+ ) -> Tuple[bytes, Accounts]:
19
+ """
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
+ """
24
+
25
+ block = Block.from_atom(node, block_hash)
26
+
27
+ if block.timestamp is None:
28
+ raise ValueError("block timestamp missing")
29
+
30
+ block_timestamp = block.timestamp
31
+ target_timestamp = int(target_time) if target_time is not None else block_timestamp + 1
32
+ if target_timestamp <= block_timestamp:
33
+ target_timestamp = block_timestamp + 1
34
+
35
+ accounts_hash = getattr(block, "accounts_hash", None)
36
+ if not accounts_hash:
37
+ raise ValueError("block missing accounts hash")
38
+ accounts = Accounts(root_hash=accounts_hash)
39
+
40
+ treasury_account = accounts.get_account(TREASURY_ADDRESS, node)
41
+ if treasury_account is None:
42
+ raise ValueError("treasury account missing from accounts trie")
43
+
44
+ stake_trie = treasury_account.data
45
+
46
+ stakes: Dict[bytes, int] = {}
47
+ for account_key, stake_amount in stake_trie.get_all(node).items():
48
+ if not account_key:
49
+ continue
50
+ stakes[account_key] = bytes_to_int(stake_amount)
51
+
52
+ if not stakes:
53
+ raise ValueError("no validator stakes found in treasury trie")
54
+
55
+ seed_source = block_hash or getattr(block, "previous_block_hash", ZERO32)
56
+ seed_value = int.from_bytes(bytes(seed_source), "big", signed=False)
57
+ rng = random.Random(seed_value)
58
+
59
+ def pick_validator() -> bytes:
60
+ positive_weights = [(key, weight) for key, weight in stakes.items() if weight > 0]
61
+ if not positive_weights:
62
+ raise ValueError("no validators with positive stake")
63
+ total_weight = sum(weight for _, weight in positive_weights)
64
+ choice = rng.randrange(total_weight)
65
+ cumulative = 0
66
+ for key, weight in sorted(positive_weights, key=lambda item: item[0]):
67
+ cumulative += weight
68
+ if choice < cumulative:
69
+ return key
70
+ return positive_weights[-1][0]
71
+
72
+ def halve_stake(validator_key: bytes) -> None:
73
+ current_amount = stakes.get(validator_key, 0)
74
+ if current_amount <= 0:
75
+ raise ValueError("validator stake must be positive")
76
+ new_amount = current_amount // 2
77
+ returned_amount = current_amount - new_amount
78
+ stakes[validator_key] = new_amount
79
+ stake_trie.put(node, validator_key, int_to_bytes(new_amount))
80
+ treasury_account.data_hash = stake_trie.root_hash or ZERO32
81
+
82
+ validator_account = accounts.get_account(validator_key, node)
83
+ if validator_account is None:
84
+ validator_account = Account.create()
85
+ validator_account.balance += returned_amount
86
+ accounts.set_account(validator_key, validator_account)
87
+ accounts.set_account(TREASURY_ADDRESS, treasury_account)
88
+
89
+ iteration_target = block_timestamp + 1
90
+ while True:
91
+ selected_validator = pick_validator()
92
+ halve_stake(selected_validator)
93
+ if iteration_target == target_timestamp:
94
+ return selected_validator, accounts
95
+ iteration_target += 1
@@ -13,6 +13,7 @@ def make_discovery_worker(node: Any):
13
13
  """
14
14
 
15
15
  def _discovery_worker() -> None:
16
+ node.logger.info("Discovery worker started")
16
17
  stop = node._validation_stop_event
17
18
  while not stop.is_set():
18
19
  try:
@@ -33,6 +34,16 @@ def make_discovery_worker(node: Any):
33
34
  for hb in latest_keys
34
35
  }
35
36
 
37
+ if not pairs:
38
+ node.logger.debug("No peers reported latest blocks; skipping queue update")
39
+ continue
40
+
41
+ node.logger.debug(
42
+ "Discovery grouped %d block hashes from %d peers",
43
+ len(grouped),
44
+ len(pairs),
45
+ )
46
+
36
47
  try:
37
48
  while True:
38
49
  node._validation_verify_queue.get_nowait()
@@ -40,9 +51,16 @@ def make_discovery_worker(node: Any):
40
51
  pass
41
52
  for latest_b, peer_set in grouped.items():
42
53
  node._validation_verify_queue.put((latest_b, peer_set))
54
+ node.logger.debug(
55
+ "Queued %d peers for validation of block %s",
56
+ len(peer_set),
57
+ latest_b.hex(),
58
+ )
43
59
  except Exception:
44
- pass
60
+ node.logger.exception("Discovery worker iteration failed")
45
61
  finally:
46
62
  time.sleep(0.5)
47
63
 
64
+ node.logger.info("Discovery worker stopped")
65
+
48
66
  return _discovery_worker