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,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
|