zexus 1.7.1 → 1.7.2

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 (159) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/__init__.py +7 -0
  4. package/src/zexus/__init__.py +1 -1
  5. package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
  7. package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
  8. package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
  9. package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
  10. package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
  11. package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
  12. package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
  13. package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
  14. package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
  15. package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
  16. package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
  17. package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
  18. package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  19. package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
  20. package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
  22. package/src/zexus/advanced_types.py +17 -2
  23. package/src/zexus/blockchain/__init__.py +411 -0
  24. package/src/zexus/blockchain/accelerator.py +1160 -0
  25. package/src/zexus/blockchain/chain.py +660 -0
  26. package/src/zexus/blockchain/consensus.py +821 -0
  27. package/src/zexus/blockchain/contract_vm.py +1019 -0
  28. package/src/zexus/blockchain/crypto.py +79 -14
  29. package/src/zexus/blockchain/events.py +526 -0
  30. package/src/zexus/blockchain/loadtest.py +721 -0
  31. package/src/zexus/blockchain/monitoring.py +350 -0
  32. package/src/zexus/blockchain/mpt.py +716 -0
  33. package/src/zexus/blockchain/multichain.py +951 -0
  34. package/src/zexus/blockchain/multiprocess_executor.py +338 -0
  35. package/src/zexus/blockchain/network.py +886 -0
  36. package/src/zexus/blockchain/node.py +666 -0
  37. package/src/zexus/blockchain/rpc.py +1203 -0
  38. package/src/zexus/blockchain/rust_bridge.py +421 -0
  39. package/src/zexus/blockchain/storage.py +423 -0
  40. package/src/zexus/blockchain/tokens.py +750 -0
  41. package/src/zexus/blockchain/upgradeable.py +1004 -0
  42. package/src/zexus/blockchain/verification.py +1602 -0
  43. package/src/zexus/blockchain/wallet.py +621 -0
  44. package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
  45. package/src/zexus/cli/main.py +300 -20
  46. package/src/zexus/cli/zpm.py +1 -1
  47. package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
  48. package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
  49. package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
  50. package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
  51. package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  52. package/src/zexus/compiler/lexer.py +10 -5
  53. package/src/zexus/concurrency_system.py +79 -0
  54. package/src/zexus/config.py +54 -0
  55. package/src/zexus/crypto_bridge.py +244 -8
  56. package/src/zexus/dap/__init__.py +10 -0
  57. package/src/zexus/dap/__main__.py +4 -0
  58. package/src/zexus/dap/dap_server.py +391 -0
  59. package/src/zexus/dap/debug_engine.py +298 -0
  60. package/src/zexus/environment.py +10 -1
  61. package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
  62. package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
  63. package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
  64. package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
  65. package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
  66. package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
  67. package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
  68. package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
  69. package/src/zexus/evaluator/bytecode_compiler.py +441 -37
  70. package/src/zexus/evaluator/core.py +560 -49
  71. package/src/zexus/evaluator/expressions.py +122 -49
  72. package/src/zexus/evaluator/functions.py +417 -16
  73. package/src/zexus/evaluator/statements.py +521 -118
  74. package/src/zexus/evaluator/unified_execution.py +573 -72
  75. package/src/zexus/evaluator/utils.py +14 -2
  76. package/src/zexus/event_loop.py +186 -0
  77. package/src/zexus/lexer.py +742 -486
  78. package/src/zexus/lsp/__init__.py +1 -1
  79. package/src/zexus/lsp/definition_provider.py +163 -9
  80. package/src/zexus/lsp/server.py +22 -8
  81. package/src/zexus/lsp/symbol_provider.py +182 -9
  82. package/src/zexus/module_cache.py +237 -9
  83. package/src/zexus/object.py +64 -6
  84. package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
  85. package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
  86. package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
  87. package/src/zexus/parser/parser.py +786 -285
  88. package/src/zexus/parser/strategy_context.py +407 -66
  89. package/src/zexus/parser/strategy_structural.py +117 -19
  90. package/src/zexus/persistence.py +15 -1
  91. package/src/zexus/renderer/__init__.py +15 -0
  92. package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
  93. package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
  94. package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
  95. package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
  96. package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
  97. package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
  98. package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
  99. package/src/zexus/renderer/tk_backend.py +208 -0
  100. package/src/zexus/renderer/web_backend.py +260 -0
  101. package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  102. package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
  103. package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
  104. package/src/zexus/runtime/file_flags.py +137 -0
  105. package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
  107. package/src/zexus/security.py +424 -34
  108. package/src/zexus/stdlib/fs.py +23 -18
  109. package/src/zexus/stdlib/http.py +289 -186
  110. package/src/zexus/stdlib/sockets.py +207 -163
  111. package/src/zexus/stdlib/websockets.py +282 -0
  112. package/src/zexus/stdlib_integration.py +369 -2
  113. package/src/zexus/strategy_recovery.py +6 -3
  114. package/src/zexus/type_checker.py +423 -0
  115. package/src/zexus/virtual_filesystem.py +189 -2
  116. package/src/zexus/vm/__init__.py +113 -3
  117. package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
  118. package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
  119. package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
  120. package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
  121. package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
  122. package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
  123. package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
  124. package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
  125. package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
  126. package/src/zexus/vm/async_optimizer.py +14 -1
  127. package/src/zexus/vm/binary_bytecode.py +659 -0
  128. package/src/zexus/vm/bytecode.py +28 -1
  129. package/src/zexus/vm/bytecode_converter.py +26 -12
  130. package/src/zexus/vm/cabi.c +1985 -0
  131. package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
  132. package/src/zexus/vm/cabi.h +127 -0
  133. package/src/zexus/vm/cache.py +557 -17
  134. package/src/zexus/vm/compiler.py +703 -5
  135. package/src/zexus/vm/fastops.c +15743 -0
  136. package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
  137. package/src/zexus/vm/fastops.pyx +288 -0
  138. package/src/zexus/vm/gas_metering.py +50 -9
  139. package/src/zexus/vm/jit.py +83 -2
  140. package/src/zexus/vm/native_jit_backend.py +1816 -0
  141. package/src/zexus/vm/native_runtime.cpp +1388 -0
  142. package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
  143. package/src/zexus/vm/optimizer.py +161 -11
  144. package/src/zexus/vm/parallel_vm.py +118 -42
  145. package/src/zexus/vm/peephole_optimizer.py +82 -4
  146. package/src/zexus/vm/profiler.py +38 -18
  147. package/src/zexus/vm/register_allocator.py +16 -5
  148. package/src/zexus/vm/register_vm.py +8 -5
  149. package/src/zexus/vm/vm.py +3411 -573
  150. package/src/zexus/vm/wasm_compiler.py +658 -0
  151. package/src/zexus/zexus_ast.py +63 -11
  152. package/src/zexus/zexus_token.py +13 -5
  153. package/src/zexus/zpm/installer.py +55 -15
  154. package/src/zexus/zpm/package_manager.py +1 -1
  155. package/src/zexus/zpm/registry.py +257 -28
  156. package/src/zexus.egg-info/PKG-INFO +7 -4
  157. package/src/zexus.egg-info/SOURCES.txt +116 -9
  158. package/src/zexus.egg-info/entry_points.txt +1 -0
  159. package/src/zexus.egg-info/requires.txt +4 -0
@@ -0,0 +1,821 @@
1
+ """
2
+ Zexus Blockchain — Pluggable Consensus Engine
3
+
4
+ Provides a consensus interface and three concrete implementations:
5
+ - Proof-of-Work (PoW): nonce-grinding on block header hash
6
+ - Proof-of-Authority (PoA): pre-approved validator set with round-robin
7
+ - Proof-of-Stake (PoS): weighted random selection by staked balance
8
+
9
+ Each engine produces blocks (``seal``) and validates them (``verify``).
10
+ The ``ConsensusEngine`` abstract base class defines the contract.
11
+ """
12
+
13
+ import abc
14
+ import hashlib
15
+ import json
16
+ import random
17
+ import time
18
+ import logging
19
+ from dataclasses import dataclass, field
20
+ from typing import Any, Callable, Dict, List, Optional, Set
21
+
22
+ from .chain import Block, BlockHeader, Transaction, TransactionReceipt, Chain, Mempool
23
+
24
+ logger = logging.getLogger("zexus.blockchain.consensus")
25
+
26
+
27
+ class ConsensusEngine(abc.ABC):
28
+ """Abstract consensus engine interface."""
29
+
30
+ @abc.abstractmethod
31
+ def seal(self, chain: Chain, mempool: Mempool, miner: str) -> Optional[Block]:
32
+ """Produce a new block from pending transactions.
33
+
34
+ Args:
35
+ chain: Current blockchain state.
36
+ mempool: Pending transaction pool.
37
+ miner: Address of the block producer.
38
+
39
+ Returns:
40
+ A sealed Block ready to add to chain, or None if unable.
41
+ """
42
+ ...
43
+
44
+ @abc.abstractmethod
45
+ def verify(self, block: Block, chain: Chain) -> bool:
46
+ """Verify that a block satisfies the consensus rules.
47
+
48
+ Args:
49
+ block: Block to verify.
50
+ chain: Chain context (for parent lookup etc.).
51
+
52
+ Returns:
53
+ True if the block is valid under this consensus.
54
+ """
55
+ ...
56
+
57
+ def _prepare_block(self, chain: Chain, mempool: Mempool, miner: str,
58
+ gas_limit: int = 10_000_000) -> Block:
59
+ """Prepare a block template with pending transactions.
60
+
61
+ Shared helper for all consensus implementations.
62
+ """
63
+ tip = chain.tip
64
+ block = Block()
65
+ block.header.version = 1
66
+ block.header.height = (tip.header.height + 1) if tip else 0
67
+ block.header.prev_hash = tip.hash if tip else "0" * 64
68
+ block.header.timestamp = time.time()
69
+ block.header.miner = miner
70
+ block.header.gas_limit = gas_limit
71
+
72
+ # Select transactions
73
+ txs = mempool.get_pending(gas_limit)
74
+ block.transactions = txs
75
+ total_gas = sum(tx.gas_limit for tx in txs)
76
+ block.header.gas_used = total_gas
77
+ block.header.tx_root = block.compute_tx_root()
78
+
79
+ # Execute transactions and produce receipts
80
+ for tx in txs:
81
+ receipt = self._execute_tx(tx, chain, block.header.height)
82
+ block.receipts.append(receipt)
83
+
84
+ return block
85
+
86
+ def _execute_tx(self, tx: Transaction, chain: Chain, block_height: int) -> TransactionReceipt:
87
+ """Execute a single transaction against the chain state.
88
+
89
+ Returns a receipt. Keeps it simple — value transfer + gas.
90
+ Contract execution hooks into the evaluator (done at node level).
91
+ """
92
+ sender_acct = chain.get_account(tx.sender)
93
+ cost = tx.value + (tx.gas_limit * tx.gas_price)
94
+
95
+ receipt = TransactionReceipt(
96
+ tx_hash=tx.tx_hash,
97
+ block_height=block_height,
98
+ gas_used=tx.gas_limit,
99
+ )
100
+
101
+ # Validate
102
+ if sender_acct["balance"] < cost:
103
+ receipt.status = 0
104
+ receipt.revert_reason = "insufficient balance"
105
+ return receipt
106
+
107
+ if tx.nonce < sender_acct["nonce"]:
108
+ receipt.status = 0
109
+ receipt.revert_reason = "nonce too low"
110
+ return receipt
111
+
112
+ # Success
113
+ receipt.status = 1
114
+
115
+ # If it's a contract deployment (no recipient)
116
+ if not tx.recipient and tx.data:
117
+ contract_addr = hashlib.sha256(
118
+ f"{tx.sender}{tx.nonce}".encode()
119
+ ).hexdigest()[:40]
120
+ receipt.contract_address = contract_addr
121
+
122
+ return receipt
123
+
124
+
125
+ class ProofOfWork(ConsensusEngine):
126
+ """Proof-of-Work consensus using SHA-256 nonce grinding.
127
+
128
+ The block hash must start with ``difficulty`` zero hex chars.
129
+ """
130
+
131
+ def __init__(self, difficulty: int = 1, max_iterations: int = 10_000_000):
132
+ self.difficulty = difficulty
133
+ self.max_iterations = max_iterations
134
+
135
+ def seal(self, chain: Chain, mempool: Mempool, miner: str) -> Optional[Block]:
136
+ block = self._prepare_block(chain, mempool, miner)
137
+ block.header.difficulty = self.difficulty
138
+
139
+ target = "0" * self.difficulty
140
+ for nonce in range(self.max_iterations):
141
+ block.header.nonce = nonce
142
+ h = block.header.compute_hash()
143
+ if h.startswith(target):
144
+ block.hash = h
145
+ logger.info("PoW: mined block %d (nonce=%d, hash=%s)",
146
+ block.header.height, nonce, h[:16])
147
+ # Remove selected txs from mempool
148
+ for tx in block.transactions:
149
+ mempool.remove(tx.tx_hash)
150
+ return block
151
+
152
+ logger.warning("PoW: failed to find valid nonce in %d iterations", self.max_iterations)
153
+ return None
154
+
155
+ def verify(self, block: Block, chain: Chain) -> bool:
156
+ target = "0" * block.header.difficulty
157
+ computed = block.header.compute_hash()
158
+ if computed != block.hash:
159
+ return False
160
+ if not block.hash.startswith(target):
161
+ return False
162
+ return True
163
+
164
+
165
+ class ProofOfAuthority(ConsensusEngine):
166
+ """Proof-of-Authority consensus with a fixed validator set.
167
+
168
+ Validators take turns producing blocks in round-robin order.
169
+ Only authorized validators can seal blocks.
170
+ """
171
+
172
+ def __init__(self, validators: Optional[List[str]] = None,
173
+ block_interval: float = 5.0):
174
+ self.validators: List[str] = validators or []
175
+ self.block_interval = block_interval
176
+
177
+ def add_validator(self, address: str):
178
+ if address not in self.validators:
179
+ self.validators.append(address)
180
+
181
+ def remove_validator(self, address: str):
182
+ self.validators = [v for v in self.validators if v != address]
183
+
184
+ def _current_validator(self, height: int) -> Optional[str]:
185
+ """Determine which validator should produce the block at ``height``."""
186
+ if not self.validators:
187
+ return None
188
+ idx = height % len(self.validators)
189
+ return self.validators[idx]
190
+
191
+ def seal(self, chain: Chain, mempool: Mempool, miner: str) -> Optional[Block]:
192
+ if miner not in self.validators:
193
+ logger.warning("PoA: %s is not an authorized validator", miner[:8])
194
+ return None
195
+
196
+ next_height = chain.height + 1
197
+ expected = self._current_validator(next_height)
198
+ if expected != miner:
199
+ logger.debug("PoA: not %s's turn (expected %s)", miner[:8], expected[:8] if expected else "?")
200
+ return None
201
+
202
+ block = self._prepare_block(chain, mempool, miner)
203
+ block.header.difficulty = 0 # No PoW
204
+ block.header.extra_data = f"poa:validator={miner[:8]}"
205
+ block.compute_hash()
206
+
207
+ for tx in block.transactions:
208
+ mempool.remove(tx.tx_hash)
209
+
210
+ logger.info("PoA: validator %s sealed block %d", miner[:8], block.header.height)
211
+ return block
212
+
213
+ def verify(self, block: Block, chain: Chain) -> bool:
214
+ # Verify the miner was the correct validator for this height
215
+ expected = self._current_validator(block.header.height)
216
+ if expected != block.header.miner:
217
+ return False
218
+ if block.header.miner not in self.validators:
219
+ return False
220
+ computed = block.header.compute_hash()
221
+ return computed == block.hash
222
+
223
+
224
+ class ProofOfStake(ConsensusEngine):
225
+ """Simple Proof-of-Stake consensus.
226
+
227
+ Validators are selected with probability proportional to their
228
+ staked balance. Uses a VRF-like (verifiable random function by
229
+ hashing parent hash + height) deterministic selection.
230
+ """
231
+
232
+ def __init__(self, min_stake: int = 1000):
233
+ self.min_stake = min_stake
234
+ self.stakes: Dict[str, int] = {} # validator -> stake amount
235
+
236
+ def stake(self, validator: str, amount: int):
237
+ """Register or increase a validator's stake."""
238
+ current = self.stakes.get(validator, 0)
239
+ self.stakes[validator] = current + amount
240
+
241
+ def unstake(self, validator: str, amount: int) -> bool:
242
+ """Reduce a validator's stake."""
243
+ current = self.stakes.get(validator, 0)
244
+ if amount > current:
245
+ return False
246
+ self.stakes[validator] = current - amount
247
+ if self.stakes[validator] <= 0:
248
+ del self.stakes[validator]
249
+ return True
250
+
251
+ def get_eligible_validators(self) -> List[str]:
252
+ """Return validators with at least min_stake."""
253
+ return [v for v, s in self.stakes.items() if s >= self.min_stake]
254
+
255
+ def _select_validator(self, parent_hash: str, height: int) -> Optional[str]:
256
+ """Deterministically select a validator based on parent hash + height."""
257
+ eligible = self.get_eligible_validators()
258
+ if not eligible:
259
+ return None
260
+
261
+ # Deterministic seed from chain state
262
+ seed_data = f"{parent_hash}{height}".encode()
263
+ seed_hash = hashlib.sha256(seed_data).hexdigest()
264
+ seed_int = int(seed_hash[:16], 16)
265
+
266
+ # Weighted selection
267
+ total_stake = sum(self.stakes[v] for v in eligible)
268
+ target = seed_int % total_stake
269
+ cumulative = 0
270
+ for v in sorted(eligible): # Sort for deterministic ordering
271
+ cumulative += self.stakes[v]
272
+ if cumulative > target:
273
+ return v
274
+ return eligible[-1]
275
+
276
+ def seal(self, chain: Chain, mempool: Mempool, miner: str) -> Optional[Block]:
277
+ if miner not in self.stakes or self.stakes[miner] < self.min_stake:
278
+ logger.warning("PoS: %s has insufficient stake", miner[:8])
279
+ return None
280
+
281
+ tip = chain.tip
282
+ parent_hash = tip.hash if tip else "0" * 64
283
+ next_height = chain.height + 1
284
+
285
+ selected = self._select_validator(parent_hash, next_height)
286
+ if selected != miner:
287
+ logger.debug("PoS: %s not selected (selected=%s)", miner[:8], selected[:8] if selected else "?")
288
+ return None
289
+
290
+ block = self._prepare_block(chain, mempool, miner)
291
+ block.header.difficulty = 0
292
+ block.header.extra_data = f"pos:stake={self.stakes.get(miner, 0)}"
293
+ block.compute_hash()
294
+
295
+ for tx in block.transactions:
296
+ mempool.remove(tx.tx_hash)
297
+
298
+ logger.info("PoS: validator %s sealed block %d (stake=%d)",
299
+ miner[:8], block.header.height, self.stakes.get(miner, 0))
300
+ return block
301
+
302
+ def verify(self, block: Block, chain: Chain) -> bool:
303
+ parent = chain.get_block(block.header.height - 1)
304
+ parent_hash = parent.hash if parent else "0" * 64
305
+ selected = self._select_validator(parent_hash, block.header.height)
306
+ if selected != block.header.miner:
307
+ return False
308
+ computed = block.header.compute_hash()
309
+ return computed == block.hash
310
+
311
+
312
+ # ══════════════════════════════════════════════════════════════════════
313
+ # BFT Consensus (PBFT / Tendermint-style)
314
+ # ══════════════════════════════════════════════════════════════════════
315
+
316
+ class BFTPhase:
317
+ """PBFT round phases."""
318
+ PRE_PREPARE = "pre-prepare"
319
+ PREPARE = "prepare"
320
+ COMMIT = "commit"
321
+ DECIDED = "decided"
322
+
323
+
324
+ @dataclass
325
+ class BFTMessage:
326
+ """A PBFT protocol message."""
327
+ phase: str # BFTPhase value
328
+ view: int # Current view number (leader rotation epoch)
329
+ height: int # Block height this message is about
330
+ block_hash: str # Hash of the proposed block
331
+ sender: str # Validator address
332
+ signature: str = "" # HMAC-SHA256 signature
333
+ timestamp: float = field(default_factory=time.time)
334
+
335
+ def signing_payload(self) -> str:
336
+ return f"{self.phase}:{self.view}:{self.height}:{self.block_hash}:{self.sender}"
337
+
338
+ def sign(self, key: bytes) -> None:
339
+ import hmac as _hmac
340
+ self.signature = _hmac.new(key, self.signing_payload().encode(), hashlib.sha256).hexdigest()
341
+
342
+ def verify_signature(self, key: bytes) -> bool:
343
+ import hmac as _hmac
344
+ expected = _hmac.new(key, self.signing_payload().encode(), hashlib.sha256).hexdigest()
345
+ return self.signature == expected
346
+
347
+ def to_dict(self) -> Dict[str, Any]:
348
+ return {
349
+ "phase": self.phase,
350
+ "view": self.view,
351
+ "height": self.height,
352
+ "block_hash": self.block_hash,
353
+ "sender": self.sender,
354
+ "signature": self.signature,
355
+ "timestamp": self.timestamp,
356
+ }
357
+
358
+ @classmethod
359
+ def from_dict(cls, d: Dict[str, Any]) -> "BFTMessage":
360
+ return cls(
361
+ phase=d["phase"],
362
+ view=d["view"],
363
+ height=d["height"],
364
+ block_hash=d["block_hash"],
365
+ sender=d["sender"],
366
+ signature=d.get("signature", ""),
367
+ timestamp=d.get("timestamp", 0.0),
368
+ )
369
+
370
+
371
+ @dataclass
372
+ class BFTRoundState:
373
+ """Tracks the state of a single PBFT round."""
374
+ view: int = 0
375
+ height: int = 0
376
+ phase: str = BFTPhase.PRE_PREPARE
377
+ proposed_block: Optional[Block] = None
378
+ proposed_hash: str = ""
379
+
380
+ # Votes collected per phase: sender -> BFTMessage
381
+ pre_prepare: Optional[BFTMessage] = None
382
+ prepares: Dict[str, BFTMessage] = field(default_factory=dict)
383
+ commits: Dict[str, BFTMessage] = field(default_factory=dict)
384
+
385
+ decided: bool = False
386
+ locked_block: Optional[Block] = None
387
+
388
+ def prepare_count(self) -> int:
389
+ return len(self.prepares)
390
+
391
+ def commit_count(self) -> int:
392
+ return len(self.commits)
393
+
394
+
395
+ class BFTConsensus(ConsensusEngine):
396
+ """Byzantine Fault Tolerant consensus engine.
397
+
398
+ Implements a PBFT / Tendermint-style 3-phase commit protocol:
399
+
400
+ 1. **Pre-Prepare** — The leader (proposer) creates a block and
401
+ broadcasts a PRE-PREPARE message.
402
+ 2. **Prepare** — Validators validate the proposed block and
403
+ broadcast PREPARE votes.
404
+ 3. **Commit** — Once ≥ 2f+1 PREPARE messages are collected,
405
+ validators broadcast COMMIT votes.
406
+ 4. **Decide** — When ≥ 2f+1 COMMIT messages are collected the
407
+ block is finalized.
408
+
409
+ Where *f = (n-1) // 3* is the maximum tolerable Byzantine faults
410
+ and *n* is the total validator count.
411
+
412
+ Features:
413
+ - Round-robin leader rotation (``view`` mod ``len(validators)``)
414
+ - View-change on timeout (leader failure recovery)
415
+ - Instant finality — no probabilistic reorgs
416
+ - Block locking to prevent equivocation
417
+ """
418
+
419
+ def __init__(self, validators: Optional[List[str]] = None,
420
+ block_interval: float = 2.0,
421
+ view_change_timeout: float = 10.0):
422
+ self.validators: List[str] = list(validators or [])
423
+ self.block_interval = block_interval
424
+ self.view_change_timeout = view_change_timeout
425
+
426
+ # Current round state
427
+ self.view: int = 0
428
+ self._rounds: Dict[int, BFTRoundState] = {} # height -> round
429
+
430
+ # View-change tracking
431
+ self._view_change_votes: Dict[int, Set[str]] = {} # new_view -> set of senders
432
+ self._last_block_time: float = time.time()
433
+
434
+ # Finalized blocks awaiting pickup
435
+ self._finalized_blocks: List[Block] = []
436
+
437
+ # Validator signing keys (shared secret per validator for simulation)
438
+ self._validator_keys: Dict[str, bytes] = {}
439
+
440
+ # ── Properties ────────────────────────────────────────────────
441
+
442
+ @property
443
+ def n(self) -> int:
444
+ """Total validator count."""
445
+ return len(self.validators)
446
+
447
+ @property
448
+ def f(self) -> int:
449
+ """Max tolerable Byzantine faults: (n-1) // 3."""
450
+ return (self.n - 1) // 3
451
+
452
+ @property
453
+ def quorum(self) -> int:
454
+ """Quorum size: 2f + 1."""
455
+ return 2 * self.f + 1
456
+
457
+ # ── Validator management ──────────────────────────────────────
458
+
459
+ def add_validator(self, address: str, key: Optional[bytes] = None) -> None:
460
+ if address not in self.validators:
461
+ self.validators.append(address)
462
+ if key:
463
+ self._validator_keys[address] = key
464
+ else:
465
+ self._validator_keys.setdefault(
466
+ address, hashlib.sha256(address.encode()).digest()
467
+ )
468
+
469
+ def remove_validator(self, address: str) -> None:
470
+ self.validators = [v for v in self.validators if v != address]
471
+ self._validator_keys.pop(address, None)
472
+
473
+ def _get_leader(self, view: int) -> Optional[str]:
474
+ """Round-robin leader selection based on view number."""
475
+ if not self.validators:
476
+ return None
477
+ return self.validators[view % len(self.validators)]
478
+
479
+ def _get_round(self, height: int) -> BFTRoundState:
480
+ if height not in self._rounds:
481
+ self._rounds[height] = BFTRoundState(view=self.view, height=height)
482
+ return self._rounds[height]
483
+
484
+ def _cleanup_old_rounds(self, current_height: int) -> None:
485
+ """Remove round state for heights well below current."""
486
+ old = [h for h in self._rounds if h < current_height - 10]
487
+ for h in old:
488
+ del self._rounds[h]
489
+
490
+ # ── PBFT Phases ───────────────────────────────────────────────
491
+
492
+ def propose(self, chain: Chain, mempool: Mempool, miner: str) -> Optional[BFTMessage]:
493
+ """Phase 1: Leader creates a block and broadcasts PRE-PREPARE.
494
+
495
+ Returns the BFTMessage (PRE-PREPARE) to broadcast.
496
+ """
497
+ leader = self._get_leader(self.view)
498
+ if leader != miner:
499
+ return None
500
+
501
+ height = chain.height + 1
502
+ block = self._prepare_block(chain, mempool, miner)
503
+ block.header.difficulty = 0
504
+ block.header.extra_data = json.dumps({
505
+ "bft": True,
506
+ "view": self.view,
507
+ "proposer": miner[:16],
508
+ })
509
+ block.compute_hash()
510
+
511
+ rnd = self._get_round(height)
512
+ rnd.proposed_block = block
513
+ rnd.proposed_hash = block.hash
514
+ rnd.phase = BFTPhase.PRE_PREPARE
515
+
516
+ msg = BFTMessage(
517
+ phase=BFTPhase.PRE_PREPARE,
518
+ view=self.view,
519
+ height=height,
520
+ block_hash=block.hash,
521
+ sender=miner,
522
+ )
523
+ key = self._validator_keys.get(miner, b"")
524
+ if key:
525
+ msg.sign(key)
526
+
527
+ rnd.pre_prepare = msg
528
+ logger.info("BFT: PRE-PREPARE (view=%d, height=%d, hash=%s)",
529
+ self.view, height, block.hash[:16])
530
+ return msg
531
+
532
+ def on_pre_prepare(self, msg: BFTMessage, proposed_block: Block,
533
+ chain: Chain, validator: str) -> Optional[BFTMessage]:
534
+ """Handle a received PRE-PREPARE: validate and send PREPARE."""
535
+ leader = self._get_leader(msg.view)
536
+ if msg.sender != leader:
537
+ logger.warning("BFT: PRE-PREPARE from non-leader %s", msg.sender[:8])
538
+ return None
539
+
540
+ if msg.view < self.view:
541
+ return None
542
+
543
+ # Validate the proposed block
544
+ if msg.block_hash != proposed_block.hash:
545
+ return None
546
+
547
+ rnd = self._get_round(msg.height)
548
+ rnd.proposed_block = proposed_block
549
+ rnd.proposed_hash = msg.block_hash
550
+ rnd.pre_prepare = msg
551
+ rnd.phase = BFTPhase.PREPARE
552
+
553
+ # Send PREPARE
554
+ prepare = BFTMessage(
555
+ phase=BFTPhase.PREPARE,
556
+ view=msg.view,
557
+ height=msg.height,
558
+ block_hash=msg.block_hash,
559
+ sender=validator,
560
+ )
561
+ key = self._validator_keys.get(validator, b"")
562
+ if key:
563
+ prepare.sign(key)
564
+
565
+ rnd.prepares[validator] = prepare
566
+ logger.debug("BFT: PREPARE from %s (height=%d)", validator[:8], msg.height)
567
+ return prepare
568
+
569
+ def on_prepare(self, msg: BFTMessage, validator: str) -> Optional[BFTMessage]:
570
+ """Handle a received PREPARE vote.
571
+
572
+ When quorum is reached, emit a COMMIT.
573
+ """
574
+ if msg.sender not in self.validators:
575
+ return None
576
+
577
+ rnd = self._get_round(msg.height)
578
+
579
+ if msg.block_hash != rnd.proposed_hash:
580
+ return None
581
+
582
+ rnd.prepares[msg.sender] = msg
583
+
584
+ # Check quorum
585
+ if rnd.prepare_count() >= self.quorum and rnd.phase == BFTPhase.PREPARE:
586
+ rnd.phase = BFTPhase.COMMIT
587
+ rnd.locked_block = rnd.proposed_block
588
+
589
+ commit = BFTMessage(
590
+ phase=BFTPhase.COMMIT,
591
+ view=msg.view,
592
+ height=msg.height,
593
+ block_hash=msg.block_hash,
594
+ sender=validator,
595
+ )
596
+ key = self._validator_keys.get(validator, b"")
597
+ if key:
598
+ commit.sign(key)
599
+
600
+ rnd.commits[validator] = commit
601
+ logger.info("BFT: COMMIT (quorum reached, height=%d, prepares=%d)",
602
+ msg.height, rnd.prepare_count())
603
+ return commit
604
+
605
+ return None
606
+
607
+ def on_commit(self, msg: BFTMessage) -> Optional[Block]:
608
+ """Handle a received COMMIT vote.
609
+
610
+ Returns the finalized block if commit quorum is reached.
611
+ """
612
+ if msg.sender not in self.validators:
613
+ return None
614
+
615
+ rnd = self._get_round(msg.height)
616
+
617
+ if msg.block_hash != rnd.proposed_hash:
618
+ return None
619
+
620
+ rnd.commits[msg.sender] = msg
621
+
622
+ if rnd.commit_count() >= self.quorum and not rnd.decided:
623
+ rnd.decided = True
624
+ rnd.phase = BFTPhase.DECIDED
625
+ self._last_block_time = time.time()
626
+
627
+ logger.info("BFT: DECIDED block %d (commits=%d/%d)",
628
+ msg.height, rnd.commit_count(), self.n)
629
+
630
+ if rnd.proposed_block:
631
+ self._finalized_blocks.append(rnd.proposed_block)
632
+ return rnd.proposed_block
633
+
634
+ return None
635
+
636
+ # ── View Change ───────────────────────────────────────────────
637
+
638
+ def request_view_change(self, validator: str) -> int:
639
+ """Initiate a view change (leader rotation).
640
+
641
+ Returns the proposed new view number.
642
+ """
643
+ new_view = self.view + 1
644
+ if new_view not in self._view_change_votes:
645
+ self._view_change_votes[new_view] = set()
646
+
647
+ self._view_change_votes[new_view].add(validator)
648
+
649
+ if len(self._view_change_votes[new_view]) >= self.quorum:
650
+ old_view = self.view
651
+ self.view = new_view
652
+ self._view_change_votes.pop(new_view, None)
653
+ logger.info("BFT: VIEW CHANGE %d -> %d (new leader: %s)",
654
+ old_view, new_view,
655
+ self._get_leader(new_view)[:8] if self._get_leader(new_view) else "?")
656
+ return new_view
657
+
658
+ def check_timeout(self, validator: str) -> bool:
659
+ """Check if the current round has timed out, triggering view change."""
660
+ elapsed = time.time() - self._last_block_time
661
+ if elapsed > self.view_change_timeout:
662
+ self.request_view_change(validator)
663
+ return True
664
+ return False
665
+
666
+ # ── ConsensusEngine interface ─────────────────────────────────
667
+
668
+ def seal(self, chain: Chain, mempool: Mempool, miner: str) -> Optional[Block]:
669
+ """Full BFT round: propose → prepare → commit → decide.
670
+
671
+ For local/single-process simulation, runs all phases
672
+ synchronously with all validators. In production, each
673
+ phase would be triggered by network messages.
674
+ """
675
+ if not self.validators:
676
+ logger.warning("BFT: no validators configured")
677
+ return None
678
+
679
+ if miner not in self.validators:
680
+ logger.warning("BFT: %s is not a validator", miner[:8])
681
+ return None
682
+
683
+ leader = self._get_leader(self.view)
684
+ if leader != miner:
685
+ return None
686
+
687
+ height = chain.height + 1
688
+ self._cleanup_old_rounds(height)
689
+
690
+ # Phase 1: PRE-PREPARE
691
+ pre_prepare = self.propose(chain, mempool, miner)
692
+ if not pre_prepare:
693
+ return None
694
+
695
+ rnd = self._get_round(height)
696
+
697
+ # Phase 2: PREPARE — simulate all validators responding
698
+ for v in self.validators:
699
+ if v == miner:
700
+ # Leader also prepares
701
+ rnd.prepares[v] = BFTMessage(
702
+ phase=BFTPhase.PREPARE,
703
+ view=self.view,
704
+ height=height,
705
+ block_hash=rnd.proposed_hash,
706
+ sender=v,
707
+ )
708
+ continue
709
+ prepare = self.on_pre_prepare(pre_prepare, rnd.proposed_block, chain, v)
710
+ if prepare:
711
+ rnd.prepares[v] = prepare
712
+
713
+ if rnd.prepare_count() < self.quorum:
714
+ logger.warning("BFT: not enough prepares (%d/%d)",
715
+ rnd.prepare_count(), self.quorum)
716
+ return None
717
+
718
+ rnd.phase = BFTPhase.COMMIT
719
+
720
+ # Phase 3: COMMIT — simulate all validators committing
721
+ for v in self.validators:
722
+ commit = BFTMessage(
723
+ phase=BFTPhase.COMMIT,
724
+ view=self.view,
725
+ height=height,
726
+ block_hash=rnd.proposed_hash,
727
+ sender=v,
728
+ )
729
+ rnd.commits[v] = commit
730
+
731
+ if rnd.commit_count() < self.quorum:
732
+ logger.warning("BFT: not enough commits (%d/%d)",
733
+ rnd.commit_count(), self.quorum)
734
+ return None
735
+
736
+ # Phase 4: DECIDE
737
+ rnd.decided = True
738
+ rnd.phase = BFTPhase.DECIDED
739
+ self._last_block_time = time.time()
740
+
741
+ block = rnd.proposed_block
742
+ if block:
743
+ for tx in block.transactions:
744
+ mempool.remove(tx.tx_hash)
745
+ logger.info("BFT: sealed block %d (view=%d, validators=%d, quorum=%d)",
746
+ height, self.view, self.n, self.quorum)
747
+ return block
748
+
749
+ return None
750
+
751
+ def verify(self, block: Block, chain: Chain) -> bool:
752
+ """Verify a BFT-sealed block.
753
+
754
+ Checks:
755
+ - Block hash matches computed hash
756
+ - Extra data contains BFT metadata
757
+ - Proposer was the correct leader for the view
758
+ """
759
+ computed = block.header.compute_hash()
760
+ if computed != block.hash:
761
+ return False
762
+
763
+ # Parse BFT metadata from extra_data
764
+ try:
765
+ meta = json.loads(block.header.extra_data)
766
+ except (json.JSONDecodeError, TypeError):
767
+ return False
768
+
769
+ if not meta.get("bft"):
770
+ return False
771
+
772
+ view = meta.get("view", 0)
773
+ leader = self._get_leader(view)
774
+ if leader != block.header.miner:
775
+ return False
776
+
777
+ return True
778
+
779
+ def get_round_state(self, height: int) -> Optional[BFTRoundState]:
780
+ """Get the round state for a given height (for debugging/RPC)."""
781
+ return self._rounds.get(height)
782
+
783
+ def get_status(self) -> Dict[str, Any]:
784
+ """Return current BFT consensus status."""
785
+ leader = self._get_leader(self.view)
786
+ return {
787
+ "algorithm": "bft",
788
+ "view": self.view,
789
+ "validators": list(self.validators),
790
+ "validator_count": self.n,
791
+ "max_faults": self.f,
792
+ "quorum": self.quorum,
793
+ "current_leader": leader or "",
794
+ "block_interval": self.block_interval,
795
+ "view_change_timeout": self.view_change_timeout,
796
+ "active_rounds": list(self._rounds.keys()),
797
+ }
798
+
799
+
800
+ # ── Factory helper ─────────────────────────────────────────────────────
801
+
802
+ def create_consensus(algorithm: str = "pow", **kwargs) -> ConsensusEngine:
803
+ """Create a consensus engine by name.
804
+
805
+ Args:
806
+ algorithm: One of 'pow', 'poa', 'pos', 'bft'.
807
+ **kwargs: Passed to the constructor.
808
+
809
+ Returns:
810
+ ConsensusEngine instance.
811
+ """
812
+ engines = {
813
+ "pow": ProofOfWork,
814
+ "poa": ProofOfAuthority,
815
+ "pos": ProofOfStake,
816
+ "bft": BFTConsensus,
817
+ }
818
+ cls = engines.get(algorithm.lower())
819
+ if not cls:
820
+ raise ValueError(f"Unknown consensus algorithm: {algorithm}. Choose from: {list(engines.keys())}")
821
+ return cls(**kwargs)