astreum 0.3.16__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.
Files changed (60) hide show
  1. astreum/__init__.py +1 -2
  2. astreum/communication/__init__.py +15 -11
  3. astreum/communication/difficulty.py +39 -0
  4. astreum/communication/disconnect.py +57 -0
  5. astreum/communication/handlers/handshake.py +105 -62
  6. astreum/communication/handlers/object_request.py +179 -149
  7. astreum/communication/handlers/object_response.py +7 -1
  8. astreum/communication/handlers/ping.py +9 -0
  9. astreum/communication/handlers/route_request.py +7 -1
  10. astreum/communication/handlers/route_response.py +7 -1
  11. astreum/communication/incoming_queue.py +96 -0
  12. astreum/communication/message_pow.py +36 -0
  13. astreum/communication/models/peer.py +4 -0
  14. astreum/communication/models/ping.py +27 -6
  15. astreum/communication/models/route.py +4 -0
  16. astreum/communication/{start.py → node.py} +10 -11
  17. astreum/communication/outgoing_queue.py +108 -0
  18. astreum/communication/processors/incoming.py +110 -37
  19. astreum/communication/processors/outgoing.py +35 -2
  20. astreum/communication/processors/peer.py +133 -58
  21. astreum/communication/setup.py +272 -113
  22. astreum/communication/util.py +14 -0
  23. astreum/node.py +99 -92
  24. astreum/storage/actions/get.py +79 -48
  25. astreum/storage/actions/set.py +171 -156
  26. astreum/storage/providers.py +24 -0
  27. astreum/storage/setup.py +23 -22
  28. astreum/utils/config.py +234 -45
  29. astreum/utils/logging.py +1 -1
  30. astreum/{consensus → validation}/__init__.py +0 -4
  31. astreum/validation/constants.py +2 -0
  32. astreum/{consensus → validation}/genesis.py +4 -6
  33. astreum/validation/models/block.py +544 -0
  34. astreum/validation/models/fork.py +511 -0
  35. astreum/{consensus → validation}/models/receipt.py +17 -4
  36. astreum/{consensus → validation}/models/transaction.py +45 -3
  37. astreum/validation/node.py +190 -0
  38. astreum/{consensus → validation}/validator.py +1 -1
  39. astreum/validation/workers/__init__.py +8 -0
  40. astreum/{consensus → validation}/workers/validation.py +360 -333
  41. astreum/verification/__init__.py +4 -0
  42. astreum/{consensus/workers/discovery.py → verification/discover.py} +1 -1
  43. astreum/verification/node.py +61 -0
  44. astreum/verification/worker.py +183 -0
  45. {astreum-0.3.16.dist-info → astreum-0.3.46.dist-info}/METADATA +43 -9
  46. astreum-0.3.46.dist-info/RECORD +79 -0
  47. astreum/consensus/models/block.py +0 -364
  48. astreum/consensus/models/chain.py +0 -66
  49. astreum/consensus/models/fork.py +0 -100
  50. astreum/consensus/setup.py +0 -83
  51. astreum/consensus/start.py +0 -67
  52. astreum/consensus/workers/__init__.py +0 -9
  53. astreum/consensus/workers/verify.py +0 -90
  54. astreum-0.3.16.dist-info/RECORD +0 -72
  55. /astreum/{consensus → validation}/models/__init__.py +0 -0
  56. /astreum/{consensus → validation}/models/account.py +0 -0
  57. /astreum/{consensus → validation}/models/accounts.py +0 -0
  58. {astreum-0.3.16.dist-info → astreum-0.3.46.dist-info}/WHEEL +0 -0
  59. {astreum-0.3.16.dist-info → astreum-0.3.46.dist-info}/licenses/LICENSE +0 -0
  60. {astreum-0.3.16.dist-info → astreum-0.3.46.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,544 @@
1
+
2
+ from typing import Any, List, Optional, Tuple, TYPE_CHECKING
3
+
4
+ from ...storage.models.atom import Atom, AtomKind, ZERO32, bytes_list_to_atoms
5
+ from .accounts import Accounts
6
+ from .receipt import Receipt
7
+ from .transaction import apply_transaction
8
+
9
+ if TYPE_CHECKING:
10
+ from ...storage.models.trie import Trie
11
+ from .transaction import Transaction
12
+ from .receipt import Receipt
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--> version_atom --next--> signature_atom --next--> body_list_atom --next--> ZERO32
36
+ where: type_atom = Atom(kind=AtomKind.SYMBOL, data=b"block")
37
+ version_atom = Atom(kind=AtomKind.BYTES, data=b"\x01")
38
+ signature_atom = Atom(kind=AtomKind.BYTES, data=<signature-bytes>)
39
+ body_list_atom = Atom(kind=AtomKind.LIST, data=<body_head_id>)
40
+
41
+ Details order in body_list:
42
+ 0: chain (byte)
43
+ 1: previous_block_hash (bytes)
44
+ 2: number (int -> big-endian bytes)
45
+ 3: timestamp (int -> big-endian bytes)
46
+ 4: accounts_hash (bytes)
47
+ 5: transactions_total_fees (int -> big-endian bytes)
48
+ 6: transactions_hash (bytes)
49
+ 7: receipts_hash (bytes)
50
+ 8: delay_difficulty (int -> big-endian bytes)
51
+ 9: validator_public_key_bytes (bytes)
52
+ 10: nonce (int -> big-endian bytes)
53
+
54
+ Notes:
55
+ - "body tree" is represented here by the body_list id (self.body_hash), not
56
+ embedded again as a field to avoid circular references.
57
+ - "signature" is a field on the class but is not required for validation
58
+ navigation; include it in the instance but it is not encoded in atoms
59
+ unless explicitly provided via details extension in the future.
60
+ """
61
+
62
+ # essential identifiers
63
+ version: int
64
+ atom_hash: Optional[bytes]
65
+ chain_id: int
66
+ previous_block_hash: bytes
67
+ previous_block: Optional["Block"]
68
+
69
+ # block details
70
+ number: int
71
+ timestamp: Optional[int]
72
+ accounts_hash: Optional[bytes]
73
+ transactions_total_fees: Optional[int]
74
+ transactions_hash: Optional[bytes]
75
+ receipts_hash: Optional[bytes]
76
+ delay_difficulty: Optional[int]
77
+ validator_public_key_bytes: Optional[bytes]
78
+ nonce: Optional[int]
79
+
80
+ # additional
81
+ body_hash: Optional[bytes]
82
+ signature: Optional[bytes]
83
+
84
+ # structures
85
+ accounts: Optional["Trie"]
86
+ transactions: Optional[List["Transaction"]]
87
+ receipts: Optional[List["Receipt"]]
88
+
89
+ def __init__(
90
+ self,
91
+ *,
92
+ chain_id: int,
93
+ previous_block_hash: bytes,
94
+ previous_block: Optional["Block"],
95
+ number: int,
96
+ timestamp: Optional[int],
97
+ accounts_hash: Optional[bytes],
98
+ transactions_total_fees: Optional[int],
99
+ transactions_hash: Optional[bytes],
100
+ receipts_hash: Optional[bytes],
101
+ delay_difficulty: Optional[int],
102
+ validator_public_key_bytes: Optional[bytes],
103
+ version: int = 1,
104
+ nonce: Optional[int] = None,
105
+ signature: Optional[bytes] = None,
106
+ atom_hash: Optional[bytes] = None,
107
+ body_hash: Optional[bytes] = None,
108
+ accounts: Optional["Trie"] = None,
109
+ transactions: Optional[List["Transaction"]] = None,
110
+ receipts: Optional[List["Receipt"]] = None,
111
+ ) -> None:
112
+ self.version = int(version)
113
+ self.atom_hash = atom_hash
114
+ self.chain_id = chain_id
115
+ self.previous_block_hash = previous_block_hash
116
+ self.previous_block = previous_block
117
+ self.number = number
118
+ self.timestamp = timestamp
119
+ self.accounts_hash = accounts_hash
120
+ self.transactions_total_fees = transactions_total_fees
121
+ self.transactions_hash = transactions_hash
122
+ self.receipts_hash = receipts_hash
123
+ self.delay_difficulty = delay_difficulty
124
+ self.validator_public_key_bytes = (
125
+ bytes(validator_public_key_bytes) if validator_public_key_bytes else None
126
+ )
127
+ self.nonce = nonce
128
+ self.body_hash = body_hash
129
+ self.signature = signature
130
+ self.accounts = accounts
131
+ self.transactions = transactions
132
+ self.receipts = receipts
133
+
134
+ def to_atom(self) -> Tuple[bytes, List[Atom]]:
135
+ # Build body details as direct byte atoms, in defined order
136
+ detail_payloads: List[bytes] = []
137
+ block_atoms: List[Atom] = []
138
+
139
+ def _emit(detail_bytes: bytes) -> None:
140
+ detail_payloads.append(detail_bytes)
141
+
142
+ # 0: chain
143
+ _emit(_int_to_be_bytes(self.chain_id))
144
+ # 1: previous_block_hash
145
+ _emit(self.previous_block_hash)
146
+ # 2: number
147
+ _emit(_int_to_be_bytes(self.number))
148
+ # 3: timestamp
149
+ _emit(_int_to_be_bytes(self.timestamp))
150
+ # 4: accounts_hash
151
+ _emit(self.accounts_hash or b"")
152
+ # 5: transactions_total_fees
153
+ _emit(_int_to_be_bytes(self.transactions_total_fees))
154
+ # 6: transactions_hash
155
+ _emit(self.transactions_hash or b"")
156
+ # 7: receipts_hash
157
+ _emit(self.receipts_hash or b"")
158
+ # 8: delay_difficulty
159
+ _emit(_int_to_be_bytes(self.delay_difficulty))
160
+ # 9: validator_public_key_bytes
161
+ _emit(self.validator_public_key_bytes or b"")
162
+ # 10: nonce
163
+ _emit(_int_to_be_bytes(self.nonce))
164
+
165
+ # Build body list chain directly from detail atoms
166
+ body_head = ZERO32
167
+ detail_atoms: List[Atom] = []
168
+ for payload in reversed(detail_payloads):
169
+ atom = Atom(data=payload, next_id=body_head, kind=AtomKind.BYTES)
170
+ detail_atoms.append(atom)
171
+ body_head = atom.object_id()
172
+ detail_atoms.reverse()
173
+
174
+ block_atoms.extend(detail_atoms)
175
+
176
+ body_list_atom = Atom(data=body_head, kind=AtomKind.LIST)
177
+ self.body_hash = body_list_atom.object_id()
178
+
179
+ # Signature atom links to body list atom; type atom links to signature atom
180
+ sig_atom = Atom(
181
+ data=bytes(self.signature or b""),
182
+ next_id=self.body_hash,
183
+ kind=AtomKind.BYTES,
184
+ )
185
+ version_atom = Atom(
186
+ data=_int_to_be_bytes(self.version),
187
+ next_id=sig_atom.object_id(),
188
+ kind=AtomKind.BYTES,
189
+ )
190
+ type_atom = Atom(
191
+ data=b"block",
192
+ next_id=version_atom.object_id(),
193
+ kind=AtomKind.SYMBOL,
194
+ )
195
+
196
+ block_atoms.append(body_list_atom)
197
+ block_atoms.append(sig_atom)
198
+ block_atoms.append(version_atom)
199
+ block_atoms.append(type_atom)
200
+
201
+ self.atom_hash = type_atom.object_id()
202
+ return self.atom_hash, block_atoms
203
+
204
+ @classmethod
205
+ def from_atom(cls, node: Any, block_id: bytes) -> "Block":
206
+
207
+ block_header = node.get_atom_list_from_storage(block_id)
208
+ if block_header is None or len(block_header) != 4:
209
+ raise ValueError("malformed block atom chain")
210
+ type_atom, version_atom, sig_atom, body_list_atom = block_header
211
+
212
+ if type_atom.kind is not AtomKind.SYMBOL or type_atom.data != b"block":
213
+ raise ValueError("not a block (type atom payload)")
214
+ if version_atom.kind is not AtomKind.BYTES:
215
+ raise ValueError("malformed block (version atom kind)")
216
+ version = _be_bytes_to_int(version_atom.data)
217
+ if version != 1:
218
+ raise ValueError("unsupported block version")
219
+ if sig_atom.kind is not AtomKind.BYTES:
220
+ raise ValueError("malformed block (signature atom kind)")
221
+ if body_list_atom.kind is not AtomKind.LIST:
222
+ raise ValueError("malformed block (body list atom kind)")
223
+ if body_list_atom.next_id != ZERO32:
224
+ raise ValueError("malformed block (body list tail)")
225
+
226
+ detail_atoms = node.get_atom_list_from_storage(body_list_atom.data)
227
+ if detail_atoms is None:
228
+ raise ValueError("missing block body list nodes")
229
+
230
+ if len(detail_atoms) != 11:
231
+ raise ValueError("block body must contain exactly 11 detail entries")
232
+
233
+ detail_values: List[bytes] = []
234
+ for detail_atom in detail_atoms:
235
+ if detail_atom.kind is not AtomKind.BYTES:
236
+ raise ValueError("block body detail atoms must be bytes")
237
+ detail_values.append(detail_atom.data)
238
+
239
+ (
240
+ chain_bytes,
241
+ prev_bytes,
242
+ number_bytes,
243
+ timestamp_bytes,
244
+ accounts_bytes,
245
+ fees_bytes,
246
+ transactions_bytes,
247
+ receipts_bytes,
248
+ delay_diff_bytes,
249
+ validator_bytes,
250
+ nonce_bytes,
251
+ ) = detail_values
252
+
253
+ return cls(
254
+ version=version,
255
+ chain_id=_be_bytes_to_int(chain_bytes),
256
+ previous_block_hash=prev_bytes or ZERO32,
257
+ previous_block=None,
258
+ number=_be_bytes_to_int(number_bytes),
259
+ timestamp=_be_bytes_to_int(timestamp_bytes),
260
+ accounts_hash=accounts_bytes or None,
261
+ transactions_total_fees=_be_bytes_to_int(fees_bytes),
262
+ transactions_hash=transactions_bytes or None,
263
+ receipts_hash=receipts_bytes or None,
264
+ delay_difficulty=_be_bytes_to_int(delay_diff_bytes),
265
+ validator_public_key_bytes=validator_bytes or None,
266
+ nonce=_be_bytes_to_int(nonce_bytes),
267
+ signature=sig_atom.data if sig_atom is not None else None,
268
+ atom_hash=block_id,
269
+ body_hash=body_list_atom.object_id(),
270
+ )
271
+
272
+ def verify(self, node: Any) -> bool:
273
+ """Verify receipts, transactions, and accounts hashes for this block."""
274
+ if node is None:
275
+ raise ValueError("node required for block verification")
276
+
277
+ logger = getattr(node, "logger", None)
278
+
279
+ def _hex(value: Optional[bytes]) -> str:
280
+ if isinstance(value, (bytes, bytearray)):
281
+ return value.hex()
282
+ return str(value)
283
+
284
+ def _log_debug(message: str, *args: object) -> None:
285
+ if logger:
286
+ logger.debug(message, *args)
287
+
288
+ def _log_warning(message: str, *args: object) -> None:
289
+ if logger:
290
+ logger.warning(message, *args)
291
+
292
+ _log_debug("Block verify start block=%s", _hex(self.atom_hash))
293
+
294
+ if self.transactions_hash is None:
295
+ _log_warning("Block verify missing transactions_hash block=%s", _hex(self.atom_hash))
296
+ return False
297
+ if self.receipts_hash is None:
298
+ _log_warning("Block verify missing receipts_hash block=%s", _hex(self.atom_hash))
299
+ return False
300
+ if self.accounts_hash is None:
301
+ _log_warning("Block verify missing accounts_hash block=%s", _hex(self.atom_hash))
302
+ return False
303
+
304
+ def _load_hash_list(head: bytes) -> Optional[List[bytes]]:
305
+ if head == ZERO32:
306
+ return []
307
+ atoms = node.get_atom_list_from_storage(head)
308
+ if atoms is None:
309
+ _log_warning("Block verify missing list atoms head=%s block=%s", _hex(head), _hex(self.atom_hash))
310
+ return None
311
+ for atom in atoms:
312
+ if atom.kind is not AtomKind.BYTES:
313
+ _log_warning("Block verify list atom kind mismatch head=%s block=%s", _hex(head), _hex(self.atom_hash))
314
+ return None
315
+ return [bytes(atom.data) for atom in atoms]
316
+
317
+ prev_hash = self.previous_block_hash or ZERO32
318
+ if prev_hash == ZERO32:
319
+ if self.transactions_hash != ZERO32:
320
+ _log_warning("Block verify genesis tx hash mismatch block=%s", _hex(self.atom_hash))
321
+ return False
322
+ if self.receipts_hash != ZERO32:
323
+ _log_warning("Block verify genesis receipts hash mismatch block=%s", _hex(self.atom_hash))
324
+ return False
325
+ if self.transactions_total_fees not in (0, None):
326
+ _log_warning("Block verify genesis fees mismatch block=%s", _hex(self.atom_hash))
327
+ return False
328
+ _log_debug("Block verify genesis passed block=%s", _hex(self.atom_hash))
329
+ return True
330
+
331
+ try:
332
+ prev_block = self.previous_block or Block.from_atom(node, prev_hash)
333
+ except Exception:
334
+ _log_warning("Block verify failed loading parent block=%s prev=%s", _hex(self.atom_hash), _hex(prev_hash))
335
+ return False
336
+
337
+ prev_accounts_hash = getattr(prev_block, "accounts_hash", None)
338
+ if not prev_accounts_hash:
339
+ _log_warning("Block verify missing parent accounts hash block=%s", _hex(self.atom_hash))
340
+ return False
341
+
342
+ tx_hashes = _load_hash_list(self.transactions_hash)
343
+ if tx_hashes is None:
344
+ _log_warning("Block verify failed loading tx list block=%s", _hex(self.atom_hash))
345
+ return False
346
+
347
+ expected_tx_head, _ = bytes_list_to_atoms(tx_hashes)
348
+ if expected_tx_head != (self.transactions_hash or ZERO32):
349
+ _log_warning(
350
+ "Block verify tx head mismatch block=%s expected=%s actual=%s",
351
+ _hex(self.atom_hash),
352
+ _hex(expected_tx_head),
353
+ _hex(self.transactions_hash),
354
+ )
355
+ return False
356
+
357
+ accounts_snapshot = Accounts(root_hash=prev_accounts_hash)
358
+ work_block = type("_WorkBlock", (), {})()
359
+ work_block.chain_id = self.chain_id
360
+ work_block.accounts = accounts_snapshot
361
+ work_block.transactions = []
362
+ work_block.receipts = []
363
+
364
+ total_fees = 0
365
+ for tx_hash in tx_hashes:
366
+ try:
367
+ total_fees += apply_transaction(node, work_block, tx_hash)
368
+ except Exception:
369
+ _log_warning(
370
+ "Block verify failed applying tx=%s block=%s",
371
+ _hex(tx_hash),
372
+ _hex(self.atom_hash),
373
+ )
374
+ return False
375
+
376
+ if self.transactions_total_fees is None:
377
+ _log_warning("Block verify missing total fees block=%s", _hex(self.atom_hash))
378
+ return False
379
+ if int(self.transactions_total_fees) != int(total_fees):
380
+ _log_warning(
381
+ "Block verify fees mismatch block=%s expected=%s actual=%s",
382
+ _hex(self.atom_hash),
383
+ total_fees,
384
+ self.transactions_total_fees,
385
+ )
386
+ return False
387
+
388
+ applied_transactions = list(work_block.transactions or [])
389
+ if len(applied_transactions) != len(tx_hashes):
390
+ _log_warning(
391
+ "Block verify tx count mismatch block=%s expected=%s actual=%s",
392
+ _hex(self.atom_hash),
393
+ len(tx_hashes),
394
+ len(applied_transactions),
395
+ )
396
+ return False
397
+
398
+ expected_receipts: List[Receipt] = list(work_block.receipts or [])
399
+ if len(expected_receipts) != len(applied_transactions):
400
+ _log_warning(
401
+ "Block verify receipt count mismatch block=%s expected=%s actual=%s",
402
+ _hex(self.atom_hash),
403
+ len(applied_transactions),
404
+ len(expected_receipts),
405
+ )
406
+ return False
407
+ expected_receipt_ids: List[bytes] = []
408
+ for receipt in expected_receipts:
409
+ receipt_id, _ = receipt.to_atom()
410
+ expected_receipt_ids.append(receipt_id)
411
+
412
+ expected_receipts_head, _ = bytes_list_to_atoms(expected_receipt_ids)
413
+ if expected_receipts_head != (self.receipts_hash or ZERO32):
414
+ _log_warning(
415
+ "Block verify receipts head mismatch block=%s expected=%s actual=%s",
416
+ _hex(self.atom_hash),
417
+ _hex(expected_receipts_head),
418
+ _hex(self.receipts_hash),
419
+ )
420
+ return False
421
+
422
+ stored_receipt_ids = _load_hash_list(self.receipts_hash)
423
+ if stored_receipt_ids is None:
424
+ _log_warning("Block verify failed loading receipts list block=%s", _hex(self.atom_hash))
425
+ return False
426
+ if stored_receipt_ids != expected_receipt_ids:
427
+ _log_warning("Block verify receipts list mismatch block=%s", _hex(self.atom_hash))
428
+ return False
429
+ for expected, stored_id in zip(expected_receipts, stored_receipt_ids):
430
+ try:
431
+ stored = Receipt.from_atom(node, stored_id)
432
+ except Exception:
433
+ _log_warning(
434
+ "Block verify failed loading receipt=%s block=%s",
435
+ _hex(stored_id),
436
+ _hex(self.atom_hash),
437
+ )
438
+ return False
439
+ if stored.transaction_hash != expected.transaction_hash:
440
+ _log_warning(
441
+ "Block verify receipt tx mismatch receipt=%s block=%s",
442
+ _hex(stored_id),
443
+ _hex(self.atom_hash),
444
+ )
445
+ return False
446
+ if stored.status != expected.status:
447
+ _log_warning(
448
+ "Block verify receipt status mismatch receipt=%s block=%s",
449
+ _hex(stored_id),
450
+ _hex(self.atom_hash),
451
+ )
452
+ return False
453
+ if stored.cost != expected.cost:
454
+ _log_warning(
455
+ "Block verify receipt cost mismatch receipt=%s block=%s",
456
+ _hex(stored_id),
457
+ _hex(self.atom_hash),
458
+ )
459
+ return False
460
+ if stored.logs_hash != expected.logs_hash:
461
+ _log_warning(
462
+ "Block verify receipt logs hash mismatch receipt=%s block=%s",
463
+ _hex(stored_id),
464
+ _hex(self.atom_hash),
465
+ )
466
+ return False
467
+
468
+ try:
469
+ accounts_snapshot.update_trie(node)
470
+ except Exception:
471
+ _log_warning("Block verify failed updating accounts trie block=%s", _hex(self.atom_hash))
472
+ return False
473
+ if accounts_snapshot.root_hash != self.accounts_hash:
474
+ _log_warning(
475
+ "Block verify accounts hash mismatch block=%s expected=%s actual=%s",
476
+ _hex(self.atom_hash),
477
+ _hex(accounts_snapshot.root_hash),
478
+ _hex(self.accounts_hash),
479
+ )
480
+ return False
481
+
482
+ _log_debug("Block verify success block=%s", _hex(self.atom_hash))
483
+ return True
484
+
485
+ @staticmethod
486
+ def _leading_zero_bits(buf: bytes) -> int:
487
+ """Return the number of leading zero bits in the provided buffer."""
488
+ zeros = 0
489
+ for byte in buf:
490
+ if byte == 0:
491
+ zeros += 8
492
+ continue
493
+ zeros += 8 - int(byte).bit_length()
494
+ break
495
+ return zeros
496
+
497
+ @staticmethod
498
+ def calculate_delay_difficulty(
499
+ *,
500
+ previous_timestamp: Optional[int],
501
+ current_timestamp: Optional[int],
502
+ previous_difficulty: Optional[int],
503
+ target_spacing: int = 2,
504
+ ) -> int:
505
+ """
506
+ Adjust the delay difficulty with linear steps relative to block spacing.
507
+
508
+ If blocks arrive too quickly (spacing <= 1), difficulty increases by one.
509
+ If blocks are slower than the target spacing, difficulty decreases by one,
510
+ and otherwise remains unchanged.
511
+ """
512
+ base_difficulty = max(1, int(previous_difficulty or 1))
513
+ if previous_timestamp is None or current_timestamp is None:
514
+ return base_difficulty
515
+
516
+ spacing = max(0, int(current_timestamp) - int(previous_timestamp))
517
+ if spacing <= 1:
518
+ return base_difficulty + 1
519
+ if spacing > target_spacing:
520
+ return max(1, base_difficulty - 1)
521
+ return base_difficulty
522
+
523
+ def generate_nonce(
524
+ self,
525
+ *,
526
+ difficulty: int,
527
+ ) -> int:
528
+ """
529
+ Find a nonce that yields a block hash with the required leading zero bits.
530
+
531
+ The search starts from the current nonce and iterates until the target
532
+ difficulty is met.
533
+ """
534
+ target = max(1, int(difficulty))
535
+ start = int(self.nonce or 0)
536
+ nonce = start
537
+ while True:
538
+ self.nonce = nonce
539
+ block_hash, _ = self.to_atom()
540
+ leading_zeros = self._leading_zero_bits(block_hash)
541
+ if leading_zeros >= target:
542
+ self.atom_hash = block_hash
543
+ return nonce
544
+ nonce += 1