zexus 1.6.8 → 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 (177) hide show
  1. package/README.md +12 -5
  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/capability_system.py +184 -9
  45. package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
  46. package/src/zexus/cli/main.py +383 -34
  47. package/src/zexus/cli/zpm.py +1 -1
  48. package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
  49. package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
  50. package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
  51. package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
  52. package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  53. package/src/zexus/compiler/bytecode.py +124 -7
  54. package/src/zexus/compiler/compat_runtime.py +6 -2
  55. package/src/zexus/compiler/lexer.py +16 -5
  56. package/src/zexus/compiler/parser.py +108 -7
  57. package/src/zexus/compiler/semantic.py +18 -19
  58. package/src/zexus/compiler/zexus_ast.py +26 -1
  59. package/src/zexus/concurrency_system.py +79 -0
  60. package/src/zexus/config.py +54 -0
  61. package/src/zexus/crypto_bridge.py +244 -8
  62. package/src/zexus/dap/__init__.py +10 -0
  63. package/src/zexus/dap/__main__.py +4 -0
  64. package/src/zexus/dap/dap_server.py +391 -0
  65. package/src/zexus/dap/debug_engine.py +298 -0
  66. package/src/zexus/environment.py +112 -9
  67. package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
  68. package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
  69. package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
  70. package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
  71. package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
  72. package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
  73. package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
  74. package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
  75. package/src/zexus/evaluator/bytecode_compiler.py +457 -37
  76. package/src/zexus/evaluator/core.py +644 -50
  77. package/src/zexus/evaluator/expressions.py +358 -62
  78. package/src/zexus/evaluator/functions.py +458 -20
  79. package/src/zexus/evaluator/resource_limiter.py +4 -4
  80. package/src/zexus/evaluator/statements.py +774 -122
  81. package/src/zexus/evaluator/unified_execution.py +573 -72
  82. package/src/zexus/evaluator/utils.py +14 -2
  83. package/src/zexus/evaluator_original.py +1 -1
  84. package/src/zexus/event_loop.py +186 -0
  85. package/src/zexus/lexer.py +742 -458
  86. package/src/zexus/lsp/__init__.py +1 -1
  87. package/src/zexus/lsp/definition_provider.py +163 -9
  88. package/src/zexus/lsp/server.py +22 -8
  89. package/src/zexus/lsp/symbol_provider.py +182 -9
  90. package/src/zexus/module_cache.py +239 -9
  91. package/src/zexus/module_manager.py +129 -1
  92. package/src/zexus/object.py +76 -6
  93. package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
  94. package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
  95. package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
  96. package/src/zexus/parser/parser.py +1349 -408
  97. package/src/zexus/parser/strategy_context.py +755 -58
  98. package/src/zexus/parser/strategy_structural.py +121 -21
  99. package/src/zexus/persistence.py +15 -1
  100. package/src/zexus/renderer/__init__.py +61 -0
  101. package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
  102. package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
  103. package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
  104. package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
  105. package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
  106. package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
  107. package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
  108. package/src/zexus/renderer/backend.py +261 -0
  109. package/src/zexus/renderer/canvas.py +78 -0
  110. package/src/zexus/renderer/color_system.py +201 -0
  111. package/src/zexus/renderer/graphics.py +31 -0
  112. package/src/zexus/renderer/layout.py +222 -0
  113. package/src/zexus/renderer/main_renderer.py +66 -0
  114. package/src/zexus/renderer/painter.py +30 -0
  115. package/src/zexus/renderer/tk_backend.py +208 -0
  116. package/src/zexus/renderer/web_backend.py +260 -0
  117. package/src/zexus/runtime/__init__.py +10 -2
  118. package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  119. package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
  120. package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
  121. package/src/zexus/runtime/file_flags.py +137 -0
  122. package/src/zexus/runtime/load_manager.py +368 -0
  123. package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
  124. package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
  125. package/src/zexus/security.py +424 -34
  126. package/src/zexus/stdlib/fs.py +23 -18
  127. package/src/zexus/stdlib/http.py +289 -186
  128. package/src/zexus/stdlib/sockets.py +207 -163
  129. package/src/zexus/stdlib/websockets.py +282 -0
  130. package/src/zexus/stdlib_integration.py +369 -2
  131. package/src/zexus/strategy_recovery.py +6 -3
  132. package/src/zexus/type_checker.py +423 -0
  133. package/src/zexus/virtual_filesystem.py +189 -2
  134. package/src/zexus/vm/__init__.py +113 -3
  135. package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
  136. package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
  137. package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
  138. package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
  139. package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
  140. package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
  141. package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
  142. package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
  143. package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
  144. package/src/zexus/vm/async_optimizer.py +80 -6
  145. package/src/zexus/vm/binary_bytecode.py +659 -0
  146. package/src/zexus/vm/bytecode.py +59 -11
  147. package/src/zexus/vm/bytecode_converter.py +26 -12
  148. package/src/zexus/vm/cabi.c +1985 -0
  149. package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
  150. package/src/zexus/vm/cabi.h +127 -0
  151. package/src/zexus/vm/cache.py +561 -17
  152. package/src/zexus/vm/compiler.py +818 -51
  153. package/src/zexus/vm/fastops.c +15743 -0
  154. package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
  155. package/src/zexus/vm/fastops.pyx +288 -0
  156. package/src/zexus/vm/gas_metering.py +50 -9
  157. package/src/zexus/vm/jit.py +364 -20
  158. package/src/zexus/vm/native_jit_backend.py +1816 -0
  159. package/src/zexus/vm/native_runtime.cpp +1388 -0
  160. package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
  161. package/src/zexus/vm/optimizer.py +161 -11
  162. package/src/zexus/vm/parallel_vm.py +140 -45
  163. package/src/zexus/vm/peephole_optimizer.py +82 -4
  164. package/src/zexus/vm/profiler.py +38 -18
  165. package/src/zexus/vm/register_allocator.py +16 -5
  166. package/src/zexus/vm/register_vm.py +8 -5
  167. package/src/zexus/vm/vm.py +3581 -531
  168. package/src/zexus/vm/wasm_compiler.py +658 -0
  169. package/src/zexus/zexus_ast.py +137 -11
  170. package/src/zexus/zexus_token.py +16 -5
  171. package/src/zexus/zpm/installer.py +55 -15
  172. package/src/zexus/zpm/package_manager.py +1 -1
  173. package/src/zexus/zpm/registry.py +257 -28
  174. package/src/zexus.egg-info/PKG-INFO +16 -6
  175. package/src/zexus.egg-info/SOURCES.txt +129 -17
  176. package/src/zexus.egg-info/entry_points.txt +1 -0
  177. package/src/zexus.egg-info/requires.txt +4 -0
@@ -0,0 +1,660 @@
1
+ """
2
+ Zexus Blockchain — Chain & Block Data Structures
3
+
4
+ Provides the core blockchain data model: Block headers, transactions,
5
+ block validation, and chain management with persistent storage.
6
+
7
+ This module implements a proper blockchain (linked list of blocks) on top
8
+ of the existing Ledger/Transaction infrastructure.
9
+ """
10
+
11
+ import hashlib
12
+ import json
13
+ import time
14
+ import copy
15
+ import os
16
+ import sqlite3
17
+ from dataclasses import dataclass, field, asdict
18
+ from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
19
+ from pathlib import Path
20
+
21
+ if TYPE_CHECKING:
22
+ from .storage import StorageBackend
23
+
24
+ # Lazy import to avoid circular dependencies
25
+ def _get_storage_backend(name: str, **kwargs):
26
+ from .storage import get_storage_backend
27
+ return get_storage_backend(name, **kwargs)
28
+
29
+ # Import real cryptographic signing from CryptoPlugin
30
+ try:
31
+ from cryptography.hazmat.primitives import hashes, serialization
32
+ from cryptography.hazmat.primitives.asymmetric import ec
33
+ from cryptography.hazmat.backends import default_backend
34
+ from cryptography.exceptions import InvalidSignature
35
+ _ECDSA_AVAILABLE = True
36
+ except ImportError:
37
+ _ECDSA_AVAILABLE = False
38
+
39
+
40
+ @dataclass
41
+ class BlockHeader:
42
+ """Block header containing metadata and proof."""
43
+ version: int = 1
44
+ height: int = 0
45
+ timestamp: float = 0.0
46
+ prev_hash: str = "0" * 64
47
+ state_root: str = ""
48
+ tx_root: str = "" # Merkle root of transactions
49
+ receipts_root: str = ""
50
+ miner: str = "" # Address of block producer
51
+ nonce: int = 0
52
+ difficulty: int = 1
53
+ gas_limit: int = 10_000_000
54
+ gas_used: int = 0
55
+ extra_data: str = ""
56
+
57
+ def compute_hash(self) -> str:
58
+ """Compute block header hash (excludes the hash itself)."""
59
+ data = json.dumps({
60
+ "version": self.version,
61
+ "height": self.height,
62
+ "timestamp": self.timestamp,
63
+ "prev_hash": self.prev_hash,
64
+ "state_root": self.state_root,
65
+ "tx_root": self.tx_root,
66
+ "receipts_root": self.receipts_root,
67
+ "miner": self.miner,
68
+ "nonce": self.nonce,
69
+ "difficulty": self.difficulty,
70
+ "gas_limit": self.gas_limit,
71
+ "gas_used": self.gas_used,
72
+ "extra_data": self.extra_data,
73
+ }, sort_keys=True)
74
+ return hashlib.sha256(data.encode()).hexdigest()
75
+
76
+
77
+ @dataclass
78
+ class Transaction:
79
+ """A blockchain transaction with cryptographic authentication."""
80
+ tx_hash: str = ""
81
+ sender: str = ""
82
+ recipient: str = ""
83
+ value: int = 0
84
+ data: str = "" # Contract call data or deployment bytecode
85
+ nonce: int = 0 # Sender's nonce (replay protection)
86
+ gas_limit: int = 21_000
87
+ gas_price: int = 1
88
+ signature: str = "" # ECDSA signature
89
+ timestamp: float = 0.0
90
+ status: str = "pending" # pending, confirmed, failed, reverted
91
+
92
+ def compute_hash(self) -> str:
93
+ """Compute transaction hash from its contents."""
94
+ data = json.dumps({
95
+ "sender": self.sender,
96
+ "recipient": self.recipient,
97
+ "value": self.value,
98
+ "data": self.data,
99
+ "nonce": self.nonce,
100
+ "gas_limit": self.gas_limit,
101
+ "gas_price": self.gas_price,
102
+ "timestamp": self.timestamp,
103
+ }, sort_keys=True)
104
+ h = hashlib.sha256(data.encode()).hexdigest()
105
+ self.tx_hash = h
106
+ return h
107
+
108
+ def sign(self, private_key: str) -> str:
109
+ """Sign the transaction with an ECDSA private key (secp256k1).
110
+
111
+ Args:
112
+ private_key: Private key in PEM format, or hex-encoded raw key.
113
+
114
+ Returns:
115
+ Hex-encoded DER signature.
116
+ """
117
+ msg = self.compute_hash()
118
+ msg_bytes = msg.encode('utf-8')
119
+
120
+ if not _ECDSA_AVAILABLE:
121
+ raise RuntimeError(
122
+ "Transaction signing requires the 'cryptography' package. "
123
+ "Install with: pip install cryptography"
124
+ )
125
+
126
+ # Load private key — accept PEM or raw hex
127
+ if private_key.strip().startswith('-----BEGIN'):
128
+ priv = serialization.load_pem_private_key(
129
+ private_key.encode('utf-8'),
130
+ password=None,
131
+ backend=default_backend(),
132
+ )
133
+ else:
134
+ # Raw hex-encoded 32-byte scalar
135
+ try:
136
+ key_bytes = bytes.fromhex(private_key)
137
+ except ValueError:
138
+ raise ValueError("Private key must be PEM-encoded or a hex string")
139
+ priv = ec.derive_private_key(
140
+ int.from_bytes(key_bytes, 'big'),
141
+ ec.SECP256K1(),
142
+ default_backend(),
143
+ )
144
+
145
+ sig_bytes = priv.sign(msg_bytes, ec.ECDSA(hashes.SHA256()))
146
+ self.signature = sig_bytes.hex()
147
+ return self.signature
148
+
149
+ def verify(self, public_key: str) -> bool:
150
+ """Verify the ECDSA (secp256k1) signature on this transaction.
151
+
152
+ Args:
153
+ public_key: Public key in PEM format, or hex-encoded
154
+ uncompressed/compressed point.
155
+
156
+ Returns:
157
+ True if the signature is valid.
158
+ """
159
+ if not self.signature:
160
+ return False
161
+
162
+ if not _ECDSA_AVAILABLE:
163
+ return False
164
+
165
+ msg = (self.tx_hash or self.compute_hash()).encode('utf-8')
166
+
167
+ try:
168
+ sig_bytes = bytes.fromhex(self.signature)
169
+ except ValueError:
170
+ return False
171
+
172
+ # Load public key — accept PEM or raw hex point
173
+ try:
174
+ if public_key.strip().startswith('-----BEGIN'):
175
+ pub = serialization.load_pem_public_key(
176
+ public_key.encode('utf-8'),
177
+ backend=default_backend(),
178
+ )
179
+ else:
180
+ point_bytes = bytes.fromhex(public_key)
181
+ pub = ec.EllipticCurvePublicKey.from_encoded_point(
182
+ ec.SECP256K1(), point_bytes,
183
+ )
184
+ except Exception:
185
+ return False
186
+
187
+ try:
188
+ pub.verify(sig_bytes, msg, ec.ECDSA(hashes.SHA256()))
189
+ return True
190
+ except InvalidSignature:
191
+ return False
192
+ except Exception:
193
+ return False
194
+
195
+ def to_dict(self) -> Dict[str, Any]:
196
+ return asdict(self)
197
+
198
+
199
+ @dataclass
200
+ class TransactionReceipt:
201
+ """Receipt produced after transaction execution."""
202
+ tx_hash: str = ""
203
+ block_hash: str = ""
204
+ block_height: int = 0
205
+ status: int = 1 # 1 = success, 0 = failure
206
+ gas_used: int = 0
207
+ logs: List[Dict[str, Any]] = field(default_factory=list)
208
+ contract_address: Optional[str] = None # If deployment
209
+ revert_reason: str = ""
210
+
211
+ def to_dict(self) -> Dict[str, Any]:
212
+ return asdict(self)
213
+
214
+
215
+ @dataclass
216
+ class Block:
217
+ """A complete block containing header, transactions, and hash."""
218
+ header: BlockHeader = field(default_factory=BlockHeader)
219
+ transactions: List[Transaction] = field(default_factory=list)
220
+ receipts: List[TransactionReceipt] = field(default_factory=list)
221
+ hash: str = ""
222
+
223
+ def compute_hash(self) -> str:
224
+ """Compute the block hash from its header."""
225
+ self.hash = self.header.compute_hash()
226
+ return self.hash
227
+
228
+ def compute_tx_root(self) -> str:
229
+ """Compute Merkle root of transactions."""
230
+ if not self.transactions:
231
+ return hashlib.sha256(b"empty").hexdigest()
232
+
233
+ hashes = [tx.tx_hash or tx.compute_hash() for tx in self.transactions]
234
+ while len(hashes) > 1:
235
+ if len(hashes) % 2 == 1:
236
+ hashes.append(hashes[-1])
237
+ next_level = []
238
+ for i in range(0, len(hashes), 2):
239
+ combined = hashes[i] + hashes[i + 1]
240
+ next_level.append(hashlib.sha256(combined.encode()).hexdigest())
241
+ hashes = next_level
242
+ return hashes[0]
243
+
244
+ def to_dict(self) -> Dict[str, Any]:
245
+ return {
246
+ "hash": self.hash,
247
+ "header": asdict(self.header),
248
+ "transactions": [tx.to_dict() for tx in self.transactions],
249
+ "receipts": [r.to_dict() for r in self.receipts],
250
+ }
251
+
252
+ @staticmethod
253
+ def from_dict(data: Dict[str, Any]) -> 'Block':
254
+ """Reconstruct a Block from a dictionary."""
255
+ header = BlockHeader(**data["header"])
256
+ txs = [Transaction(**td) for td in data.get("transactions", [])]
257
+ receipts = [TransactionReceipt(**rd) for rd in data.get("receipts", [])]
258
+ b = Block(header=header, transactions=txs, receipts=receipts, hash=data.get("hash", ""))
259
+ return b
260
+
261
+
262
+ class Mempool:
263
+ """Transaction mempool with priority ordering and Replace-by-Fee (RBF).
264
+
265
+ Holds pending transactions, ordered by gas_price (descending)
266
+ for block producers to select from.
267
+
268
+ **Replace-by-Fee (RBF):** If a new transaction has the same
269
+ ``(sender, nonce)`` as an existing mempool entry, it replaces
270
+ the old one *only* if its ``gas_price`` exceeds the old price
271
+ by at least ``rbf_increment_pct`` percent (default 10 %).
272
+ """
273
+
274
+ def __init__(self, max_size: int = 10_000,
275
+ rbf_enabled: bool = True,
276
+ rbf_increment_pct: int = 10):
277
+ self.max_size = max_size
278
+ self.rbf_enabled = rbf_enabled
279
+ self.rbf_increment_pct = rbf_increment_pct
280
+ self._txs: Dict[str, Transaction] = {} # tx_hash -> Transaction
281
+ self._nonces: Dict[str, int] = {} # sender -> highest nonce seen
282
+ # RBF index: (sender, nonce) -> tx_hash — fast lookup for replacement
283
+ self._sender_nonce_idx: Dict[tuple, str] = {}
284
+
285
+ def add(self, tx: Transaction) -> bool:
286
+ """Add transaction to mempool. Returns True if accepted.
287
+
288
+ If RBF is enabled and a transaction from the same sender with the
289
+ same nonce already exists, the new transaction replaces it only
290
+ when its gas_price is at least ``rbf_increment_pct`` % higher.
291
+ """
292
+ if not tx.tx_hash:
293
+ tx.compute_hash()
294
+ if tx.tx_hash in self._txs:
295
+ return False # Exact duplicate
296
+
297
+ key = (tx.sender, tx.nonce)
298
+
299
+ # ── RBF path ──
300
+ if self.rbf_enabled and key in self._sender_nonce_idx:
301
+ old_hash = self._sender_nonce_idx[key]
302
+ old_tx = self._txs.get(old_hash)
303
+ if old_tx is not None:
304
+ min_price = old_tx.gas_price * (100 + self.rbf_increment_pct) // 100
305
+ if tx.gas_price >= min_price:
306
+ # Replace
307
+ del self._txs[old_hash]
308
+ self._txs[tx.tx_hash] = tx
309
+ self._sender_nonce_idx[key] = tx.tx_hash
310
+ return True
311
+ return False # Bump too small
312
+
313
+ # ── Normal path ──
314
+ if len(self._txs) >= self.max_size:
315
+ return False
316
+ # Nonce check
317
+ expected = self._nonces.get(tx.sender, 0)
318
+ if tx.nonce < expected:
319
+ return False # Replay
320
+ self._txs[tx.tx_hash] = tx
321
+ self._sender_nonce_idx[key] = tx.tx_hash
322
+ if tx.nonce >= expected:
323
+ self._nonces[tx.sender] = tx.nonce + 1
324
+ return True
325
+
326
+ def replace_by_fee(self, tx: Transaction) -> Dict[str, Any]:
327
+ """Explicitly attempt a replace-by-fee.
328
+
329
+ Returns ``{"replaced": bool, "old_hash": str|None, "error": str}``.
330
+ """
331
+ if not self.rbf_enabled:
332
+ return {"replaced": False, "old_hash": None,
333
+ "error": "RBF is disabled on this mempool"}
334
+ if not tx.tx_hash:
335
+ tx.compute_hash()
336
+
337
+ key = (tx.sender, tx.nonce)
338
+ old_hash = self._sender_nonce_idx.get(key)
339
+ if old_hash is None:
340
+ return {"replaced": False, "old_hash": None,
341
+ "error": "no existing tx with this sender+nonce"}
342
+ old_tx = self._txs.get(old_hash)
343
+ if old_tx is None:
344
+ return {"replaced": False, "old_hash": None,
345
+ "error": "index stale — old tx already removed"}
346
+
347
+ min_price = old_tx.gas_price * (100 + self.rbf_increment_pct) // 100
348
+ if tx.gas_price < min_price:
349
+ return {"replaced": False, "old_hash": old_hash,
350
+ "error": f"gas_price too low: need >= {min_price}, got {tx.gas_price}"}
351
+
352
+ del self._txs[old_hash]
353
+ self._txs[tx.tx_hash] = tx
354
+ self._sender_nonce_idx[key] = tx.tx_hash
355
+ return {"replaced": True, "old_hash": old_hash, "error": ""}
356
+
357
+ def get_by_sender_nonce(self, sender: str, nonce: int) -> Optional[Transaction]:
358
+ """Look up the current mempool tx for a given (sender, nonce)."""
359
+ h = self._sender_nonce_idx.get((sender, nonce))
360
+ return self._txs.get(h) if h else None
361
+
362
+ def remove(self, tx_hash: str) -> Optional[Transaction]:
363
+ """Remove a transaction from the mempool."""
364
+ tx = self._txs.pop(tx_hash, None)
365
+ if tx is not None:
366
+ key = (tx.sender, tx.nonce)
367
+ if self._sender_nonce_idx.get(key) == tx_hash:
368
+ del self._sender_nonce_idx[key]
369
+ return tx
370
+
371
+ def get_pending(self, gas_limit: int = 10_000_000) -> List[Transaction]:
372
+ """Get pending transactions ordered by gas_price, fitting within gas_limit."""
373
+ sorted_txs = sorted(self._txs.values(), key=lambda t: t.gas_price, reverse=True)
374
+ result = []
375
+ total_gas = 0
376
+ for tx in sorted_txs:
377
+ if total_gas + tx.gas_limit <= gas_limit:
378
+ result.append(tx)
379
+ total_gas += tx.gas_limit
380
+ return result
381
+
382
+ @property
383
+ def size(self) -> int:
384
+ return len(self._txs)
385
+
386
+ def clear(self):
387
+ self._txs.clear()
388
+ self._nonces.clear()
389
+ self._sender_nonce_idx.clear()
390
+
391
+
392
+ class Chain:
393
+ """The blockchain — an ordered sequence of validated blocks.
394
+
395
+ Features:
396
+ - Genesis block creation
397
+ - Block validation (hash chain, PoW, timestamps)
398
+ - Chain state management with accounts
399
+ - Persistent storage (SQLite)
400
+ - Fork detection and chain tip tracking
401
+ """
402
+
403
+ def __init__(self, chain_id: str = "zexus-mainnet",
404
+ data_dir: Optional[str] = None,
405
+ storage: Optional["StorageBackend"] = None,
406
+ storage_backend: str = "sqlite"):
407
+ """Initialise the chain.
408
+
409
+ Parameters
410
+ ----------
411
+ chain_id : str
412
+ Unique identifier for this chain.
413
+ data_dir : str, optional
414
+ On-disk directory. When provided (and *storage* is ``None``),
415
+ a storage backend is created automatically.
416
+ storage : StorageBackend, optional
417
+ A pre-configured storage backend. Takes priority over
418
+ *data_dir* / *storage_backend*.
419
+ storage_backend : str
420
+ Which backend to create when *data_dir* is given and
421
+ *storage* is not. One of ``"sqlite"`` (default),
422
+ ``"leveldb"``, ``"rocksdb"``, or ``"memory"``.
423
+ """
424
+ self.chain_id = chain_id
425
+ self.blocks: List[Block] = []
426
+ self.block_index: Dict[str, Block] = {} # hash -> Block
427
+ self.height_index: Dict[int, Block] = {} # height -> Block
428
+ self.accounts: Dict[str, Dict[str, Any]] = {} # address -> {balance, nonce, code, storage}
429
+ self.contract_state: Dict[str, Dict[str, Any]] = {} # contract_addr -> state
430
+ self.difficulty: int = 1
431
+ self.target_block_time: float = 10.0 # seconds
432
+
433
+ # Persistent storage — pluggable backend
434
+ self._data_dir = data_dir
435
+ self._storage: Optional["StorageBackend"] = storage
436
+
437
+ # Legacy compat: if no explicit storage, auto-create from data_dir
438
+ if self._storage is None and data_dir:
439
+ os.makedirs(data_dir, exist_ok=True)
440
+ backend_name = storage_backend.lower()
441
+ if backend_name == "sqlite":
442
+ self._storage = _get_storage_backend(
443
+ "sqlite", db_path=os.path.join(data_dir, "chain.db")
444
+ )
445
+ else:
446
+ self._storage = _get_storage_backend(
447
+ backend_name, db_path=os.path.join(data_dir, "chaindb")
448
+ )
449
+
450
+ # Also keep legacy _db attribute for any external code that checks it
451
+ self._db = self._storage
452
+
453
+ if self._storage is not None:
454
+ self._load_from_storage()
455
+
456
+ # -- persistence (pluggable) -----------------------------------------
457
+
458
+ def _load_from_storage(self):
459
+ """Load chain state from the configured storage backend."""
460
+ if not self._storage:
461
+ return
462
+
463
+ # Blocks — iterate in height order
464
+ for _key, value in self._storage.iterate_sorted("blocks"):
465
+ block = Block.from_dict(json.loads(value))
466
+ self.blocks.append(block)
467
+ self.block_index[block.hash] = block
468
+ self.height_index[block.header.height] = block
469
+
470
+ # Account state
471
+ for address, data in self._storage.iterate("state"):
472
+ self.accounts[address] = json.loads(data)
473
+
474
+ # Contract state
475
+ for address, data in self._storage.iterate("contract_state"):
476
+ self.contract_state[address] = json.loads(data)
477
+
478
+ def _persist_block(self, block: Block):
479
+ """Persist a single block to the backend."""
480
+ if not self._storage:
481
+ return
482
+ self._storage.put(
483
+ "blocks",
484
+ str(block.header.height),
485
+ json.dumps(block.to_dict()),
486
+ )
487
+ self._storage.commit()
488
+
489
+ def _persist_state(self):
490
+ """Persist full account & contract state to the backend."""
491
+ if not self._storage:
492
+ return
493
+ for address, data in self.accounts.items():
494
+ self._storage.put("state", address, json.dumps(data))
495
+ for address, data in self.contract_state.items():
496
+ self._storage.put(
497
+ "contract_state", address, json.dumps(data, default=str)
498
+ )
499
+ self._storage.commit()
500
+
501
+ def create_genesis(self, miner: str = "0x0000000000000000000000000000000000000000",
502
+ initial_balances: Optional[Dict[str, int]] = None) -> Block:
503
+ """Create the genesis block."""
504
+ if self.blocks:
505
+ raise RuntimeError("Genesis block already exists")
506
+
507
+ genesis = Block()
508
+ genesis.header.height = 0
509
+ genesis.header.timestamp = time.time()
510
+ genesis.header.miner = miner
511
+ genesis.header.extra_data = f"Zexus Genesis — {self.chain_id}"
512
+ genesis.header.prev_hash = "0" * 64
513
+ genesis.compute_hash()
514
+
515
+ # Initialize accounts with balances
516
+ if initial_balances:
517
+ for addr, balance in initial_balances.items():
518
+ self.accounts[addr] = {"balance": balance, "nonce": 0, "code": "", "storage": {}}
519
+
520
+ self.blocks.append(genesis)
521
+ self.block_index[genesis.hash] = genesis
522
+ self.height_index[0] = genesis
523
+ self._persist_block(genesis)
524
+ self._persist_state()
525
+ return genesis
526
+
527
+ @property
528
+ def tip(self) -> Optional[Block]:
529
+ """Get the latest block (chain tip)."""
530
+ return self.blocks[-1] if self.blocks else None
531
+
532
+ @property
533
+ def height(self) -> int:
534
+ """Current chain height."""
535
+ return len(self.blocks) - 1 if self.blocks else -1
536
+
537
+ def get_block(self, hash_or_height) -> Optional[Block]:
538
+ """Get block by hash or height."""
539
+ if isinstance(hash_or_height, int):
540
+ return self.height_index.get(hash_or_height)
541
+ return self.block_index.get(hash_or_height)
542
+
543
+ def get_account(self, address: str) -> Dict[str, Any]:
544
+ """Get account state, creating if needed."""
545
+ if address not in self.accounts:
546
+ self.accounts[address] = {"balance": 0, "nonce": 0, "code": "", "storage": {}}
547
+ return self.accounts[address]
548
+
549
+ def add_block(self, block: Block) -> Tuple[bool, str]:
550
+ """Validate and add a block to the chain.
551
+
552
+ Returns (success, error_message).
553
+ """
554
+ if not self.blocks:
555
+ return False, "No genesis block — call create_genesis() first"
556
+
557
+ tip = self.tip
558
+
559
+ # Validate parent hash
560
+ if block.header.prev_hash != tip.hash:
561
+ return False, f"Invalid prev_hash: expected {tip.hash}, got {block.header.prev_hash}"
562
+
563
+ # Validate height
564
+ expected_height = tip.header.height + 1
565
+ if block.header.height != expected_height:
566
+ return False, f"Invalid height: expected {expected_height}, got {block.header.height}"
567
+
568
+ # Validate timestamp
569
+ if block.header.timestamp <= tip.header.timestamp:
570
+ return False, "Block timestamp must be after parent"
571
+
572
+ # Validate hash
573
+ computed = block.header.compute_hash()
574
+ if block.hash != computed:
575
+ return False, f"Invalid block hash: expected {computed}, got {block.hash}"
576
+
577
+ # Validate PoW (if difficulty > 0)
578
+ if self.difficulty > 0:
579
+ target = "0" * self.difficulty
580
+ if not block.hash.startswith(target):
581
+ return False, f"Block hash does not meet difficulty {self.difficulty}"
582
+
583
+ # Validate transactions
584
+ for tx in block.transactions:
585
+ if not tx.tx_hash:
586
+ return False, f"Transaction missing hash"
587
+ if not tx.signature:
588
+ return False, f"Transaction {tx.tx_hash[:16]} has no signature"
589
+
590
+ # Validate tx root
591
+ expected_root = block.compute_tx_root()
592
+ if block.header.tx_root and block.header.tx_root != expected_root:
593
+ return False, f"Invalid tx_root"
594
+
595
+ # Add block
596
+ self.blocks.append(block)
597
+ self.block_index[block.hash] = block
598
+ self.height_index[block.header.height] = block
599
+
600
+ # Process transactions — update account state
601
+ for i, tx in enumerate(block.transactions):
602
+ receipt = block.receipts[i] if i < len(block.receipts) else None
603
+ if receipt and receipt.status == 1:
604
+ # Debit sender
605
+ sender_acct = self.get_account(tx.sender)
606
+ sender_acct["balance"] -= tx.value + (tx.gas_limit * tx.gas_price)
607
+ sender_acct["nonce"] = max(sender_acct["nonce"], tx.nonce + 1)
608
+
609
+ # Credit recipient
610
+ if tx.recipient:
611
+ recv_acct = self.get_account(tx.recipient)
612
+ recv_acct["balance"] += tx.value
613
+
614
+ # Adjust difficulty
615
+ self._adjust_difficulty()
616
+
617
+ self._persist_block(block)
618
+ self._persist_state()
619
+ return True, ""
620
+
621
+ def _adjust_difficulty(self):
622
+ """Adjust mining difficulty based on block time."""
623
+ if len(self.blocks) < 3:
624
+ return
625
+ last_two = self.blocks[-2:]
626
+ actual_time = last_two[1].header.timestamp - last_two[0].header.timestamp
627
+ if actual_time < self.target_block_time * 0.5:
628
+ self.difficulty = min(self.difficulty + 1, 8)
629
+ elif actual_time > self.target_block_time * 2.0:
630
+ self.difficulty = max(self.difficulty - 1, 1)
631
+
632
+ def validate_chain(self) -> Tuple[bool, str]:
633
+ """Validate the entire chain integrity."""
634
+ for i, block in enumerate(self.blocks):
635
+ computed = block.header.compute_hash()
636
+ if block.hash != computed:
637
+ return False, f"Block {i} hash mismatch"
638
+ if i > 0:
639
+ if block.header.prev_hash != self.blocks[i - 1].hash:
640
+ return False, f"Block {i} prev_hash mismatch"
641
+ if block.header.height != i:
642
+ return False, f"Block {i} height mismatch"
643
+ return True, ""
644
+
645
+ def get_chain_info(self) -> Dict[str, Any]:
646
+ """Get chain status information."""
647
+ return {
648
+ "chain_id": self.chain_id,
649
+ "height": self.height,
650
+ "difficulty": self.difficulty,
651
+ "tip_hash": self.tip.hash if self.tip else None,
652
+ "total_accounts": len(self.accounts),
653
+ "total_blocks": len(self.blocks),
654
+ }
655
+
656
+ def close(self):
657
+ """Close persistent storage."""
658
+ if self._db:
659
+ self._db.close()
660
+ self._db = None