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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1160 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1019 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +421 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +300 -20
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/lexer.py +10 -5
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +10 -1
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +441 -37
- package/src/zexus/evaluator/core.py +560 -49
- package/src/zexus/evaluator/expressions.py +122 -49
- package/src/zexus/evaluator/functions.py +417 -16
- package/src/zexus/evaluator/statements.py +521 -118
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -486
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +237 -9
- package/src/zexus/object.py +64 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +786 -285
- package/src/zexus/parser/strategy_context.py +407 -66
- package/src/zexus/parser/strategy_structural.py +117 -19
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +15 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +14 -1
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +28 -1
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +557 -17
- package/src/zexus/vm/compiler.py +703 -5
- package/src/zexus/vm/fastops.c +15743 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +50 -9
- package/src/zexus/vm/jit.py +83 -2
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +118 -42
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3411 -573
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +63 -11
- package/src/zexus/zexus_token.py +13 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +7 -4
- package/src/zexus.egg-info/SOURCES.txt +116 -9
- package/src/zexus.egg-info/entry_points.txt +1 -0
- 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)
|