astreum 0.3.9__py3-none-any.whl → 0.3.46__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 +5 -4
- astreum/communication/__init__.py +15 -11
- astreum/communication/difficulty.py +39 -0
- astreum/communication/disconnect.py +57 -0
- astreum/communication/handlers/handshake.py +105 -89
- astreum/communication/handlers/object_request.py +179 -149
- astreum/communication/handlers/object_response.py +7 -1
- astreum/communication/handlers/ping.py +9 -0
- astreum/communication/handlers/route_request.py +7 -1
- astreum/communication/handlers/route_response.py +7 -1
- astreum/communication/incoming_queue.py +96 -0
- astreum/communication/message_pow.py +36 -0
- astreum/communication/models/peer.py +4 -0
- astreum/communication/models/ping.py +27 -6
- astreum/communication/models/route.py +4 -0
- astreum/communication/{start.py → node.py} +10 -11
- astreum/communication/outgoing_queue.py +108 -0
- astreum/communication/processors/incoming.py +110 -37
- astreum/communication/processors/outgoing.py +35 -2
- astreum/communication/processors/peer.py +134 -0
- astreum/communication/setup.py +273 -112
- astreum/communication/util.py +14 -0
- astreum/node.py +99 -89
- astreum/storage/actions/get.py +79 -48
- astreum/storage/actions/set.py +171 -156
- astreum/storage/providers.py +24 -0
- astreum/storage/setup.py +23 -22
- astreum/utils/config.py +247 -30
- astreum/utils/logging.py +1 -1
- astreum/{consensus → validation}/__init__.py +0 -4
- astreum/validation/constants.py +2 -0
- astreum/{consensus → validation}/genesis.py +4 -6
- astreum/validation/models/block.py +544 -0
- astreum/validation/models/fork.py +511 -0
- astreum/{consensus → validation}/models/receipt.py +17 -4
- astreum/{consensus → validation}/models/transaction.py +45 -3
- astreum/validation/node.py +190 -0
- astreum/{consensus → validation}/validator.py +18 -9
- astreum/validation/workers/__init__.py +8 -0
- astreum/{consensus → validation}/workers/validation.py +361 -307
- astreum/verification/__init__.py +4 -0
- astreum/{consensus/workers/discovery.py → verification/discover.py} +1 -1
- astreum/verification/node.py +61 -0
- astreum/verification/worker.py +183 -0
- {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/METADATA +43 -9
- astreum-0.3.46.dist-info/RECORD +79 -0
- astreum/consensus/models/block.py +0 -364
- astreum/consensus/models/chain.py +0 -66
- astreum/consensus/models/fork.py +0 -100
- astreum/consensus/setup.py +0 -83
- astreum/consensus/start.py +0 -67
- astreum/consensus/workers/__init__.py +0 -9
- astreum/consensus/workers/verify.py +0 -90
- astreum-0.3.9.dist-info/RECORD +0 -71
- /astreum/{consensus → validation}/models/__init__.py +0 -0
- /astreum/{consensus → validation}/models/account.py +0 -0
- /astreum/{consensus → validation}/models/accounts.py +0 -0
- {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/WHEEL +0 -0
- {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.3.9.dist-info → astreum-0.3.46.dist-info}/top_level.txt +0 -0
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING
|
|
3
|
-
|
|
4
|
-
from ...storage.models.atom import Atom, AtomKind, ZERO32, hash_bytes
|
|
5
|
-
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from ...storage.models.trie import Trie
|
|
8
|
-
from .transaction import Transaction
|
|
9
|
-
from .receipt import Receipt
|
|
10
|
-
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
11
|
-
from cryptography.exceptions import InvalidSignature
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _int_to_be_bytes(n: Optional[int]) -> bytes:
|
|
15
|
-
if n is None:
|
|
16
|
-
return b""
|
|
17
|
-
n = int(n)
|
|
18
|
-
if n == 0:
|
|
19
|
-
return b"\x00"
|
|
20
|
-
size = (n.bit_length() + 7) // 8
|
|
21
|
-
return n.to_bytes(size, "big")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _be_bytes_to_int(b: Optional[bytes]) -> int:
|
|
25
|
-
if not b:
|
|
26
|
-
return 0
|
|
27
|
-
return int.from_bytes(b, "big")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class Block:
|
|
31
|
-
"""Validation Block representation using Atom storage.
|
|
32
|
-
|
|
33
|
-
Top-level encoding:
|
|
34
|
-
block_id = type_atom.object_id()
|
|
35
|
-
chain: type_atom --next--> signature_atom --next--> body_list_atom --next--> ZERO32
|
|
36
|
-
where: type_atom = Atom(kind=AtomKind.SYMBOL, data=b"block")
|
|
37
|
-
signature_atom = Atom(kind=AtomKind.BYTES, data=<signature-bytes>)
|
|
38
|
-
body_list_atom = Atom(kind=AtomKind.LIST, data=<body_head_id>)
|
|
39
|
-
|
|
40
|
-
Details order in body_list:
|
|
41
|
-
0: chain (byte)
|
|
42
|
-
1: previous_block_hash (bytes)
|
|
43
|
-
2: number (int -> big-endian bytes)
|
|
44
|
-
3: timestamp (int -> big-endian bytes)
|
|
45
|
-
4: accounts_hash (bytes)
|
|
46
|
-
5: transactions_total_fees (int -> big-endian bytes)
|
|
47
|
-
6: transactions_hash (bytes)
|
|
48
|
-
7: receipts_hash (bytes)
|
|
49
|
-
8: delay_difficulty (int -> big-endian bytes)
|
|
50
|
-
9: validator_public_key (bytes)
|
|
51
|
-
10: nonce (int -> big-endian bytes)
|
|
52
|
-
|
|
53
|
-
Notes:
|
|
54
|
-
- "body tree" is represented here by the body_list id (self.body_hash), not
|
|
55
|
-
embedded again as a field to avoid circular references.
|
|
56
|
-
- "signature" is a field on the class but is not required for validation
|
|
57
|
-
navigation; include it in the instance but it is not encoded in atoms
|
|
58
|
-
unless explicitly provided via details extension in the future.
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
# essential identifiers
|
|
62
|
-
atom_hash: Optional[bytes]
|
|
63
|
-
chain_id: int
|
|
64
|
-
previous_block_hash: bytes
|
|
65
|
-
previous_block: Optional["Block"]
|
|
66
|
-
|
|
67
|
-
# block details
|
|
68
|
-
number: int
|
|
69
|
-
timestamp: Optional[int]
|
|
70
|
-
accounts_hash: Optional[bytes]
|
|
71
|
-
transactions_total_fees: Optional[int]
|
|
72
|
-
transactions_hash: Optional[bytes]
|
|
73
|
-
receipts_hash: Optional[bytes]
|
|
74
|
-
delay_difficulty: Optional[int]
|
|
75
|
-
validator_public_key: Optional[bytes]
|
|
76
|
-
nonce: Optional[int]
|
|
77
|
-
|
|
78
|
-
# additional
|
|
79
|
-
body_hash: Optional[bytes]
|
|
80
|
-
signature: Optional[bytes]
|
|
81
|
-
|
|
82
|
-
# structures
|
|
83
|
-
accounts: Optional["Trie"]
|
|
84
|
-
transactions: Optional[List["Transaction"]]
|
|
85
|
-
receipts: Optional[List["Receipt"]]
|
|
86
|
-
|
|
87
|
-
def __init__(
|
|
88
|
-
self,
|
|
89
|
-
*,
|
|
90
|
-
chain_id: int,
|
|
91
|
-
previous_block_hash: bytes,
|
|
92
|
-
previous_block: Optional["Block"],
|
|
93
|
-
number: int,
|
|
94
|
-
timestamp: Optional[int],
|
|
95
|
-
accounts_hash: Optional[bytes],
|
|
96
|
-
transactions_total_fees: Optional[int],
|
|
97
|
-
transactions_hash: Optional[bytes],
|
|
98
|
-
receipts_hash: Optional[bytes],
|
|
99
|
-
delay_difficulty: Optional[int],
|
|
100
|
-
validator_public_key: Optional[bytes],
|
|
101
|
-
nonce: Optional[int] = None,
|
|
102
|
-
signature: Optional[bytes] = None,
|
|
103
|
-
atom_hash: Optional[bytes] = None,
|
|
104
|
-
body_hash: Optional[bytes] = None,
|
|
105
|
-
accounts: Optional["Trie"] = None,
|
|
106
|
-
transactions: Optional[List["Transaction"]] = None,
|
|
107
|
-
receipts: Optional[List["Receipt"]] = None,
|
|
108
|
-
) -> None:
|
|
109
|
-
self.atom_hash = atom_hash
|
|
110
|
-
self.chain_id = chain_id
|
|
111
|
-
self.previous_block_hash = previous_block_hash
|
|
112
|
-
self.previous_block = previous_block
|
|
113
|
-
self.number = number
|
|
114
|
-
self.timestamp = timestamp
|
|
115
|
-
self.accounts_hash = accounts_hash
|
|
116
|
-
self.transactions_total_fees = transactions_total_fees
|
|
117
|
-
self.transactions_hash = transactions_hash
|
|
118
|
-
self.receipts_hash = receipts_hash
|
|
119
|
-
self.delay_difficulty = delay_difficulty
|
|
120
|
-
self.validator_public_key = validator_public_key
|
|
121
|
-
self.nonce = nonce
|
|
122
|
-
self.body_hash = body_hash
|
|
123
|
-
self.signature = signature
|
|
124
|
-
self.accounts = accounts
|
|
125
|
-
self.transactions = transactions
|
|
126
|
-
self.receipts = receipts
|
|
127
|
-
|
|
128
|
-
def to_atom(self) -> Tuple[bytes, List[Atom]]:
|
|
129
|
-
# Build body details as direct byte atoms, in defined order
|
|
130
|
-
detail_payloads: List[bytes] = []
|
|
131
|
-
block_atoms: List[Atom] = []
|
|
132
|
-
|
|
133
|
-
def _emit(detail_bytes: bytes) -> None:
|
|
134
|
-
detail_payloads.append(detail_bytes)
|
|
135
|
-
|
|
136
|
-
# 0: chain
|
|
137
|
-
_emit(_int_to_be_bytes(self.chain_id))
|
|
138
|
-
# 1: previous_block_hash
|
|
139
|
-
_emit(self.previous_block_hash)
|
|
140
|
-
# 2: number
|
|
141
|
-
_emit(_int_to_be_bytes(self.number))
|
|
142
|
-
# 3: timestamp
|
|
143
|
-
_emit(_int_to_be_bytes(self.timestamp))
|
|
144
|
-
# 4: accounts_hash
|
|
145
|
-
_emit(self.accounts_hash or b"")
|
|
146
|
-
# 5: transactions_total_fees
|
|
147
|
-
_emit(_int_to_be_bytes(self.transactions_total_fees))
|
|
148
|
-
# 6: transactions_hash
|
|
149
|
-
_emit(self.transactions_hash or b"")
|
|
150
|
-
# 7: receipts_hash
|
|
151
|
-
_emit(self.receipts_hash or b"")
|
|
152
|
-
# 8: delay_difficulty
|
|
153
|
-
_emit(_int_to_be_bytes(self.delay_difficulty))
|
|
154
|
-
# 9: validator_public_key
|
|
155
|
-
_emit(self.validator_public_key or b"")
|
|
156
|
-
# 10: nonce
|
|
157
|
-
_emit(_int_to_be_bytes(self.nonce))
|
|
158
|
-
|
|
159
|
-
# Build body list chain directly from detail atoms
|
|
160
|
-
body_head = ZERO32
|
|
161
|
-
detail_atoms: List[Atom] = []
|
|
162
|
-
for payload in reversed(detail_payloads):
|
|
163
|
-
atom = Atom(data=payload, next_id=body_head, kind=AtomKind.BYTES)
|
|
164
|
-
detail_atoms.append(atom)
|
|
165
|
-
body_head = atom.object_id()
|
|
166
|
-
detail_atoms.reverse()
|
|
167
|
-
|
|
168
|
-
block_atoms.extend(detail_atoms)
|
|
169
|
-
|
|
170
|
-
body_list_atom = Atom(data=body_head, kind=AtomKind.LIST)
|
|
171
|
-
self.body_hash = body_list_atom.object_id()
|
|
172
|
-
|
|
173
|
-
# Signature atom links to body list atom; type atom links to signature atom
|
|
174
|
-
sig_atom = Atom(
|
|
175
|
-
data=bytes(self.signature or b""),
|
|
176
|
-
next_id=self.body_hash,
|
|
177
|
-
kind=AtomKind.BYTES,
|
|
178
|
-
)
|
|
179
|
-
type_atom = Atom(data=b"block", next_id=sig_atom.object_id(), kind=AtomKind.SYMBOL)
|
|
180
|
-
|
|
181
|
-
block_atoms.append(body_list_atom)
|
|
182
|
-
block_atoms.append(sig_atom)
|
|
183
|
-
block_atoms.append(type_atom)
|
|
184
|
-
|
|
185
|
-
self.atom_hash = type_atom.object_id()
|
|
186
|
-
return self.atom_hash, block_atoms
|
|
187
|
-
|
|
188
|
-
@classmethod
|
|
189
|
-
def from_atom(cls, node: Any, block_id: bytes) -> "Block":
|
|
190
|
-
|
|
191
|
-
block_header = node.get_atom_list_from_storage(block_id)
|
|
192
|
-
if block_header is None or len(block_header) != 3:
|
|
193
|
-
raise ValueError("malformed block atom chain")
|
|
194
|
-
type_atom, sig_atom, body_list_atom = block_header
|
|
195
|
-
|
|
196
|
-
if type_atom.kind is not AtomKind.SYMBOL or type_atom.data != b"block":
|
|
197
|
-
raise ValueError("not a block (type atom payload)")
|
|
198
|
-
if sig_atom.kind is not AtomKind.BYTES:
|
|
199
|
-
raise ValueError("malformed block (signature atom kind)")
|
|
200
|
-
if body_list_atom.kind is not AtomKind.LIST:
|
|
201
|
-
raise ValueError("malformed block (body list atom kind)")
|
|
202
|
-
if body_list_atom.next_id != ZERO32:
|
|
203
|
-
raise ValueError("malformed block (body list tail)")
|
|
204
|
-
|
|
205
|
-
detail_atoms = node.get_atom_list_from_storage(body_list_atom.data)
|
|
206
|
-
if detail_atoms is None:
|
|
207
|
-
raise ValueError("missing block body list nodes")
|
|
208
|
-
|
|
209
|
-
if len(detail_atoms) != 11:
|
|
210
|
-
raise ValueError("block body must contain exactly 11 detail entries")
|
|
211
|
-
|
|
212
|
-
detail_values: List[bytes] = []
|
|
213
|
-
for detail_atom in detail_atoms:
|
|
214
|
-
if detail_atom.kind is not AtomKind.BYTES:
|
|
215
|
-
raise ValueError("block body detail atoms must be bytes")
|
|
216
|
-
detail_values.append(detail_atom.data)
|
|
217
|
-
|
|
218
|
-
(
|
|
219
|
-
chain_bytes,
|
|
220
|
-
prev_bytes,
|
|
221
|
-
number_bytes,
|
|
222
|
-
timestamp_bytes,
|
|
223
|
-
accounts_bytes,
|
|
224
|
-
fees_bytes,
|
|
225
|
-
transactions_bytes,
|
|
226
|
-
receipts_bytes,
|
|
227
|
-
delay_diff_bytes,
|
|
228
|
-
validator_bytes,
|
|
229
|
-
nonce_bytes,
|
|
230
|
-
) = detail_values
|
|
231
|
-
|
|
232
|
-
return cls(
|
|
233
|
-
chain_id=_be_bytes_to_int(chain_bytes),
|
|
234
|
-
previous_block_hash=prev_bytes or ZERO32,
|
|
235
|
-
previous_block=None,
|
|
236
|
-
number=_be_bytes_to_int(number_bytes),
|
|
237
|
-
timestamp=_be_bytes_to_int(timestamp_bytes),
|
|
238
|
-
accounts_hash=accounts_bytes or None,
|
|
239
|
-
transactions_total_fees=_be_bytes_to_int(fees_bytes),
|
|
240
|
-
transactions_hash=transactions_bytes or None,
|
|
241
|
-
receipts_hash=receipts_bytes or None,
|
|
242
|
-
delay_difficulty=_be_bytes_to_int(delay_diff_bytes),
|
|
243
|
-
validator_public_key=validator_bytes or None,
|
|
244
|
-
nonce=_be_bytes_to_int(nonce_bytes),
|
|
245
|
-
signature=sig_atom.data if sig_atom is not None else None,
|
|
246
|
-
atom_hash=block_id,
|
|
247
|
-
body_hash=body_list_atom.object_id(),
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
def validate(self, storage_get: Callable[[bytes], Optional[Atom]]) -> bool:
|
|
251
|
-
"""Validate this block against storage.
|
|
252
|
-
|
|
253
|
-
Checks:
|
|
254
|
-
- Signature: signature must verify over the body list id using the
|
|
255
|
-
validator's public key.
|
|
256
|
-
- Timestamp monotonicity: if previous block exists (not ZERO32), this
|
|
257
|
-
block's timestamp must be >= previous.timestamp + 1.
|
|
258
|
-
"""
|
|
259
|
-
# Unverifiable if critical fields are missing
|
|
260
|
-
if not self.body_hash:
|
|
261
|
-
return False
|
|
262
|
-
if not self.signature:
|
|
263
|
-
return False
|
|
264
|
-
if not self.validator_public_key:
|
|
265
|
-
return False
|
|
266
|
-
if self.timestamp is None:
|
|
267
|
-
return False
|
|
268
|
-
|
|
269
|
-
# 1) Signature check over body hash
|
|
270
|
-
try:
|
|
271
|
-
pub = Ed25519PublicKey.from_public_bytes(bytes(self.validator_public_key))
|
|
272
|
-
pub.verify(self.signature, self.body_hash)
|
|
273
|
-
except InvalidSignature as e:
|
|
274
|
-
raise ValueError("invalid signature") from e
|
|
275
|
-
|
|
276
|
-
# 2) Timestamp monotonicity against previous block
|
|
277
|
-
prev_ts: Optional[int] = None
|
|
278
|
-
prev_hash = self.previous_block_hash or ZERO32
|
|
279
|
-
|
|
280
|
-
if self.previous_block is not None:
|
|
281
|
-
prev_ts = int(self.previous_block.timestamp or 0)
|
|
282
|
-
prev_hash = self.previous_block.atom_hash or prev_hash or ZERO32
|
|
283
|
-
|
|
284
|
-
if prev_hash and prev_hash != ZERO32 and prev_ts is None:
|
|
285
|
-
# If previous block cannot be loaded, treat as unverifiable, not malicious
|
|
286
|
-
try:
|
|
287
|
-
prev = Block.from_atom(storage_get, prev_hash)
|
|
288
|
-
except Exception:
|
|
289
|
-
return False
|
|
290
|
-
prev_ts = int(prev.timestamp or 0)
|
|
291
|
-
|
|
292
|
-
if prev_hash and prev_hash != ZERO32:
|
|
293
|
-
if prev_ts is None:
|
|
294
|
-
return False
|
|
295
|
-
cur_ts = int(self.timestamp or 0)
|
|
296
|
-
if cur_ts < prev_ts + 1:
|
|
297
|
-
raise ValueError("timestamp must be at least prev+1")
|
|
298
|
-
|
|
299
|
-
return True
|
|
300
|
-
|
|
301
|
-
@staticmethod
|
|
302
|
-
def _leading_zero_bits(buf: bytes) -> int:
|
|
303
|
-
"""Return the number of leading zero bits in the provided buffer."""
|
|
304
|
-
zeros = 0
|
|
305
|
-
for byte in buf:
|
|
306
|
-
if byte == 0:
|
|
307
|
-
zeros += 8
|
|
308
|
-
continue
|
|
309
|
-
zeros += 8 - int(byte).bit_length()
|
|
310
|
-
break
|
|
311
|
-
return zeros
|
|
312
|
-
|
|
313
|
-
@staticmethod
|
|
314
|
-
def calculate_delay_difficulty(
|
|
315
|
-
*,
|
|
316
|
-
previous_timestamp: Optional[int],
|
|
317
|
-
current_timestamp: Optional[int],
|
|
318
|
-
previous_difficulty: Optional[int],
|
|
319
|
-
target_spacing: int = 2,
|
|
320
|
-
) -> int:
|
|
321
|
-
"""
|
|
322
|
-
Adjust the delay difficulty based on how quickly the previous block was produced.
|
|
323
|
-
|
|
324
|
-
The previous block difficulty is increased if the spacing is below the target,
|
|
325
|
-
decreased if above, and returned unchanged when the target spacing is met.
|
|
326
|
-
"""
|
|
327
|
-
base_difficulty = max(1, int(previous_difficulty or 1))
|
|
328
|
-
if previous_timestamp is None or current_timestamp is None:
|
|
329
|
-
return base_difficulty
|
|
330
|
-
|
|
331
|
-
spacing = max(0, int(current_timestamp) - int(previous_timestamp))
|
|
332
|
-
if spacing <= 1:
|
|
333
|
-
adjusted = base_difficulty * 1.618
|
|
334
|
-
elif spacing == target_spacing:
|
|
335
|
-
adjusted = float(base_difficulty)
|
|
336
|
-
elif spacing > target_spacing:
|
|
337
|
-
adjusted = base_difficulty * 0.618
|
|
338
|
-
else:
|
|
339
|
-
adjusted = float(base_difficulty)
|
|
340
|
-
|
|
341
|
-
return max(1, int(round(adjusted)))
|
|
342
|
-
|
|
343
|
-
def generate_nonce(
|
|
344
|
-
self,
|
|
345
|
-
*,
|
|
346
|
-
difficulty: int,
|
|
347
|
-
) -> int:
|
|
348
|
-
"""
|
|
349
|
-
Find a nonce that yields a block hash with the required leading zero bits.
|
|
350
|
-
|
|
351
|
-
The search starts from the current nonce and iterates until the target
|
|
352
|
-
difficulty is met.
|
|
353
|
-
"""
|
|
354
|
-
target = max(1, int(difficulty))
|
|
355
|
-
start = int(self.nonce or 0)
|
|
356
|
-
nonce = start
|
|
357
|
-
while True:
|
|
358
|
-
self.nonce = nonce
|
|
359
|
-
block_hash, _ = self.to_atom()
|
|
360
|
-
leading_zeros = self._leading_zero_bits(block_hash)
|
|
361
|
-
if leading_zeros >= target:
|
|
362
|
-
self.atom_hash = block_hash
|
|
363
|
-
return nonce
|
|
364
|
-
nonce += 1
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# chain.py
|
|
2
|
-
from typing import Callable, Dict, Optional
|
|
3
|
-
from .block import Block
|
|
4
|
-
from ...storage.models.atom import ZERO32, Atom
|
|
5
|
-
|
|
6
|
-
class Chain:
|
|
7
|
-
def __init__(self, head_block: Block):
|
|
8
|
-
self.head_block = head_block
|
|
9
|
-
self.validated_upto_block = None
|
|
10
|
-
# Root (genesis) hash for this chain; set by validation setup when known
|
|
11
|
-
self.root: Optional[bytes] = None
|
|
12
|
-
# Fork position: the head hash of the default/current fork for this chain
|
|
13
|
-
self.fork_position: Optional[bytes] = getattr(head_block, "atom_hash", None)
|
|
14
|
-
# Mark the first malicious block encountered during validation; None means not found
|
|
15
|
-
self.malicious_block_hash: Optional[bytes] = None
|
|
16
|
-
|
|
17
|
-
def validate(self, storage_get: Callable[[bytes], Atom]) -> Block:
|
|
18
|
-
"""Validate the chain from head to genesis and return the root block.
|
|
19
|
-
|
|
20
|
-
Incorporates per-block validation (signature on body and timestamp
|
|
21
|
-
monotonicity). Uses a simple cache to avoid duplicate Atom fetches and
|
|
22
|
-
duplicate block decoding during the backward walk.
|
|
23
|
-
"""
|
|
24
|
-
# Atom and Block caches for this validation pass
|
|
25
|
-
atom_cache: Dict[bytes, Optional[Atom]] = {}
|
|
26
|
-
block_cache: Dict[bytes, Block] = {}
|
|
27
|
-
|
|
28
|
-
def get_cached(k: bytes) -> Optional[Atom]:
|
|
29
|
-
if k in atom_cache:
|
|
30
|
-
return atom_cache[k]
|
|
31
|
-
a = storage_get(k)
|
|
32
|
-
atom_cache[k] = a
|
|
33
|
-
return a
|
|
34
|
-
|
|
35
|
-
def load_block(bid: bytes) -> Block:
|
|
36
|
-
if bid in block_cache:
|
|
37
|
-
return block_cache[bid]
|
|
38
|
-
b = Block.from_atom(get_cached, bid)
|
|
39
|
-
block_cache[bid] = b
|
|
40
|
-
return b
|
|
41
|
-
|
|
42
|
-
blk = self.head_block
|
|
43
|
-
# Ensure head is in cache if it has a hash
|
|
44
|
-
if getattr(blk, "atom_hash", None):
|
|
45
|
-
block_cache[blk.atom_hash] = blk # type: ignore[attr-defined]
|
|
46
|
-
|
|
47
|
-
# Walk back, validating each block
|
|
48
|
-
while True:
|
|
49
|
-
# Validate current block (signature over body, timestamp rule)
|
|
50
|
-
try:
|
|
51
|
-
blk.validate(get_cached) # may decode previous but uses cached atoms
|
|
52
|
-
except Exception:
|
|
53
|
-
# record first failure point then propagate
|
|
54
|
-
self.malicious_block_hash = getattr(blk, "atom_hash", None)
|
|
55
|
-
raise
|
|
56
|
-
|
|
57
|
-
prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
|
|
58
|
-
if prev_hash == ZERO32:
|
|
59
|
-
break
|
|
60
|
-
# Move to previous block using cache-aware loader
|
|
61
|
-
prev_blk = load_block(prev_hash)
|
|
62
|
-
blk.previous_block = prev_blk # cache the object for any downstream use
|
|
63
|
-
blk = prev_blk
|
|
64
|
-
|
|
65
|
-
self.validated_upto_block = blk
|
|
66
|
-
return blk
|
astreum/consensus/models/fork.py
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Optional, Set, Any, Callable, Dict
|
|
4
|
-
from .block import Block
|
|
5
|
-
from ...storage.models.atom import ZERO32, Atom
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Fork:
|
|
9
|
-
"""A branch head within a Chain (same root).
|
|
10
|
-
|
|
11
|
-
- head: current tip block id (bytes)
|
|
12
|
-
- peers: identifiers (e.g., peer pubkey objects) following this head
|
|
13
|
-
- root: genesis block id for this chain (optional)
|
|
14
|
-
- validated_upto: earliest verified ancestor (optional)
|
|
15
|
-
- chain_fork_position: the chain's fork anchor relevant to this fork
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
def __init__(
|
|
19
|
-
self,
|
|
20
|
-
head: bytes,
|
|
21
|
-
) -> None:
|
|
22
|
-
self.head: bytes = head
|
|
23
|
-
self.peers: Set[Any] = set()
|
|
24
|
-
self.root: Optional[bytes] = None
|
|
25
|
-
self.validated_upto: Optional[bytes] = None
|
|
26
|
-
self.chain_fork_position: Optional[bytes] = None
|
|
27
|
-
# Mark the first block found malicious during validation; None means not found
|
|
28
|
-
self.malicious_block_hash: Optional[bytes] = None
|
|
29
|
-
|
|
30
|
-
def add_peer(self, peer_id: Any) -> None:
|
|
31
|
-
self.peers.add(peer_id)
|
|
32
|
-
|
|
33
|
-
def remove_peer(self, peer_id: Any) -> None:
|
|
34
|
-
self.peers.discard(peer_id)
|
|
35
|
-
|
|
36
|
-
def validate(
|
|
37
|
-
self,
|
|
38
|
-
storage_get: Callable[[bytes], Optional[object]],
|
|
39
|
-
stop_heads: Optional[Set[bytes]] = None,
|
|
40
|
-
) -> bool:
|
|
41
|
-
"""Validate only up to the chain fork position, not genesis.
|
|
42
|
-
|
|
43
|
-
Returns True if self.head descends from self.chain_fork_position (or if
|
|
44
|
-
chain_fork_position is None/equals head), and updates validated_upto to
|
|
45
|
-
that anchor. If stop_heads is provided, returns True early if ancestry
|
|
46
|
-
reaches any of those heads, setting validated_upto to the matched head.
|
|
47
|
-
Returns False if ancestry cannot be confirmed.
|
|
48
|
-
"""
|
|
49
|
-
if self.chain_fork_position is None or self.chain_fork_position == self.head:
|
|
50
|
-
self.validated_upto = self.head
|
|
51
|
-
return True
|
|
52
|
-
# Caches to avoid double fetching/decoding
|
|
53
|
-
atom_cache: Dict[bytes, Optional[Atom]] = {}
|
|
54
|
-
block_cache: Dict[bytes, Block] = {}
|
|
55
|
-
|
|
56
|
-
def get_cached(k: bytes) -> Optional[Atom]:
|
|
57
|
-
if k in atom_cache:
|
|
58
|
-
return atom_cache[k]
|
|
59
|
-
a = storage_get(k) # type: ignore[call-arg]
|
|
60
|
-
atom_cache[k] = a # may be None if missing
|
|
61
|
-
return a
|
|
62
|
-
|
|
63
|
-
def load_block(bid: bytes) -> Optional[Block]:
|
|
64
|
-
if bid in block_cache:
|
|
65
|
-
return block_cache[bid]
|
|
66
|
-
try:
|
|
67
|
-
b = Block.from_atom(get_cached, bid)
|
|
68
|
-
except Exception:
|
|
69
|
-
return None
|
|
70
|
-
block_cache[bid] = b
|
|
71
|
-
return b
|
|
72
|
-
|
|
73
|
-
blk = load_block(self.head)
|
|
74
|
-
if blk is None:
|
|
75
|
-
# Missing head data: unverifiable, not malicious
|
|
76
|
-
return False
|
|
77
|
-
# Walk up to fork anchor, validating each block signature + timestamp
|
|
78
|
-
while True:
|
|
79
|
-
try:
|
|
80
|
-
blk.validate(get_cached) # type: ignore[arg-type]
|
|
81
|
-
except Exception:
|
|
82
|
-
# mark the first failure point
|
|
83
|
-
self.malicious_block_hash = blk.atom_hash
|
|
84
|
-
return False
|
|
85
|
-
|
|
86
|
-
# Early-exit if we met another known fork head
|
|
87
|
-
if stop_heads and blk.atom_hash in stop_heads:
|
|
88
|
-
self.validated_upto = blk.atom_hash
|
|
89
|
-
return True
|
|
90
|
-
|
|
91
|
-
if blk.atom_hash == self.chain_fork_position:
|
|
92
|
-
self.validated_upto = blk.atom_hash
|
|
93
|
-
return True
|
|
94
|
-
|
|
95
|
-
prev_hash = blk.previous_block_hash if hasattr(blk, "previous_block_hash") else ZERO32
|
|
96
|
-
nxt = load_block(prev_hash)
|
|
97
|
-
if nxt is None:
|
|
98
|
-
return False
|
|
99
|
-
blk.previous_block = nxt # cache for future use
|
|
100
|
-
blk = nxt
|
astreum/consensus/setup.py
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import threading
|
|
4
|
-
from queue import Queue
|
|
5
|
-
from typing import Any, Optional
|
|
6
|
-
|
|
7
|
-
from .validator import current_validator # re-exported for compatibility
|
|
8
|
-
from .workers import (
|
|
9
|
-
make_discovery_worker,
|
|
10
|
-
make_validation_worker,
|
|
11
|
-
make_verify_worker,
|
|
12
|
-
)
|
|
13
|
-
from ..utils.bytes import hex_to_bytes
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def consensus_setup(node: Any, config: Optional[dict] = None) -> None:
|
|
17
|
-
config = config or {}
|
|
18
|
-
node.logger.info("Setting up node consensus")
|
|
19
|
-
|
|
20
|
-
# Shared state
|
|
21
|
-
node.validation_lock = getattr(node, "validation_lock", threading.RLock())
|
|
22
|
-
|
|
23
|
-
# Public maps per your spec
|
|
24
|
-
# - chains: Dict[root, Chain]
|
|
25
|
-
# - forks: Dict[head, Fork]
|
|
26
|
-
node.chains = getattr(node, "chains", {})
|
|
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
|
-
)
|
|
33
|
-
|
|
34
|
-
latest_block_hex = config.get("latest_block_hash")
|
|
35
|
-
if latest_block_hex is not None:
|
|
36
|
-
node.latest_block_hash = hex_to_bytes(latest_block_hex, expected_length=32)
|
|
37
|
-
|
|
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
|
-
)
|
|
44
|
-
|
|
45
|
-
# Pending transactions queue (hash-only entries)
|
|
46
|
-
node._validation_transaction_queue = getattr(
|
|
47
|
-
node, "_validation_transaction_queue", Queue()
|
|
48
|
-
)
|
|
49
|
-
# Single work queue of grouped items: (latest_block_hash, set(peer_ids))
|
|
50
|
-
node._validation_verify_queue = getattr(
|
|
51
|
-
node, "_validation_verify_queue", Queue()
|
|
52
|
-
)
|
|
53
|
-
node._validation_stop_event = getattr(
|
|
54
|
-
node, "_validation_stop_event", threading.Event()
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
def enqueue_transaction_hash(tx_hash: bytes) -> None:
|
|
58
|
-
"""Schedule a transaction hash for validation processing."""
|
|
59
|
-
if not isinstance(tx_hash, (bytes, bytearray)):
|
|
60
|
-
raise TypeError("transaction hash must be bytes-like")
|
|
61
|
-
node._validation_transaction_queue.put(bytes(tx_hash))
|
|
62
|
-
|
|
63
|
-
node.enqueue_transaction_hash = enqueue_transaction_hash
|
|
64
|
-
|
|
65
|
-
verify_worker = make_verify_worker(node)
|
|
66
|
-
validation_worker = make_validation_worker(node)
|
|
67
|
-
|
|
68
|
-
# Start workers as daemons
|
|
69
|
-
discovery_worker = make_discovery_worker(node)
|
|
70
|
-
node.consensus_discovery_thread = threading.Thread(
|
|
71
|
-
target=discovery_worker, daemon=True, name="consensus-discovery"
|
|
72
|
-
)
|
|
73
|
-
node.consensus_verify_thread = threading.Thread(
|
|
74
|
-
target=verify_worker, daemon=True, name="consensus-verify"
|
|
75
|
-
)
|
|
76
|
-
node.consensus_validation_thread = threading.Thread(
|
|
77
|
-
target=validation_worker, daemon=True, name="consensus-validation"
|
|
78
|
-
)
|
|
79
|
-
node.consensus_discovery_thread.start()
|
|
80
|
-
node.logger.info("Started consensus discovery thread (%s)", node.consensus_discovery_thread.name)
|
|
81
|
-
node.consensus_verify_thread.start()
|
|
82
|
-
node.logger.info("Started consensus verify thread (%s)", node.consensus_verify_thread.name)
|
|
83
|
-
node.logger.info("Consensus setup ready")
|
astreum/consensus/start.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
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
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Worker thread factories for the consensus subsystem.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from .discovery import make_discovery_worker
|
|
6
|
-
from .validation import make_validation_worker
|
|
7
|
-
from .verify import make_verify_worker
|
|
8
|
-
|
|
9
|
-
__all__ = ["make_discovery_worker", "make_verify_worker", "make_validation_worker"]
|