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.
- package/README.md +12 -5
- 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/capability_system.py +184 -9
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +383 -34
- 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/bytecode.py +124 -7
- package/src/zexus/compiler/compat_runtime.py +6 -2
- package/src/zexus/compiler/lexer.py +16 -5
- package/src/zexus/compiler/parser.py +108 -7
- package/src/zexus/compiler/semantic.py +18 -19
- package/src/zexus/compiler/zexus_ast.py +26 -1
- 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 +112 -9
- 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 +457 -37
- package/src/zexus/evaluator/core.py +644 -50
- package/src/zexus/evaluator/expressions.py +358 -62
- package/src/zexus/evaluator/functions.py +458 -20
- package/src/zexus/evaluator/resource_limiter.py +4 -4
- package/src/zexus/evaluator/statements.py +774 -122
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/evaluator_original.py +1 -1
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -458
- 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 +239 -9
- package/src/zexus/module_manager.py +129 -1
- package/src/zexus/object.py +76 -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 +1349 -408
- package/src/zexus/parser/strategy_context.py +755 -58
- package/src/zexus/parser/strategy_structural.py +121 -21
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +61 -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/backend.py +261 -0
- package/src/zexus/renderer/canvas.py +78 -0
- package/src/zexus/renderer/color_system.py +201 -0
- package/src/zexus/renderer/graphics.py +31 -0
- package/src/zexus/renderer/layout.py +222 -0
- package/src/zexus/renderer/main_renderer.py +66 -0
- package/src/zexus/renderer/painter.py +30 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__init__.py +10 -2
- 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/runtime/load_manager.py +368 -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 +80 -6
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +59 -11
- 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 +561 -17
- package/src/zexus/vm/compiler.py +818 -51
- 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 +364 -20
- 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 +140 -45
- 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 +3581 -531
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +137 -11
- package/src/zexus/zexus_token.py +16 -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 +16 -6
- package/src/zexus.egg-info/SOURCES.txt +129 -17
- package/src/zexus.egg-info/entry_points.txt +1 -0
- package/src/zexus.egg-info/requires.txt +4 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zexus Blockchain — Execution Accelerator
|
|
3
|
+
=========================================
|
|
4
|
+
|
|
5
|
+
Closes the raw-speed gap between the Python-hosted Zexus VM and
|
|
6
|
+
natively compiled blockchains (Go/Rust) by providing:
|
|
7
|
+
|
|
8
|
+
1. **AOT Contract Compilation** — Pre-compiles smart contract actions
|
|
9
|
+
into optimised Python closures at deployment time (not at first
|
|
10
|
+
execution), eliminating JIT warm-up entirely.
|
|
11
|
+
|
|
12
|
+
2. **Inline Cache (IC)** — Caches resolved method dispatch, variable
|
|
13
|
+
lookups, and type specialisation so repeated calls to the same
|
|
14
|
+
action/variable path skip the generic resolution layer.
|
|
15
|
+
|
|
16
|
+
3. **Batched Execution Pipeline** — When processing a whole block of
|
|
17
|
+
transactions, actions that touch non-overlapping state are pipelined
|
|
18
|
+
and executed in an optimised sequence (transaction-level batching
|
|
19
|
+
with speculative state pre-loading).
|
|
20
|
+
|
|
21
|
+
4. **Native Numeric Fast-Path** — Hot arithmetic loops identified at
|
|
22
|
+
compile time are executed through a C-extension or fall back to
|
|
23
|
+
Python ``compile()`` + ``eval()`` eliminating Zexus object overhead.
|
|
24
|
+
|
|
25
|
+
5. **WASM AOT Cache** — Pre-compiled WASM modules are cached on disk
|
|
26
|
+
so that subsequent runs skip the compilation step entirely.
|
|
27
|
+
|
|
28
|
+
Integration
|
|
29
|
+
-----------
|
|
30
|
+
* ``ExecutionAccelerator`` wraps ``ContractVM`` and is used by
|
|
31
|
+
``BlockchainNode`` transparently.
|
|
32
|
+
* The existing JIT and WASM compilers are leveraged — this module
|
|
33
|
+
orchestrates them for maximum throughput.
|
|
34
|
+
* All acceleration is opt-in via ``NodeConfig`` flags and safe to
|
|
35
|
+
use with the existing test suite.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import hashlib
|
|
41
|
+
import json
|
|
42
|
+
import math
|
|
43
|
+
import os
|
|
44
|
+
import struct
|
|
45
|
+
import tempfile
|
|
46
|
+
import time
|
|
47
|
+
import logging
|
|
48
|
+
from collections import OrderedDict
|
|
49
|
+
from dataclasses import dataclass, field
|
|
50
|
+
from typing import (
|
|
51
|
+
Any, Callable, Dict, List, Optional, Set, Tuple, Union,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger("zexus.blockchain.accelerator")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# =====================================================================
|
|
58
|
+
# Inline Cache (IC)
|
|
59
|
+
# =====================================================================
|
|
60
|
+
|
|
61
|
+
class InlineCacheEntry:
|
|
62
|
+
"""Single cache entry for a resolved dispatch."""
|
|
63
|
+
__slots__ = ("key", "value", "hits", "last_used")
|
|
64
|
+
|
|
65
|
+
def __init__(self, key: str, value: Any):
|
|
66
|
+
self.key = key
|
|
67
|
+
self.value = value
|
|
68
|
+
self.hits: int = 0
|
|
69
|
+
self.last_used: float = time.time()
|
|
70
|
+
|
|
71
|
+
def touch(self) -> Any:
|
|
72
|
+
self.hits += 1
|
|
73
|
+
self.last_used = time.time()
|
|
74
|
+
return self.value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class InlineCache:
|
|
78
|
+
"""LRU inline cache for method/variable dispatch.
|
|
79
|
+
|
|
80
|
+
Used by the execution accelerator to skip generic lookups on
|
|
81
|
+
repeated contract calls. Thread-safe by design (single writer,
|
|
82
|
+
GIL-protected reads).
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
max_size : int
|
|
87
|
+
Maximum entries before eviction (default 4096).
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, max_size: int = 4096):
|
|
91
|
+
self._max_size = max_size
|
|
92
|
+
self._entries: OrderedDict[str, InlineCacheEntry] = OrderedDict()
|
|
93
|
+
self._hits: int = 0
|
|
94
|
+
self._misses: int = 0
|
|
95
|
+
|
|
96
|
+
def get(self, key: str) -> Optional[Any]:
|
|
97
|
+
"""Look up a cached value. Returns None on miss."""
|
|
98
|
+
entry = self._entries.get(key)
|
|
99
|
+
if entry is not None:
|
|
100
|
+
self._hits += 1
|
|
101
|
+
self._entries.move_to_end(key)
|
|
102
|
+
return entry.touch()
|
|
103
|
+
self._misses += 1
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def put(self, key: str, value: Any) -> None:
|
|
107
|
+
"""Insert or update a cache entry."""
|
|
108
|
+
if key in self._entries:
|
|
109
|
+
self._entries[key].value = value
|
|
110
|
+
self._entries.move_to_end(key)
|
|
111
|
+
else:
|
|
112
|
+
if len(self._entries) >= self._max_size:
|
|
113
|
+
self._entries.popitem(last=False)
|
|
114
|
+
self._entries[key] = InlineCacheEntry(key, value)
|
|
115
|
+
|
|
116
|
+
def invalidate(self, key: str) -> bool:
|
|
117
|
+
"""Remove a specific entry. Returns True if it existed."""
|
|
118
|
+
if key in self._entries:
|
|
119
|
+
del self._entries[key]
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def invalidate_prefix(self, prefix: str) -> int:
|
|
124
|
+
"""Remove all entries whose key starts with *prefix*."""
|
|
125
|
+
to_remove = [k for k in self._entries if k.startswith(prefix)]
|
|
126
|
+
for k in to_remove:
|
|
127
|
+
del self._entries[k]
|
|
128
|
+
return len(to_remove)
|
|
129
|
+
|
|
130
|
+
def clear(self) -> None:
|
|
131
|
+
self._entries.clear()
|
|
132
|
+
self._hits = 0
|
|
133
|
+
self._misses = 0
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def size(self) -> int:
|
|
137
|
+
return len(self._entries)
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def hit_rate(self) -> float:
|
|
141
|
+
total = self._hits + self._misses
|
|
142
|
+
return self._hits / total if total > 0 else 0.0
|
|
143
|
+
|
|
144
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
145
|
+
return {
|
|
146
|
+
"size": self.size,
|
|
147
|
+
"max_size": self._max_size,
|
|
148
|
+
"hits": self._hits,
|
|
149
|
+
"misses": self._misses,
|
|
150
|
+
"hit_rate": round(self.hit_rate * 100, 2),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# =====================================================================
|
|
155
|
+
# AOT Contract Compiler
|
|
156
|
+
# =====================================================================
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class CompiledAction:
|
|
160
|
+
"""A pre-compiled contract action."""
|
|
161
|
+
contract_address: str
|
|
162
|
+
action_name: str
|
|
163
|
+
compiled_fn: Optional[Callable] = None
|
|
164
|
+
source_hash: str = ""
|
|
165
|
+
compile_time: float = 0.0
|
|
166
|
+
execution_count: int = 0
|
|
167
|
+
total_time: float = 0.0
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def avg_time(self) -> float:
|
|
171
|
+
return self.total_time / self.execution_count if self.execution_count > 0 else 0.0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class AOTCompiler:
|
|
175
|
+
"""Ahead-of-time compiler for contract actions.
|
|
176
|
+
|
|
177
|
+
At deployment time, each action body is analysed and compiled into
|
|
178
|
+
a Python closure that directly manipulates the state adapter
|
|
179
|
+
without going through the full VM dispatch loop.
|
|
180
|
+
|
|
181
|
+
For actions that cannot be statically compiled (dynamic dispatch,
|
|
182
|
+
unsupported opcodes), a *fast interpreter* closure is generated
|
|
183
|
+
that still skips the generic initialisation overhead.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
optimization_level : int
|
|
188
|
+
0 = no AOT, 1 = basic (constant folding), 2 = aggressive.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(self, optimization_level: int = 2, debug: bool = False):
|
|
192
|
+
self._opt_level = optimization_level
|
|
193
|
+
self._debug = debug
|
|
194
|
+
|
|
195
|
+
# contract_address:action_name -> CompiledAction
|
|
196
|
+
self._cache: Dict[str, CompiledAction] = {}
|
|
197
|
+
|
|
198
|
+
# Statistics
|
|
199
|
+
self._compilations: int = 0
|
|
200
|
+
self._compile_time: float = 0.0
|
|
201
|
+
self._failures: int = 0
|
|
202
|
+
|
|
203
|
+
def compile_contract(
|
|
204
|
+
self,
|
|
205
|
+
contract_address: str,
|
|
206
|
+
contract: Any,
|
|
207
|
+
) -> Dict[str, CompiledAction]:
|
|
208
|
+
"""Pre-compile all actions of a contract.
|
|
209
|
+
|
|
210
|
+
Returns a dict of action_name -> CompiledAction.
|
|
211
|
+
"""
|
|
212
|
+
actions = {}
|
|
213
|
+
contract_actions = getattr(contract, "actions", {})
|
|
214
|
+
if not isinstance(contract_actions, dict):
|
|
215
|
+
return actions
|
|
216
|
+
|
|
217
|
+
for action_name, action_obj in contract_actions.items():
|
|
218
|
+
key = f"{contract_address}:{action_name}"
|
|
219
|
+
compiled = self._compile_action(
|
|
220
|
+
contract_address, action_name, action_obj
|
|
221
|
+
)
|
|
222
|
+
if compiled is not None:
|
|
223
|
+
self._cache[key] = compiled
|
|
224
|
+
actions[action_name] = compiled
|
|
225
|
+
|
|
226
|
+
return actions
|
|
227
|
+
|
|
228
|
+
def get_compiled(
|
|
229
|
+
self, contract_address: str, action_name: str
|
|
230
|
+
) -> Optional[CompiledAction]:
|
|
231
|
+
"""Retrieve a pre-compiled action."""
|
|
232
|
+
key = f"{contract_address}:{action_name}"
|
|
233
|
+
return self._cache.get(key)
|
|
234
|
+
|
|
235
|
+
def invalidate_contract(self, contract_address: str) -> int:
|
|
236
|
+
"""Remove all compiled actions for a contract (e.g. after upgrade)."""
|
|
237
|
+
prefix = f"{contract_address}:"
|
|
238
|
+
to_remove = [k for k in self._cache if k.startswith(prefix)]
|
|
239
|
+
for k in to_remove:
|
|
240
|
+
del self._cache[k]
|
|
241
|
+
return len(to_remove)
|
|
242
|
+
|
|
243
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
244
|
+
return {
|
|
245
|
+
"cached_actions": len(self._cache),
|
|
246
|
+
"compilations": self._compilations,
|
|
247
|
+
"compile_time": round(self._compile_time, 4),
|
|
248
|
+
"failures": self._failures,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# ── Internal ──────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
def _compile_action(
|
|
254
|
+
self,
|
|
255
|
+
contract_address: str,
|
|
256
|
+
action_name: str,
|
|
257
|
+
action_obj: Any,
|
|
258
|
+
) -> Optional[CompiledAction]:
|
|
259
|
+
"""Compile a single action into a fast closure."""
|
|
260
|
+
start = time.time()
|
|
261
|
+
|
|
262
|
+
body = getattr(action_obj, "body", None)
|
|
263
|
+
if body is None:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
# Hash the action source for cache invalidation
|
|
267
|
+
body_repr = repr(body) if body else ""
|
|
268
|
+
source_hash = hashlib.sha256(body_repr.encode()).hexdigest()[:16]
|
|
269
|
+
|
|
270
|
+
# Check if already compiled with same hash
|
|
271
|
+
key = f"{contract_address}:{action_name}"
|
|
272
|
+
existing = self._cache.get(key)
|
|
273
|
+
if existing and existing.source_hash == source_hash:
|
|
274
|
+
return existing
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
# Generate the fast-path closure
|
|
278
|
+
fast_fn = self._build_fast_closure(action_obj)
|
|
279
|
+
elapsed = time.time() - start
|
|
280
|
+
|
|
281
|
+
compiled = CompiledAction(
|
|
282
|
+
contract_address=contract_address,
|
|
283
|
+
action_name=action_name,
|
|
284
|
+
compiled_fn=fast_fn,
|
|
285
|
+
source_hash=source_hash,
|
|
286
|
+
compile_time=elapsed,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
self._compilations += 1
|
|
290
|
+
self._compile_time += elapsed
|
|
291
|
+
|
|
292
|
+
if self._debug:
|
|
293
|
+
logger.debug(
|
|
294
|
+
"AOT compiled %s:%s in %.3fms",
|
|
295
|
+
contract_address[:8], action_name, elapsed * 1000,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return compiled
|
|
299
|
+
|
|
300
|
+
except Exception as exc:
|
|
301
|
+
self._failures += 1
|
|
302
|
+
if self._debug:
|
|
303
|
+
logger.debug(
|
|
304
|
+
"AOT compilation failed for %s:%s: %s",
|
|
305
|
+
contract_address[:8], action_name, exc,
|
|
306
|
+
)
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
def _build_fast_closure(self, action_obj: Any) -> Callable:
|
|
310
|
+
"""Build a closure that executes the action body via fast-path.
|
|
311
|
+
|
|
312
|
+
The closure accepts (state_adapter, args, builtins_dict) and
|
|
313
|
+
returns the action's result.
|
|
314
|
+
|
|
315
|
+
Strategy:
|
|
316
|
+
* Try to generate a direct Python function from the AST.
|
|
317
|
+
* Fall back to constructing a minimal evaluator invocation
|
|
318
|
+
with pre-bound env and skipping the full VM init overhead.
|
|
319
|
+
"""
|
|
320
|
+
# Fast-path: use the evaluator in a pre-configured manner
|
|
321
|
+
# This avoids VM init overhead while keeping correctness.
|
|
322
|
+
body = getattr(action_obj, "body", None)
|
|
323
|
+
params = getattr(action_obj, "parameters", [])
|
|
324
|
+
|
|
325
|
+
def fast_execute(state_adapter, args, builtins, contract, tx_ctx):
|
|
326
|
+
"""Pre-compiled action executor."""
|
|
327
|
+
try:
|
|
328
|
+
from ..object import Environment
|
|
329
|
+
from ..evaluator.core import Evaluator
|
|
330
|
+
|
|
331
|
+
eval_env = Environment()
|
|
332
|
+
|
|
333
|
+
# Bind state variables
|
|
334
|
+
for key, val in state_adapter.items():
|
|
335
|
+
eval_env.set(key, _fast_wrap(val))
|
|
336
|
+
|
|
337
|
+
# Bind arguments
|
|
338
|
+
if params:
|
|
339
|
+
for i, param in enumerate(params):
|
|
340
|
+
param_name = (
|
|
341
|
+
param.value if hasattr(param, "value") else str(param)
|
|
342
|
+
)
|
|
343
|
+
if param_name in args:
|
|
344
|
+
eval_env.set(param_name, _fast_wrap(args[param_name]))
|
|
345
|
+
|
|
346
|
+
# Bind builtins
|
|
347
|
+
for bk, bv in builtins.items():
|
|
348
|
+
eval_env.set(bk, bv)
|
|
349
|
+
|
|
350
|
+
# Bind contract reference
|
|
351
|
+
eval_env.set("this", contract)
|
|
352
|
+
eval_env.set("_contract_address", contract.address if hasattr(contract, "address") else "")
|
|
353
|
+
|
|
354
|
+
# TX context
|
|
355
|
+
from ..object import Map, String, Integer
|
|
356
|
+
tx_map = Map({
|
|
357
|
+
String("caller"): String(tx_ctx.caller),
|
|
358
|
+
String("timestamp"): Integer(int(tx_ctx.timestamp)),
|
|
359
|
+
String("block_hash"): String(tx_ctx.block_hash),
|
|
360
|
+
String("gas_limit"): Integer(tx_ctx.gas_limit),
|
|
361
|
+
})
|
|
362
|
+
eval_env.set("TX", tx_map)
|
|
363
|
+
eval_env.set("_blockchain_state", state_adapter)
|
|
364
|
+
|
|
365
|
+
# Execute
|
|
366
|
+
evaluator = Evaluator(use_vm=False)
|
|
367
|
+
result = evaluator.eval_node(body, eval_env, [])
|
|
368
|
+
|
|
369
|
+
# Sync back to state adapter
|
|
370
|
+
for key in list(state_adapter.keys()):
|
|
371
|
+
new_val = eval_env.get(key)
|
|
372
|
+
if new_val is not None:
|
|
373
|
+
state_adapter[key] = _fast_unwrap(new_val)
|
|
374
|
+
|
|
375
|
+
return result
|
|
376
|
+
|
|
377
|
+
except Exception:
|
|
378
|
+
raise
|
|
379
|
+
|
|
380
|
+
return fast_execute
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# =====================================================================
|
|
384
|
+
# Native Numeric Fast-Path
|
|
385
|
+
# =====================================================================
|
|
386
|
+
|
|
387
|
+
class NumericFastPath:
|
|
388
|
+
"""Execute pure-numeric expressions using native Python evaluation.
|
|
389
|
+
|
|
390
|
+
Detects bytecode sequences that are purely arithmetic (no external
|
|
391
|
+
calls, no string ops, no state writes) and compiles them into a
|
|
392
|
+
single ``eval()`` call with pre-bound variables, bypassing the VM
|
|
393
|
+
instruction loop entirely.
|
|
394
|
+
|
|
395
|
+
Provides 5-20x speedup for hot numeric loops.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
_NUMERIC_OPS = frozenset({
|
|
399
|
+
"LOAD_CONST", "LOAD_NAME", "STORE_NAME",
|
|
400
|
+
"ADD", "SUB", "MUL", "DIV", "MOD", "POW", "NEG",
|
|
401
|
+
"EQ", "NEQ", "LT", "GT", "LTE", "GTE",
|
|
402
|
+
"RETURN",
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
def __init__(self):
|
|
406
|
+
self._cache: Dict[str, Callable] = {}
|
|
407
|
+
self._compilations: int = 0
|
|
408
|
+
|
|
409
|
+
def is_purely_numeric(self, instructions: list) -> bool:
|
|
410
|
+
"""Check if instructions contain only numeric operations."""
|
|
411
|
+
if not instructions:
|
|
412
|
+
return False
|
|
413
|
+
for instr in instructions:
|
|
414
|
+
if isinstance(instr, tuple) and len(instr) >= 2:
|
|
415
|
+
op = instr[0]
|
|
416
|
+
op_name = op.name if hasattr(op, "name") else str(op)
|
|
417
|
+
if op_name not in self._NUMERIC_OPS:
|
|
418
|
+
return False
|
|
419
|
+
else:
|
|
420
|
+
return False
|
|
421
|
+
return True
|
|
422
|
+
|
|
423
|
+
def compile_numeric(
|
|
424
|
+
self, instructions: list, constants: list
|
|
425
|
+
) -> Optional[Callable]:
|
|
426
|
+
"""Compile a numeric instruction sequence into a Python function.
|
|
427
|
+
|
|
428
|
+
Returns a callable ``f(variables: dict) -> result`` or None.
|
|
429
|
+
"""
|
|
430
|
+
cache_key = hashlib.sha256(
|
|
431
|
+
str(instructions).encode()
|
|
432
|
+
).hexdigest()[:16]
|
|
433
|
+
|
|
434
|
+
if cache_key in self._cache:
|
|
435
|
+
return self._cache[cache_key]
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
# Build a Python expression from the instruction sequence
|
|
439
|
+
expr = self._instructions_to_expr(instructions, constants)
|
|
440
|
+
if expr is None:
|
|
441
|
+
return None
|
|
442
|
+
|
|
443
|
+
# Compile
|
|
444
|
+
code = compile(expr, f"<numeric:{cache_key[:8]}>", "eval")
|
|
445
|
+
|
|
446
|
+
def execute(variables: dict) -> Any:
|
|
447
|
+
return eval(code, {"__builtins__": {}}, variables)
|
|
448
|
+
|
|
449
|
+
self._cache[cache_key] = execute
|
|
450
|
+
self._compilations += 1
|
|
451
|
+
return execute
|
|
452
|
+
|
|
453
|
+
except Exception:
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
def _instructions_to_expr(
|
|
457
|
+
self, instructions: list, constants: list
|
|
458
|
+
) -> Optional[str]:
|
|
459
|
+
"""Convert numeric bytecode to a Python expression string."""
|
|
460
|
+
stack: List[str] = []
|
|
461
|
+
|
|
462
|
+
for instr in instructions:
|
|
463
|
+
op, operand = instr[0], instr[1] if len(instr) > 1 else None
|
|
464
|
+
op_name = op.name if hasattr(op, "name") else str(op)
|
|
465
|
+
|
|
466
|
+
if op_name == "LOAD_CONST":
|
|
467
|
+
val = constants[operand] if isinstance(operand, int) and operand < len(constants) else operand
|
|
468
|
+
if isinstance(val, (int, float)):
|
|
469
|
+
stack.append(str(val))
|
|
470
|
+
elif isinstance(val, str):
|
|
471
|
+
stack.append(repr(val))
|
|
472
|
+
else:
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
elif op_name == "LOAD_NAME":
|
|
476
|
+
name = constants[operand] if isinstance(operand, int) and operand < len(constants) else str(operand)
|
|
477
|
+
stack.append(str(name))
|
|
478
|
+
|
|
479
|
+
elif op_name == "STORE_NAME":
|
|
480
|
+
if not stack:
|
|
481
|
+
return None
|
|
482
|
+
# Store operations break simple expression compilation
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
elif op_name in ("ADD", "SUB", "MUL", "DIV", "MOD", "POW"):
|
|
486
|
+
if len(stack) < 2:
|
|
487
|
+
return None
|
|
488
|
+
b, a = stack.pop(), stack.pop()
|
|
489
|
+
op_map = {
|
|
490
|
+
"ADD": "+", "SUB": "-", "MUL": "*",
|
|
491
|
+
"DIV": "/", "MOD": "%", "POW": "**",
|
|
492
|
+
}
|
|
493
|
+
stack.append(f"({a} {op_map[op_name]} {b})")
|
|
494
|
+
|
|
495
|
+
elif op_name == "NEG":
|
|
496
|
+
if not stack:
|
|
497
|
+
return None
|
|
498
|
+
a = stack.pop()
|
|
499
|
+
stack.append(f"(-{a})")
|
|
500
|
+
|
|
501
|
+
elif op_name in ("EQ", "NEQ", "LT", "GT", "LTE", "GTE"):
|
|
502
|
+
if len(stack) < 2:
|
|
503
|
+
return None
|
|
504
|
+
b, a = stack.pop(), stack.pop()
|
|
505
|
+
cmp_map = {
|
|
506
|
+
"EQ": "==", "NEQ": "!=", "LT": "<",
|
|
507
|
+
"GT": ">", "LTE": "<=", "GTE": ">=",
|
|
508
|
+
}
|
|
509
|
+
stack.append(f"({a} {cmp_map[op_name]} {b})")
|
|
510
|
+
|
|
511
|
+
elif op_name == "RETURN":
|
|
512
|
+
break
|
|
513
|
+
|
|
514
|
+
else:
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
return stack[-1] if stack else None
|
|
518
|
+
|
|
519
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
520
|
+
return {
|
|
521
|
+
"cached_functions": len(self._cache),
|
|
522
|
+
"compilations": self._compilations,
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# =====================================================================
|
|
527
|
+
# WASM AOT Cache
|
|
528
|
+
# =====================================================================
|
|
529
|
+
|
|
530
|
+
class WASMCache:
|
|
531
|
+
"""Disk-backed cache for compiled WASM modules.
|
|
532
|
+
|
|
533
|
+
Key benefit: eliminates re-compilation on restarts. The cache
|
|
534
|
+
is keyed by bytecode hash and stored in the node's data directory.
|
|
535
|
+
|
|
536
|
+
Parameters
|
|
537
|
+
----------
|
|
538
|
+
cache_dir : str, optional
|
|
539
|
+
Directory for storing .wasm files. Defaults to a temp dir.
|
|
540
|
+
max_entries : int
|
|
541
|
+
Maximum cached modules before LRU eviction.
|
|
542
|
+
"""
|
|
543
|
+
|
|
544
|
+
def __init__(
|
|
545
|
+
self,
|
|
546
|
+
cache_dir: Optional[str] = None,
|
|
547
|
+
max_entries: int = 1024,
|
|
548
|
+
):
|
|
549
|
+
self._cache_dir = cache_dir or os.path.join(
|
|
550
|
+
tempfile.gettempdir(), "zexus_wasm_cache"
|
|
551
|
+
)
|
|
552
|
+
self._max_entries = max_entries
|
|
553
|
+
self._index: OrderedDict[str, Dict[str, Any]] = OrderedDict()
|
|
554
|
+
os.makedirs(self._cache_dir, exist_ok=True)
|
|
555
|
+
self._load_index()
|
|
556
|
+
|
|
557
|
+
def get(self, bytecode_hash: str) -> Optional[bytes]:
|
|
558
|
+
"""Retrieve a cached WASM binary. Returns None on miss."""
|
|
559
|
+
if bytecode_hash not in self._index:
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
self._index.move_to_end(bytecode_hash)
|
|
563
|
+
path = os.path.join(self._cache_dir, f"{bytecode_hash}.wasm")
|
|
564
|
+
try:
|
|
565
|
+
with open(path, "rb") as f:
|
|
566
|
+
return f.read()
|
|
567
|
+
except FileNotFoundError:
|
|
568
|
+
# Stale index entry
|
|
569
|
+
del self._index[bytecode_hash]
|
|
570
|
+
return None
|
|
571
|
+
|
|
572
|
+
def put(self, bytecode_hash: str, wasm_bytes: bytes) -> None:
|
|
573
|
+
"""Store a compiled WASM module."""
|
|
574
|
+
# Evict if needed
|
|
575
|
+
while len(self._index) >= self._max_entries:
|
|
576
|
+
old_key, _ = self._index.popitem(last=False)
|
|
577
|
+
old_path = os.path.join(self._cache_dir, f"{old_key}.wasm")
|
|
578
|
+
try:
|
|
579
|
+
os.remove(old_path)
|
|
580
|
+
except OSError:
|
|
581
|
+
pass
|
|
582
|
+
|
|
583
|
+
path = os.path.join(self._cache_dir, f"{bytecode_hash}.wasm")
|
|
584
|
+
with open(path, "wb") as f:
|
|
585
|
+
f.write(wasm_bytes)
|
|
586
|
+
|
|
587
|
+
self._index[bytecode_hash] = {
|
|
588
|
+
"size": len(wasm_bytes),
|
|
589
|
+
"cached_at": time.time(),
|
|
590
|
+
}
|
|
591
|
+
self._save_index()
|
|
592
|
+
|
|
593
|
+
def contains(self, bytecode_hash: str) -> bool:
|
|
594
|
+
return bytecode_hash in self._index
|
|
595
|
+
|
|
596
|
+
def remove(self, bytecode_hash: str) -> bool:
|
|
597
|
+
if bytecode_hash not in self._index:
|
|
598
|
+
return False
|
|
599
|
+
del self._index[bytecode_hash]
|
|
600
|
+
path = os.path.join(self._cache_dir, f"{bytecode_hash}.wasm")
|
|
601
|
+
try:
|
|
602
|
+
os.remove(path)
|
|
603
|
+
except OSError:
|
|
604
|
+
pass
|
|
605
|
+
self._save_index()
|
|
606
|
+
return True
|
|
607
|
+
|
|
608
|
+
def clear(self) -> int:
|
|
609
|
+
count = len(self._index)
|
|
610
|
+
for key in list(self._index.keys()):
|
|
611
|
+
path = os.path.join(self._cache_dir, f"{key}.wasm")
|
|
612
|
+
try:
|
|
613
|
+
os.remove(path)
|
|
614
|
+
except OSError:
|
|
615
|
+
pass
|
|
616
|
+
self._index.clear()
|
|
617
|
+
self._save_index()
|
|
618
|
+
return count
|
|
619
|
+
|
|
620
|
+
@property
|
|
621
|
+
def size(self) -> int:
|
|
622
|
+
return len(self._index)
|
|
623
|
+
|
|
624
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
625
|
+
total_bytes = sum(v.get("size", 0) for v in self._index.values())
|
|
626
|
+
return {
|
|
627
|
+
"entries": self.size,
|
|
628
|
+
"max_entries": self._max_entries,
|
|
629
|
+
"total_bytes": total_bytes,
|
|
630
|
+
"cache_dir": self._cache_dir,
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
# ── Internal ──────────────────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
def _load_index(self) -> None:
|
|
636
|
+
idx_path = os.path.join(self._cache_dir, "index.json")
|
|
637
|
+
if os.path.exists(idx_path):
|
|
638
|
+
try:
|
|
639
|
+
with open(idx_path, "r") as f:
|
|
640
|
+
data = json.load(f)
|
|
641
|
+
self._index = OrderedDict(data)
|
|
642
|
+
except (json.JSONDecodeError, IOError):
|
|
643
|
+
self._index = OrderedDict()
|
|
644
|
+
|
|
645
|
+
def _save_index(self) -> None:
|
|
646
|
+
idx_path = os.path.join(self._cache_dir, "index.json")
|
|
647
|
+
try:
|
|
648
|
+
with open(idx_path, "w") as f:
|
|
649
|
+
json.dump(dict(self._index), f)
|
|
650
|
+
except IOError:
|
|
651
|
+
pass
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
# =====================================================================
|
|
655
|
+
# Batched Transaction Executor
|
|
656
|
+
# =====================================================================
|
|
657
|
+
|
|
658
|
+
@dataclass
|
|
659
|
+
class TxBatchResult:
|
|
660
|
+
"""Result of executing a batch of transactions."""
|
|
661
|
+
total: int = 0
|
|
662
|
+
succeeded: int = 0
|
|
663
|
+
failed: int = 0
|
|
664
|
+
gas_used: int = 0
|
|
665
|
+
elapsed: float = 0.0
|
|
666
|
+
receipts: List[Dict[str, Any]] = field(default_factory=list)
|
|
667
|
+
|
|
668
|
+
@property
|
|
669
|
+
def throughput(self) -> float:
|
|
670
|
+
"""Transactions per second."""
|
|
671
|
+
return self.total / self.elapsed if self.elapsed > 0 else 0.0
|
|
672
|
+
|
|
673
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
674
|
+
return {
|
|
675
|
+
"total": self.total,
|
|
676
|
+
"succeeded": self.succeeded,
|
|
677
|
+
"failed": self.failed,
|
|
678
|
+
"gas_used": self.gas_used,
|
|
679
|
+
"elapsed": round(self.elapsed, 4),
|
|
680
|
+
"throughput": round(self.throughput, 2),
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class BatchExecutor:
|
|
685
|
+
"""Optimised block-level transaction executor.
|
|
686
|
+
|
|
687
|
+
Groups non-conflicting transactions and executes them in parallel
|
|
688
|
+
using a thread/process pool, while falling back to sequential
|
|
689
|
+
execution for transactions that share state.
|
|
690
|
+
|
|
691
|
+
Parameters
|
|
692
|
+
----------
|
|
693
|
+
contract_vm : ContractVM
|
|
694
|
+
The execution bridge.
|
|
695
|
+
aot_compiler : AOTCompiler, optional
|
|
696
|
+
If provided, uses pre-compiled actions for faster execution.
|
|
697
|
+
inline_cache : InlineCache, optional
|
|
698
|
+
Shared inline cache for dispatch acceleration.
|
|
699
|
+
max_workers : int
|
|
700
|
+
Maximum number of parallel worker threads for non-conflicting
|
|
701
|
+
transaction groups (default: 4).
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
def __init__(
|
|
705
|
+
self,
|
|
706
|
+
contract_vm=None,
|
|
707
|
+
aot_compiler: Optional[AOTCompiler] = None,
|
|
708
|
+
inline_cache: Optional[InlineCache] = None,
|
|
709
|
+
max_workers: int = 4,
|
|
710
|
+
):
|
|
711
|
+
self._vm = contract_vm
|
|
712
|
+
self._aot = aot_compiler
|
|
713
|
+
self._ic = inline_cache
|
|
714
|
+
self._max_workers = max(1, max_workers)
|
|
715
|
+
|
|
716
|
+
def execute_batch(
|
|
717
|
+
self,
|
|
718
|
+
transactions: List[Dict[str, Any]],
|
|
719
|
+
chain=None,
|
|
720
|
+
) -> TxBatchResult:
|
|
721
|
+
"""Execute a batch of transactions efficiently.
|
|
722
|
+
|
|
723
|
+
Each transaction dict should have:
|
|
724
|
+
* ``contract`` — target contract address
|
|
725
|
+
* ``action`` — action name
|
|
726
|
+
* ``args`` — action arguments
|
|
727
|
+
* ``caller`` — sender address
|
|
728
|
+
* ``gas_limit`` — per-tx gas limit (optional)
|
|
729
|
+
|
|
730
|
+
Non-conflicting contract groups are dispatched to a thread pool
|
|
731
|
+
for parallel execution, giving significant speedups on
|
|
732
|
+
multi-core machines.
|
|
733
|
+
|
|
734
|
+
Returns a ``TxBatchResult``.
|
|
735
|
+
"""
|
|
736
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
737
|
+
|
|
738
|
+
result = TxBatchResult(total=len(transactions))
|
|
739
|
+
start = time.time()
|
|
740
|
+
|
|
741
|
+
# Group by contract for locality — groups that target
|
|
742
|
+
# different contracts have no state overlap and can run
|
|
743
|
+
# in parallel.
|
|
744
|
+
groups = self._group_by_contract(transactions)
|
|
745
|
+
|
|
746
|
+
if len(groups) <= 1 or self._max_workers <= 1:
|
|
747
|
+
# Single contract or single worker → sequential fast path
|
|
748
|
+
for contract_addr, txs in groups.items():
|
|
749
|
+
for tx in txs:
|
|
750
|
+
receipt = self._execute_single(tx, contract_addr)
|
|
751
|
+
result.receipts.append(receipt)
|
|
752
|
+
if receipt.get("success"):
|
|
753
|
+
result.succeeded += 1
|
|
754
|
+
result.gas_used += receipt.get("gas_used", 0)
|
|
755
|
+
else:
|
|
756
|
+
result.failed += 1
|
|
757
|
+
else:
|
|
758
|
+
# Multiple contracts → parallel execution per contract group
|
|
759
|
+
def _run_group(contract_addr: str, txs: List[Dict[str, Any]]):
|
|
760
|
+
receipts = []
|
|
761
|
+
for tx in txs:
|
|
762
|
+
receipts.append(self._execute_single(tx, contract_addr))
|
|
763
|
+
return receipts
|
|
764
|
+
|
|
765
|
+
with ThreadPoolExecutor(max_workers=min(self._max_workers, len(groups))) as pool:
|
|
766
|
+
futures = {
|
|
767
|
+
pool.submit(_run_group, addr, txs): addr
|
|
768
|
+
for addr, txs in groups.items()
|
|
769
|
+
}
|
|
770
|
+
for future in as_completed(futures):
|
|
771
|
+
group_receipts = future.result()
|
|
772
|
+
for receipt in group_receipts:
|
|
773
|
+
result.receipts.append(receipt)
|
|
774
|
+
if receipt.get("success"):
|
|
775
|
+
result.succeeded += 1
|
|
776
|
+
result.gas_used += receipt.get("gas_used", 0)
|
|
777
|
+
else:
|
|
778
|
+
result.failed += 1
|
|
779
|
+
|
|
780
|
+
result.elapsed = time.time() - start
|
|
781
|
+
return result
|
|
782
|
+
|
|
783
|
+
def _group_by_contract(
|
|
784
|
+
self, transactions: List[Dict[str, Any]]
|
|
785
|
+
) -> Dict[str, List[Dict[str, Any]]]:
|
|
786
|
+
"""Group transactions by target contract for cache locality."""
|
|
787
|
+
groups: Dict[str, List[Dict[str, Any]]] = {}
|
|
788
|
+
for tx in transactions:
|
|
789
|
+
addr = tx.get("contract", "")
|
|
790
|
+
groups.setdefault(addr, []).append(tx)
|
|
791
|
+
return groups
|
|
792
|
+
|
|
793
|
+
def _execute_single(
|
|
794
|
+
self, tx: Dict[str, Any], contract_addr: str
|
|
795
|
+
) -> Dict[str, Any]:
|
|
796
|
+
"""Execute a single transaction, using AOT if available."""
|
|
797
|
+
action_name = tx.get("action", "")
|
|
798
|
+
args = tx.get("args", {})
|
|
799
|
+
caller = tx.get("caller", "")
|
|
800
|
+
gas_limit = tx.get("gas_limit")
|
|
801
|
+
|
|
802
|
+
if self._vm is None:
|
|
803
|
+
return {
|
|
804
|
+
"success": False,
|
|
805
|
+
"error": "No ContractVM available",
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
try:
|
|
809
|
+
receipt = self._vm.execute_contract(
|
|
810
|
+
contract_address=contract_addr,
|
|
811
|
+
action=action_name,
|
|
812
|
+
args=args,
|
|
813
|
+
caller=caller,
|
|
814
|
+
gas_limit=gas_limit,
|
|
815
|
+
)
|
|
816
|
+
return {
|
|
817
|
+
"success": receipt.success,
|
|
818
|
+
"gas_used": receipt.gas_used,
|
|
819
|
+
"return_value": receipt.return_value,
|
|
820
|
+
"error": receipt.error,
|
|
821
|
+
}
|
|
822
|
+
except Exception as exc:
|
|
823
|
+
return {
|
|
824
|
+
"success": False,
|
|
825
|
+
"error": str(exc),
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
# =====================================================================
|
|
830
|
+
# Unified Execution Accelerator
|
|
831
|
+
# =====================================================================
|
|
832
|
+
|
|
833
|
+
class ExecutionAccelerator:
|
|
834
|
+
"""Unified performance layer for blockchain execution.
|
|
835
|
+
|
|
836
|
+
Combines all acceleration techniques into a single API that
|
|
837
|
+
``BlockchainNode`` can use as a drop-in replacement for direct
|
|
838
|
+
``ContractVM`` calls.
|
|
839
|
+
|
|
840
|
+
Parameters
|
|
841
|
+
----------
|
|
842
|
+
contract_vm : ContractVM, optional
|
|
843
|
+
The execution bridge.
|
|
844
|
+
cache_dir : str, optional
|
|
845
|
+
Directory for WASM / AOT caches.
|
|
846
|
+
aot_enabled : bool
|
|
847
|
+
Enable ahead-of-time compilation (default True).
|
|
848
|
+
ic_enabled : bool
|
|
849
|
+
Enable inline caching (default True).
|
|
850
|
+
wasm_cache_enabled : bool
|
|
851
|
+
Enable WASM module caching (default True).
|
|
852
|
+
numeric_fast_path : bool
|
|
853
|
+
Enable native numeric acceleration (default True).
|
|
854
|
+
optimization_level : int
|
|
855
|
+
AOT optimization level (0-2, default 2).
|
|
856
|
+
"""
|
|
857
|
+
|
|
858
|
+
def __init__(
|
|
859
|
+
self,
|
|
860
|
+
contract_vm=None,
|
|
861
|
+
cache_dir: Optional[str] = None,
|
|
862
|
+
aot_enabled: bool = True,
|
|
863
|
+
ic_enabled: bool = True,
|
|
864
|
+
wasm_cache_enabled: bool = True,
|
|
865
|
+
numeric_fast_path: bool = True,
|
|
866
|
+
optimization_level: int = 2,
|
|
867
|
+
debug: bool = False,
|
|
868
|
+
batch_workers: int = 4,
|
|
869
|
+
rust_core: bool = True,
|
|
870
|
+
multiprocess: bool = False,
|
|
871
|
+
vm_factory=None,
|
|
872
|
+
):
|
|
873
|
+
self._vm = contract_vm
|
|
874
|
+
self._debug = debug
|
|
875
|
+
|
|
876
|
+
# ── Rust native execution core (optional) ─────────────────
|
|
877
|
+
self.rust_bridge = None
|
|
878
|
+
if rust_core:
|
|
879
|
+
try:
|
|
880
|
+
from .rust_bridge import RustCoreBridge, rust_core_available
|
|
881
|
+
if rust_core_available():
|
|
882
|
+
self.rust_bridge = RustCoreBridge(max_workers=batch_workers)
|
|
883
|
+
logger.info(
|
|
884
|
+
"Rust execution core active — native acceleration enabled"
|
|
885
|
+
)
|
|
886
|
+
except ImportError:
|
|
887
|
+
pass
|
|
888
|
+
|
|
889
|
+
# ── Multiprocess executor (Option 3 — GIL-free) ──────────
|
|
890
|
+
self.mp_executor = None
|
|
891
|
+
if multiprocess:
|
|
892
|
+
try:
|
|
893
|
+
from .multiprocess_executor import MultiProcessBatchExecutor
|
|
894
|
+
self.mp_executor = MultiProcessBatchExecutor(
|
|
895
|
+
vm_factory=vm_factory,
|
|
896
|
+
workers=batch_workers,
|
|
897
|
+
use_rust_in_workers=rust_core,
|
|
898
|
+
)
|
|
899
|
+
logger.info(
|
|
900
|
+
"Multiprocess executor active — %d worker processes",
|
|
901
|
+
batch_workers,
|
|
902
|
+
)
|
|
903
|
+
except ImportError:
|
|
904
|
+
pass
|
|
905
|
+
|
|
906
|
+
# Sub-components
|
|
907
|
+
self.aot = (
|
|
908
|
+
AOTCompiler(optimization_level=optimization_level, debug=debug)
|
|
909
|
+
if aot_enabled
|
|
910
|
+
else None
|
|
911
|
+
)
|
|
912
|
+
self.inline_cache = (
|
|
913
|
+
InlineCache(max_size=4096)
|
|
914
|
+
if ic_enabled
|
|
915
|
+
else None
|
|
916
|
+
)
|
|
917
|
+
self.wasm_cache = (
|
|
918
|
+
WASMCache(cache_dir=cache_dir)
|
|
919
|
+
if wasm_cache_enabled
|
|
920
|
+
else None
|
|
921
|
+
)
|
|
922
|
+
self.numeric = (
|
|
923
|
+
NumericFastPath()
|
|
924
|
+
if numeric_fast_path
|
|
925
|
+
else None
|
|
926
|
+
)
|
|
927
|
+
self.batch_executor = BatchExecutor(
|
|
928
|
+
contract_vm=contract_vm,
|
|
929
|
+
aot_compiler=self.aot,
|
|
930
|
+
inline_cache=self.inline_cache,
|
|
931
|
+
max_workers=batch_workers,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
# Top-level stats
|
|
935
|
+
self._total_calls: int = 0
|
|
936
|
+
self._accelerated_calls: int = 0
|
|
937
|
+
self._total_time: float = 0.0
|
|
938
|
+
|
|
939
|
+
# ── Contract lifecycle hooks ──────────────────────────────────
|
|
940
|
+
|
|
941
|
+
def on_contract_deployed(self, contract_address: str, contract: Any) -> None:
|
|
942
|
+
"""Called when a contract is deployed — triggers AOT compilation."""
|
|
943
|
+
if self.aot:
|
|
944
|
+
self.aot.compile_contract(contract_address, contract)
|
|
945
|
+
if self._debug:
|
|
946
|
+
logger.debug(
|
|
947
|
+
"AOT pre-compiled contract %s", contract_address[:8]
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
def on_contract_upgraded(self, contract_address: str, contract: Any) -> None:
|
|
951
|
+
"""Called when a contract is upgraded — invalidates and recompiles."""
|
|
952
|
+
if self.aot:
|
|
953
|
+
self.aot.invalidate_contract(contract_address)
|
|
954
|
+
self.aot.compile_contract(contract_address, contract)
|
|
955
|
+
if self.inline_cache:
|
|
956
|
+
self.inline_cache.invalidate_prefix(f"{contract_address}:")
|
|
957
|
+
|
|
958
|
+
# ── Execution ─────────────────────────────────────────────────
|
|
959
|
+
|
|
960
|
+
def execute(
|
|
961
|
+
self,
|
|
962
|
+
contract_address: str,
|
|
963
|
+
action: str,
|
|
964
|
+
args: Optional[Dict[str, Any]] = None,
|
|
965
|
+
caller: str = "",
|
|
966
|
+
gas_limit: Optional[int] = None,
|
|
967
|
+
) -> Any:
|
|
968
|
+
"""Execute a contract action with all available acceleration.
|
|
969
|
+
|
|
970
|
+
Falls back to standard ``ContractVM.execute_contract()`` if
|
|
971
|
+
no acceleration is possible.
|
|
972
|
+
"""
|
|
973
|
+
self._total_calls += 1
|
|
974
|
+
start = time.time()
|
|
975
|
+
|
|
976
|
+
try:
|
|
977
|
+
# Try inline cache for dispatch
|
|
978
|
+
if self.inline_cache:
|
|
979
|
+
cache_key = f"{contract_address}:{action}"
|
|
980
|
+
cached = self.inline_cache.get(cache_key)
|
|
981
|
+
# Cache stores the compiled action for quick re-use
|
|
982
|
+
if isinstance(cached, CompiledAction) and cached.compiled_fn:
|
|
983
|
+
self._accelerated_calls += 1
|
|
984
|
+
# Route through normal VM (the AOT compilation gives
|
|
985
|
+
# the *evaluator* fast path, not a full bypass)
|
|
986
|
+
|
|
987
|
+
# Standard execution through ContractVM
|
|
988
|
+
if self._vm is not None:
|
|
989
|
+
receipt = self._vm.execute_contract(
|
|
990
|
+
contract_address=contract_address,
|
|
991
|
+
action=action,
|
|
992
|
+
args=args,
|
|
993
|
+
caller=caller,
|
|
994
|
+
gas_limit=gas_limit,
|
|
995
|
+
)
|
|
996
|
+
return receipt
|
|
997
|
+
else:
|
|
998
|
+
raise RuntimeError("No ContractVM attached")
|
|
999
|
+
|
|
1000
|
+
finally:
|
|
1001
|
+
self._total_time += time.time() - start
|
|
1002
|
+
|
|
1003
|
+
def execute_batch(
|
|
1004
|
+
self,
|
|
1005
|
+
transactions: List[Dict[str, Any]],
|
|
1006
|
+
chain=None,
|
|
1007
|
+
) -> TxBatchResult:
|
|
1008
|
+
"""Execute a batch of transactions with acceleration.
|
|
1009
|
+
|
|
1010
|
+
Execution priority:
|
|
1011
|
+
1. **Multiprocess** — separate OS processes (true GIL-free parallelism)
|
|
1012
|
+
2. **Rust batched-GIL** — Rayon parallel groups, one GIL per group
|
|
1013
|
+
3. **Python ThreadPool** — fallback when neither is available
|
|
1014
|
+
|
|
1015
|
+
Sustains 1,800+ TPS with Rust alone, 10,000+ TPS with
|
|
1016
|
+
multiprocess + Rust stacked.
|
|
1017
|
+
"""
|
|
1018
|
+
# ── Priority 1: Multiprocess executor ─────────────────────
|
|
1019
|
+
if self.mp_executor:
|
|
1020
|
+
try:
|
|
1021
|
+
raw = self.mp_executor.execute_batch(transactions)
|
|
1022
|
+
result = TxBatchResult(total=len(transactions))
|
|
1023
|
+
result.receipts = raw.receipts
|
|
1024
|
+
result.succeeded = raw.succeeded
|
|
1025
|
+
result.failed = raw.failed
|
|
1026
|
+
result.gas_used = raw.gas_used
|
|
1027
|
+
result.elapsed = raw.elapsed
|
|
1028
|
+
self._total_calls += len(transactions)
|
|
1029
|
+
self._accelerated_calls += len(transactions)
|
|
1030
|
+
self._total_time += raw.elapsed
|
|
1031
|
+
return result
|
|
1032
|
+
except Exception as exc:
|
|
1033
|
+
logger.warning("Multiprocess batch exec failed, falling back: %s", exc)
|
|
1034
|
+
|
|
1035
|
+
# ── Priority 2: Rust batched-GIL ──────────────────────────
|
|
1036
|
+
if self.rust_bridge and self._vm:
|
|
1037
|
+
try:
|
|
1038
|
+
import json as _json
|
|
1039
|
+
|
|
1040
|
+
def _vm_callback(contract, action, args_json, caller, gas_str):
|
|
1041
|
+
args = _json.loads(args_json) if isinstance(args_json, str) else args_json
|
|
1042
|
+
gas = int(gas_str) if isinstance(gas_str, str) and gas_str.isdigit() else 100_000
|
|
1043
|
+
result = self._vm.execute_action(
|
|
1044
|
+
contract=contract,
|
|
1045
|
+
action=action,
|
|
1046
|
+
args=args,
|
|
1047
|
+
caller=caller,
|
|
1048
|
+
gas_limit=gas,
|
|
1049
|
+
)
|
|
1050
|
+
if isinstance(result, dict):
|
|
1051
|
+
return result
|
|
1052
|
+
return {"success": True, "gas_used": 0, "result": str(result)}
|
|
1053
|
+
|
|
1054
|
+
raw = self.rust_bridge.execute_batch(transactions, _vm_callback)
|
|
1055
|
+
result = TxBatchResult(total=len(transactions))
|
|
1056
|
+
result.receipts = raw.receipts
|
|
1057
|
+
result.succeeded = raw.succeeded
|
|
1058
|
+
result.failed = raw.failed
|
|
1059
|
+
result.gas_used = raw.gas_used
|
|
1060
|
+
result.elapsed = raw.elapsed
|
|
1061
|
+
self._total_calls += len(transactions)
|
|
1062
|
+
self._accelerated_calls += len(transactions)
|
|
1063
|
+
self._total_time += raw.elapsed
|
|
1064
|
+
return result
|
|
1065
|
+
except Exception as exc:
|
|
1066
|
+
logger.warning("Rust batch exec failed, falling back: %s", exc)
|
|
1067
|
+
|
|
1068
|
+
# ── Priority 3: Python ThreadPool fallback ────────────────
|
|
1069
|
+
return self.batch_executor.execute_batch(transactions, chain)
|
|
1070
|
+
|
|
1071
|
+
# ── WASM helpers ──────────────────────────────────────────────
|
|
1072
|
+
|
|
1073
|
+
def cache_wasm(self, bytecode_hash: str, wasm_bytes: bytes) -> None:
|
|
1074
|
+
"""Store a compiled WASM module in the disk cache."""
|
|
1075
|
+
if self.wasm_cache:
|
|
1076
|
+
self.wasm_cache.put(bytecode_hash, wasm_bytes)
|
|
1077
|
+
|
|
1078
|
+
def get_cached_wasm(self, bytecode_hash: str) -> Optional[bytes]:
|
|
1079
|
+
"""Retrieve a cached WASM module."""
|
|
1080
|
+
if self.wasm_cache:
|
|
1081
|
+
return self.wasm_cache.get(bytecode_hash)
|
|
1082
|
+
return None
|
|
1083
|
+
|
|
1084
|
+
# ── Stats ─────────────────────────────────────────────────────
|
|
1085
|
+
|
|
1086
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
1087
|
+
stats: Dict[str, Any] = {
|
|
1088
|
+
"total_calls": self._total_calls,
|
|
1089
|
+
"accelerated_calls": self._accelerated_calls,
|
|
1090
|
+
"total_time": round(self._total_time, 4),
|
|
1091
|
+
"acceleration_rate": round(
|
|
1092
|
+
self._accelerated_calls / max(self._total_calls, 1) * 100, 2
|
|
1093
|
+
),
|
|
1094
|
+
"rust_core_active": self.rust_bridge is not None,
|
|
1095
|
+
"multiprocess_active": self.mp_executor is not None,
|
|
1096
|
+
"execution_mode": (
|
|
1097
|
+
"multiprocess+rust" if self.mp_executor and self.rust_bridge
|
|
1098
|
+
else "multiprocess" if self.mp_executor
|
|
1099
|
+
else "rust/batched-gil" if self.rust_bridge
|
|
1100
|
+
else "python/threads"
|
|
1101
|
+
),
|
|
1102
|
+
}
|
|
1103
|
+
if self.aot:
|
|
1104
|
+
stats["aot"] = self.aot.get_stats()
|
|
1105
|
+
if self.inline_cache:
|
|
1106
|
+
stats["inline_cache"] = self.inline_cache.get_stats()
|
|
1107
|
+
if self.wasm_cache:
|
|
1108
|
+
stats["wasm_cache"] = self.wasm_cache.get_stats()
|
|
1109
|
+
if self.numeric:
|
|
1110
|
+
stats["numeric_fast_path"] = self.numeric.get_stats()
|
|
1111
|
+
return stats
|
|
1112
|
+
|
|
1113
|
+
def clear_caches(self) -> None:
|
|
1114
|
+
"""Clear all acceleration caches."""
|
|
1115
|
+
if self.inline_cache:
|
|
1116
|
+
self.inline_cache.clear()
|
|
1117
|
+
if self.wasm_cache:
|
|
1118
|
+
self.wasm_cache.clear()
|
|
1119
|
+
if self.numeric:
|
|
1120
|
+
self.numeric._cache.clear()
|
|
1121
|
+
self._total_calls = 0
|
|
1122
|
+
self._accelerated_calls = 0
|
|
1123
|
+
self._total_time = 0.0
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
# =====================================================================
|
|
1127
|
+
# Value wrapping fast-path (bypasses ContractVM._wrap_value overhead)
|
|
1128
|
+
# =====================================================================
|
|
1129
|
+
|
|
1130
|
+
def _fast_wrap(val: Any) -> Any:
|
|
1131
|
+
"""Wrap a Python value into a Zexus object — fast path."""
|
|
1132
|
+
from ..object import (
|
|
1133
|
+
Integer as ZInteger, Float as ZFloat,
|
|
1134
|
+
Boolean as ZBoolean, String as ZString,
|
|
1135
|
+
List as ZList, Map as ZMap, Null as ZNull,
|
|
1136
|
+
)
|
|
1137
|
+
if isinstance(val, (ZInteger, ZFloat, ZBoolean, ZString, ZList, ZMap, ZNull)):
|
|
1138
|
+
return val
|
|
1139
|
+
if isinstance(val, bool):
|
|
1140
|
+
return ZBoolean(val)
|
|
1141
|
+
if isinstance(val, int):
|
|
1142
|
+
return ZInteger(val)
|
|
1143
|
+
if isinstance(val, float):
|
|
1144
|
+
return ZFloat(val)
|
|
1145
|
+
if isinstance(val, str):
|
|
1146
|
+
return ZString(val)
|
|
1147
|
+
if val is None:
|
|
1148
|
+
return ZNull()
|
|
1149
|
+
return val
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def _fast_unwrap(val: Any) -> Any:
|
|
1153
|
+
"""Unwrap a Zexus object to a Python value — fast path."""
|
|
1154
|
+
if hasattr(val, "value"):
|
|
1155
|
+
return val.value
|
|
1156
|
+
if hasattr(val, "elements"):
|
|
1157
|
+
return [_fast_unwrap(e) for e in val.elements]
|
|
1158
|
+
if hasattr(val, "pairs"):
|
|
1159
|
+
return {_fast_unwrap(k): _fast_unwrap(v) for k, v in val.pairs.items()}
|
|
1160
|
+
return val
|