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.
- astreum/__init__.py +16 -7
- astreum/{_communication → communication}/__init__.py +3 -3
- astreum/communication/handlers/handshake.py +89 -0
- astreum/communication/handlers/object_request.py +176 -0
- astreum/communication/handlers/object_response.py +115 -0
- astreum/communication/handlers/ping.py +34 -0
- astreum/communication/handlers/route_request.py +76 -0
- astreum/communication/handlers/route_response.py +53 -0
- astreum/communication/models/__init__.py +0 -0
- astreum/communication/models/message.py +124 -0
- astreum/communication/models/peer.py +51 -0
- astreum/{_communication → communication/models}/route.py +7 -12
- astreum/communication/processors/__init__.py +0 -0
- astreum/communication/processors/incoming.py +98 -0
- astreum/communication/processors/outgoing.py +20 -0
- astreum/communication/setup.py +166 -0
- astreum/communication/start.py +37 -0
- astreum/{_communication → communication}/util.py +7 -0
- astreum/consensus/__init__.py +20 -0
- astreum/consensus/genesis.py +66 -0
- astreum/consensus/models/__init__.py +0 -0
- astreum/consensus/models/account.py +84 -0
- astreum/consensus/models/accounts.py +72 -0
- astreum/consensus/models/block.py +364 -0
- astreum/{_consensus → consensus/models}/chain.py +7 -7
- astreum/{_consensus → consensus/models}/fork.py +8 -8
- astreum/consensus/models/receipt.py +98 -0
- astreum/{_consensus → consensus/models}/transaction.py +76 -78
- astreum/{_consensus → consensus}/setup.py +18 -50
- astreum/consensus/start.py +67 -0
- astreum/consensus/validator.py +95 -0
- astreum/{_consensus → consensus}/workers/discovery.py +19 -1
- astreum/consensus/workers/validation.py +307 -0
- astreum/{_consensus → consensus}/workers/verify.py +29 -2
- astreum/crypto/chacha20poly1305.py +74 -0
- astreum/machine/__init__.py +20 -0
- astreum/machine/evaluations/__init__.py +0 -0
- astreum/{_lispeum → machine/evaluations}/high_evaluation.py +237 -236
- astreum/machine/evaluations/low_evaluation.py +281 -0
- astreum/machine/evaluations/script_evaluation.py +27 -0
- astreum/machine/models/__init__.py +0 -0
- astreum/machine/models/environment.py +31 -0
- astreum/{_lispeum → machine/models}/expression.py +36 -8
- astreum/machine/tokenizer.py +90 -0
- astreum/node.py +78 -767
- astreum/storage/__init__.py +7 -0
- astreum/storage/actions/get.py +183 -0
- astreum/storage/actions/set.py +178 -0
- astreum/{_storage → storage/models}/atom.py +55 -57
- astreum/{_storage/patricia.py → storage/models/trie.py} +227 -203
- astreum/storage/requests.py +28 -0
- astreum/storage/setup.py +22 -15
- astreum/utils/config.py +48 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/METADATA +27 -26
- astreum-0.3.9.dist-info/RECORD +71 -0
- astreum/_communication/message.py +0 -101
- astreum/_communication/peer.py +0 -23
- astreum/_communication/setup.py +0 -322
- astreum/_consensus/__init__.py +0 -20
- astreum/_consensus/account.py +0 -95
- astreum/_consensus/accounts.py +0 -38
- astreum/_consensus/block.py +0 -311
- astreum/_consensus/genesis.py +0 -72
- astreum/_consensus/receipt.py +0 -136
- astreum/_consensus/workers/validation.py +0 -125
- astreum/_lispeum/__init__.py +0 -16
- astreum/_lispeum/environment.py +0 -13
- astreum/_lispeum/low_evaluation.py +0 -123
- astreum/_lispeum/tokenizer.py +0 -22
- astreum/_node.py +0 -198
- astreum/_storage/__init__.py +0 -7
- astreum/_storage/setup.py +0 -35
- astreum/format.py +0 -75
- astreum/models/block.py +0 -441
- astreum/models/merkle.py +0 -205
- astreum/models/patricia.py +0 -393
- astreum/storage/object.py +0 -68
- astreum-0.2.61.dist-info/RECORD +0 -57
- /astreum/{models → communication/handlers}/__init__.py +0 -0
- /astreum/{_communication → communication/models}/ping.py +0 -0
- /astreum/{_consensus → consensus}/workers/__init__.py +0 -0
- /astreum/{_lispeum → machine/models}/meter.py +0 -0
- /astreum/{_lispeum → machine}/parser.py +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/WHEEL +0 -0
- {astreum-0.2.61.dist-info → astreum-0.3.9.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
7
|
-
from
|
|
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
|
|
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
|
-
|
|
25
|
+
detail_payloads: List[bytes] = []
|
|
25
26
|
acc: List[Atom] = []
|
|
26
27
|
|
|
27
28
|
def emit(payload: bytes) -> None:
|
|
28
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
body_head =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
51
|
+
signature_atom = Atom(
|
|
53
52
|
data=bytes(self.signature),
|
|
54
|
-
|
|
53
|
+
next_id=body_list_id,
|
|
55
54
|
kind=AtomKind.BYTES,
|
|
56
55
|
)
|
|
57
|
-
type_atom = Atom
|
|
56
|
+
type_atom = Atom(
|
|
58
57
|
data=b"transaction",
|
|
59
|
-
|
|
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.
|
|
134
|
-
body_list_atom = _require_atom(signature_atom.
|
|
135
|
-
if body_list_atom.
|
|
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
|
-
|
|
139
|
-
if
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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) ->
|
|
160
|
-
"""Apply transaction to the candidate block
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
status=STATUS_FAILED
|
|
170
|
+
status=STATUS_FAILED,
|
|
178
171
|
)
|
|
179
|
-
low_sender_balance_receipt.
|
|
180
|
-
block.receipts
|
|
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.
|
|
203
|
-
|
|
204
|
-
block.
|
|
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=
|
|
211
|
-
logs=b"",
|
|
206
|
+
cost=tx_fee,
|
|
212
207
|
status=STATUS_SUCCESS,
|
|
213
208
|
)
|
|
214
|
-
receipt.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|