zexus 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +26 -3
  2. package/package.json +1 -1
  3. package/src/__init__.py +7 -0
  4. package/src/zexus/__init__.py +1 -1
  5. package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
  7. package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
  8. package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
  9. package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
  10. package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
  11. package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
  12. package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
  13. package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
  14. package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
  15. package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
  16. package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
  17. package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
  18. package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  19. package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
  20. package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
  22. package/src/zexus/advanced_types.py +17 -2
  23. package/src/zexus/blockchain/__init__.py +411 -0
  24. package/src/zexus/blockchain/accelerator.py +1187 -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 +1425 -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 +485 -0
  39. package/src/zexus/blockchain/storage.py +423 -0
  40. package/src/zexus/blockchain/tokens.py +750 -0
  41. package/src/zexus/blockchain/upgradeable.py +1004 -0
  42. package/src/zexus/blockchain/verification.py +1602 -0
  43. package/src/zexus/blockchain/wallet.py +621 -0
  44. package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
  45. package/src/zexus/cli/main.py +300 -20
  46. package/src/zexus/cli/zpm.py +1 -1
  47. package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
  48. package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
  49. package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
  50. package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
  51. package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  52. package/src/zexus/compiler/lexer.py +10 -5
  53. package/src/zexus/concurrency_system.py +79 -0
  54. package/src/zexus/config.py +54 -0
  55. package/src/zexus/crypto_bridge.py +244 -8
  56. package/src/zexus/dap/__init__.py +10 -0
  57. package/src/zexus/dap/__main__.py +4 -0
  58. package/src/zexus/dap/dap_server.py +391 -0
  59. package/src/zexus/dap/debug_engine.py +298 -0
  60. package/src/zexus/environment.py +10 -1
  61. package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
  62. package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
  63. package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
  64. package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
  65. package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
  66. package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
  67. package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
  68. package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
  69. package/src/zexus/evaluator/bytecode_compiler.py +441 -37
  70. package/src/zexus/evaluator/core.py +560 -49
  71. package/src/zexus/evaluator/expressions.py +122 -49
  72. package/src/zexus/evaluator/functions.py +417 -16
  73. package/src/zexus/evaluator/statements.py +521 -118
  74. package/src/zexus/evaluator/unified_execution.py +573 -72
  75. package/src/zexus/evaluator/utils.py +14 -2
  76. package/src/zexus/event_loop.py +186 -0
  77. package/src/zexus/lexer.py +742 -486
  78. package/src/zexus/lsp/__init__.py +1 -1
  79. package/src/zexus/lsp/definition_provider.py +163 -9
  80. package/src/zexus/lsp/server.py +22 -8
  81. package/src/zexus/lsp/symbol_provider.py +182 -9
  82. package/src/zexus/module_cache.py +237 -9
  83. package/src/zexus/object.py +64 -6
  84. package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
  85. package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
  86. package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
  87. package/src/zexus/parser/parser.py +786 -285
  88. package/src/zexus/parser/strategy_context.py +407 -66
  89. package/src/zexus/parser/strategy_structural.py +117 -19
  90. package/src/zexus/persistence.py +15 -1
  91. package/src/zexus/renderer/__init__.py +15 -0
  92. package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
  93. package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
  94. package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
  95. package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
  96. package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
  97. package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
  98. package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
  99. package/src/zexus/renderer/tk_backend.py +208 -0
  100. package/src/zexus/renderer/web_backend.py +260 -0
  101. package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  102. package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
  103. package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
  104. package/src/zexus/runtime/file_flags.py +137 -0
  105. package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
  107. package/src/zexus/security.py +424 -34
  108. package/src/zexus/stdlib/fs.py +23 -18
  109. package/src/zexus/stdlib/http.py +289 -186
  110. package/src/zexus/stdlib/sockets.py +207 -163
  111. package/src/zexus/stdlib/websockets.py +282 -0
  112. package/src/zexus/stdlib_integration.py +369 -2
  113. package/src/zexus/strategy_recovery.py +6 -3
  114. package/src/zexus/type_checker.py +423 -0
  115. package/src/zexus/virtual_filesystem.py +189 -2
  116. package/src/zexus/vm/__init__.py +113 -3
  117. package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
  118. package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
  119. package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
  120. package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
  121. package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
  122. package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
  123. package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
  124. package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
  125. package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
  126. package/src/zexus/vm/async_optimizer.py +14 -1
  127. package/src/zexus/vm/binary_bytecode.py +659 -0
  128. package/src/zexus/vm/bytecode.py +28 -1
  129. package/src/zexus/vm/bytecode_converter.py +26 -12
  130. package/src/zexus/vm/cabi.c +1985 -0
  131. package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
  132. package/src/zexus/vm/cabi.h +127 -0
  133. package/src/zexus/vm/cache.py +557 -17
  134. package/src/zexus/vm/compiler.py +703 -5
  135. package/src/zexus/vm/fastops.c +13861 -0
  136. package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
  137. package/src/zexus/vm/fastops.pyx +288 -0
  138. package/src/zexus/vm/gas_metering.py +52 -11
  139. package/src/zexus/vm/jit.py +83 -2
  140. package/src/zexus/vm/native_jit_backend.py +1816 -0
  141. package/src/zexus/vm/native_runtime.cpp +1388 -0
  142. package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
  143. package/src/zexus/vm/optimizer.py +161 -11
  144. package/src/zexus/vm/parallel_vm.py +118 -42
  145. package/src/zexus/vm/peephole_optimizer.py +82 -4
  146. package/src/zexus/vm/profiler.py +38 -18
  147. package/src/zexus/vm/register_allocator.py +16 -5
  148. package/src/zexus/vm/register_vm.py +8 -5
  149. package/src/zexus/vm/vm.py +3589 -588
  150. package/src/zexus/vm/wasm_compiler.py +658 -0
  151. package/src/zexus/zexus_ast.py +63 -11
  152. package/src/zexus/zexus_token.py +13 -5
  153. package/src/zexus/zpm/installer.py +55 -15
  154. package/src/zexus/zpm/package_manager.py +1 -1
  155. package/src/zexus/zpm/registry.py +257 -28
  156. package/src/zexus.egg-info/PKG-INFO +30 -4
  157. package/src/zexus.egg-info/SOURCES.txt +133 -9
  158. package/src/zexus.egg-info/entry_points.txt +1 -0
  159. package/src/zexus.egg-info/requires.txt +4 -0
@@ -0,0 +1,1425 @@
1
+ """
2
+ Zexus Blockchain — Contract VM Bridge
3
+
4
+ Connects the Zexus VM's smart-contract opcodes to the real blockchain
5
+ infrastructure (Chain, Ledger, TransactionContext, CryptoPlugin).
6
+
7
+ The existing VM has blockchain opcodes (110-119, 130-137) that operate on
8
+ a raw ``env["_blockchain_state"]`` dict. This bridge replaces that naive
9
+ dict with a proper adapter that delegates to:
10
+
11
+ - ``Chain.contract_state`` for persistent contract storage
12
+ - ``Ledger`` for versioned, auditable state writes
13
+ - ``TransactionContext`` for gas tracking & TX metadata
14
+ - ``CryptoPlugin`` for real signature verification
15
+
16
+ Usage
17
+ -----
18
+ ::
19
+
20
+ from zexus.blockchain.contract_vm import ContractVM
21
+
22
+ contract_vm = ContractVM(chain=node.chain)
23
+ receipt = contract_vm.execute_contract(
24
+ contract_address="0xabc...",
25
+ action="transfer",
26
+ args={"to": "0xdef...", "amount": 100},
27
+ caller="0x123...",
28
+ gas_limit=500_000,
29
+ )
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import copy
35
+ import hashlib
36
+ import json
37
+ import os
38
+ import time
39
+ import logging
40
+ from dataclasses import dataclass, field
41
+ from typing import Any, Callable, Dict, List, Optional, Tuple
42
+
43
+ from .chain import Chain, Transaction, TransactionReceipt
44
+ from .transaction import TransactionContext
45
+ from .crypto import CryptoPlugin
46
+
47
+ logger = logging.getLogger("zexus.blockchain.contract_vm")
48
+
49
+ # Try importing the VM — it may not be installed in every deployment.
50
+ try:
51
+ from ..vm.vm import VM as ZexusVM
52
+ from ..vm.gas_metering import GasMetering, GasCost, OutOfGasError
53
+ _VM_AVAILABLE = True
54
+ except ImportError:
55
+ _VM_AVAILABLE = False
56
+ ZexusVM = None # type: ignore
57
+ GasMetering = None # type: ignore
58
+ GasCost = None # type: ignore
59
+ OutOfGasError = None # type: ignore
60
+
61
+ # Rust VM (Phase 3 — adaptive contract execution)
62
+ try:
63
+ from zexus_core import RustVMExecutor as _RustVMExecutor
64
+ _RUST_VM_AVAILABLE = True
65
+ except Exception:
66
+ _RUST_VM_AVAILABLE = False
67
+ _RustVMExecutor = None # type: ignore
68
+
69
+ # Rust ContractVM orchestrator (Phase 4)
70
+ try:
71
+ from zexus_core import RustContractVM as _RustContractVM
72
+ _RUST_CONTRACT_VM_AVAILABLE = True
73
+ except Exception:
74
+ _RUST_CONTRACT_VM_AVAILABLE = False
75
+ _RustContractVM = None # type: ignore
76
+
77
+ # SmartContract from security module
78
+ try:
79
+ from ..security import SmartContract
80
+ _CONTRACT_AVAILABLE = True
81
+ except ImportError:
82
+ _CONTRACT_AVAILABLE = False
83
+ SmartContract = None # type: ignore
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Contract State Adapter
88
+ # ---------------------------------------------------------------------------
89
+
90
+ class ContractStateAdapter(dict):
91
+ """A dict-like object that transparently delegates reads/writes to
92
+ ``Chain.contract_state[contract_address]``.
93
+
94
+ Every *write* is recorded in a pending journal so the caller can
95
+ commit or rollback atomically.
96
+ """
97
+
98
+ def __init__(self, chain: Chain, contract_address: str):
99
+ super().__init__()
100
+ self._chain = chain
101
+ self._contract_address = contract_address
102
+ # Seed from chain (make a shallow copy so mutations don't leak back)
103
+ stored = chain.contract_state.get(contract_address, {})
104
+ super().update(copy.deepcopy(stored))
105
+
106
+ # Reads ---------------------------------------------------------------
107
+
108
+ def __getitem__(self, key: str) -> Any:
109
+ return super().__getitem__(key)
110
+
111
+ def get(self, key: str, default: Any = None) -> Any:
112
+ return super().get(key, default)
113
+
114
+ # Writes (journalled) -------------------------------------------------
115
+
116
+ def __setitem__(self, key: str, value: Any):
117
+ super().__setitem__(key, value)
118
+
119
+ def update(self, other=(), **kwargs):
120
+ super().update(other, **kwargs)
121
+
122
+ # Commit / Rollback ---------------------------------------------------
123
+
124
+ def commit(self):
125
+ """Flush all current state back to ``chain.contract_state``."""
126
+ self._chain.contract_state[self._contract_address] = dict(self)
127
+
128
+ def rollback(self, snapshot: Dict[str, Any]):
129
+ """Restore from a previously captured snapshot."""
130
+ self.clear()
131
+ super().update(snapshot)
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Execution Receipt
136
+ # ---------------------------------------------------------------------------
137
+
138
+ @dataclass
139
+ class ContractExecutionReceipt:
140
+ """Result of executing a contract action through the VM."""
141
+ success: bool = True
142
+ return_value: Any = None
143
+ gas_used: int = 0
144
+ gas_limit: int = 0
145
+ logs: List[Dict[str, Any]] = field(default_factory=list)
146
+ error: str = ""
147
+ revert_reason: str = ""
148
+ state_changes: Dict[str, Any] = field(default_factory=dict)
149
+
150
+ def to_dict(self) -> Dict[str, Any]:
151
+ return {
152
+ "success": self.success,
153
+ "return_value": str(self.return_value),
154
+ "gas_used": self.gas_used,
155
+ "gas_limit": self.gas_limit,
156
+ "logs": self.logs,
157
+ "error": self.error,
158
+ "revert_reason": self.revert_reason,
159
+ "state_changes": self.state_changes,
160
+ }
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # ContractVM — the bridge
165
+ # ---------------------------------------------------------------------------
166
+
167
+ class ContractVM:
168
+ """Bridge between the Zexus VM and the real blockchain infrastructure.
169
+
170
+ Responsibilities
171
+ ----------------
172
+ 1. Provide a real ``_blockchain_state`` backed by ``Chain.contract_state``
173
+ so that STATE_READ / STATE_WRITE opcodes persist to the chain.
174
+ 2. Inject a proper ``verify_sig`` builtin so VERIFY_SIGNATURE uses
175
+ ``CryptoPlugin.verify_signature`` instead of the insecure SHA-256
176
+ fallback.
177
+ 3. Enforce gas metering for every opcode in both sync *and* async paths.
178
+ 4. Wire TX_BEGIN / TX_COMMIT / TX_REVERT to atomic chain-state updates.
179
+ 5. Execute ``SmartContract`` actions through the VM with a full
180
+ ``TransactionContext``.
181
+ """
182
+
183
+ def __init__(
184
+ self,
185
+ chain: Chain,
186
+ gas_limit: int = 10_000_000,
187
+ debug: bool = False,
188
+ use_bytecode_vm: bool = False,
189
+ ):
190
+ if not _VM_AVAILABLE:
191
+ raise RuntimeError(
192
+ "ContractVM requires the Zexus VM. "
193
+ "Ensure src/zexus/vm/ is present and importable."
194
+ )
195
+ self._chain = chain
196
+ self._default_gas_limit = gas_limit
197
+ self._debug = debug
198
+ self._use_bytecode_vm = use_bytecode_vm
199
+
200
+ # Deployed contract registry: address -> SmartContract
201
+ self._contracts: Dict[str, SmartContract] = {}
202
+
203
+ # Reentrancy guard — tracks contracts currently being executed
204
+ self._executing: set = set()
205
+
206
+ # Cross-contract call depth tracking
207
+ self._call_depth: int = 0
208
+ self._max_call_depth: int = 10
209
+
210
+ # Phase 0 stats: track bytecode vs tree-walk executions
211
+ self._vm_stats = {
212
+ "bytecode_executions": 0,
213
+ "bytecode_fallbacks": 0,
214
+ "treewalk_executions": 0,
215
+ "rust_executions": 0,
216
+ "rust_fallbacks": 0,
217
+ }
218
+
219
+ # Rust VM executor (Phase 3 + Phase 6 — Rust-first execution)
220
+ self._rust_vm_executor = _RustVMExecutor() if _RUST_VM_AVAILABLE else None
221
+ self._rust_vm_threshold = 0 # Phase 6: route ALL contracts through Rust by default
222
+ try:
223
+ _env_thresh = os.environ.get("ZEXUS_RUST_VM_THRESHOLD")
224
+ if _env_thresh is not None:
225
+ self._rust_vm_threshold = int(_env_thresh)
226
+ except (ValueError, TypeError):
227
+ pass
228
+
229
+ # Rust ContractVM orchestrator (Phase 4)
230
+ self._rust_contract_vm = (
231
+ _RustContractVM(
232
+ gas_discount=0.6,
233
+ default_gas_limit=gas_limit,
234
+ max_call_depth=self._max_call_depth,
235
+ )
236
+ if _RUST_CONTRACT_VM_AVAILABLE
237
+ else None
238
+ )
239
+
240
+ # ------------------------------------------------------------------
241
+ # Contract lifecycle
242
+ # ------------------------------------------------------------------
243
+
244
+ def deploy_contract(
245
+ self,
246
+ contract: "SmartContract",
247
+ deployer: str,
248
+ gas_limit: Optional[int] = None,
249
+ initial_value: int = 0,
250
+ ) -> ContractExecutionReceipt:
251
+ """Deploy a SmartContract onto the chain.
252
+
253
+ - Assigns the contract an on-chain address.
254
+ - Stores initial bytecode / storage in ``chain.contract_state``.
255
+ - Runs the constructor (if any) inside the VM.
256
+ """
257
+ gas_limit = gas_limit or self._default_gas_limit
258
+ address = contract.address
259
+
260
+ # Register on-chain account
261
+ acct = self._chain.get_account(address)
262
+ acct["balance"] = initial_value
263
+ acct["code"] = contract.name # Store contract "type" as code
264
+ acct["nonce"] = 0
265
+
266
+ # Save initial storage
267
+ initial_storage: Dict[str, Any] = {}
268
+ if hasattr(contract, 'storage') and hasattr(contract.storage, 'current_state'):
269
+ initial_storage = dict(contract.storage.current_state)
270
+ elif hasattr(contract, 'storage') and hasattr(contract.storage, 'data'):
271
+ initial_storage = dict(contract.storage.data)
272
+ self._chain.contract_state[address] = initial_storage
273
+
274
+ # Register locally
275
+ self._contracts[address] = contract
276
+
277
+ receipt = ContractExecutionReceipt(
278
+ success=True,
279
+ gas_limit=gas_limit,
280
+ state_changes={"deployed": address, "storage_keys": list(initial_storage.keys())},
281
+ )
282
+
283
+ logger.info("Contract '%s' deployed at %s", contract.name, address)
284
+ return receipt
285
+
286
+ def get_contract(self, address: str) -> Optional["SmartContract"]:
287
+ """Look up a deployed contract by address."""
288
+ return self._contracts.get(address)
289
+
290
+ # ------------------------------------------------------------------
291
+ # Contract execution
292
+ # ------------------------------------------------------------------
293
+
294
+ def execute_contract(
295
+ self,
296
+ contract_address: str,
297
+ action: str,
298
+ args: Optional[Dict[str, Any]] = None,
299
+ caller: str = "",
300
+ gas_limit: Optional[int] = None,
301
+ value: int = 0,
302
+ ) -> ContractExecutionReceipt:
303
+ """Execute a contract action inside the VM.
304
+
305
+ This is the main entry-point used by ``BlockchainNode`` when
306
+ processing a contract-call transaction.
307
+
308
+ Steps
309
+ -----
310
+ 1. Build a ``TransactionContext`` with the caller, gas limit, etc.
311
+ 2. Create a ``ContractStateAdapter`` backed by the chain.
312
+ 3. Construct a fresh VM with the state adapter as
313
+ ``env["_blockchain_state"]`` and real ``verify_sig``.
314
+ 4. Execute the contract's action body via the VM.
315
+ 5. On success → commit state; on failure → rollback.
316
+ 6. Return a ``ContractExecutionReceipt``.
317
+ """
318
+ gas_limit = gas_limit or self._default_gas_limit
319
+ # Per-execution log list — avoids sharing state across concurrent calls
320
+ logs: List[Dict[str, Any]] = []
321
+
322
+ contract = self._contracts.get(contract_address)
323
+ if contract is None:
324
+ return ContractExecutionReceipt(
325
+ success=False,
326
+ error=f"Contract not found at {contract_address}",
327
+ gas_limit=gas_limit,
328
+ )
329
+
330
+ action_obj = contract.actions.get(action)
331
+ if action_obj is None:
332
+ return ContractExecutionReceipt(
333
+ success=False,
334
+ error=f"Action '{action}' not found on contract '{contract.name}'",
335
+ gas_limit=gas_limit,
336
+ )
337
+
338
+ # Reentrancy guard
339
+ if contract_address in self._executing:
340
+ return ContractExecutionReceipt(
341
+ success=False,
342
+ error="ReentrancyGuard",
343
+ revert_reason=f"Reentrant call to contract {contract_address}",
344
+ gas_limit=gas_limit,
345
+ )
346
+
347
+ # Call-depth guard (cross-contract calls)
348
+ if self._call_depth >= self._max_call_depth:
349
+ return ContractExecutionReceipt(
350
+ success=False,
351
+ error="CallDepthExceeded",
352
+ revert_reason=f"Call depth {self._call_depth} exceeds max {self._max_call_depth}",
353
+ gas_limit=gas_limit,
354
+ )
355
+
356
+ self._executing.add(contract_address)
357
+ self._call_depth += 1
358
+
359
+ # 1. TX context
360
+ tip = self._chain.tip
361
+ tx_ctx = TransactionContext(
362
+ caller=caller,
363
+ timestamp=time.time(),
364
+ block_hash=tip.hash if tip else "0" * 64,
365
+ gas_limit=gas_limit,
366
+ )
367
+
368
+ # 2. Chain-backed state adapter
369
+ state_adapter = ContractStateAdapter(self._chain, contract_address)
370
+ snapshot = dict(state_adapter) # for rollback
371
+
372
+ # 3. Build VM environment + builtins
373
+ env = self._build_env(state_adapter, tx_ctx, contract, args or {})
374
+ builtins = self._build_builtins(tx_ctx, contract_address, logs)
375
+
376
+ # ── Phase 4: Rust ContractVM orchestration ──
377
+ # Try to handle the entire execution lifecycle in Rust.
378
+ # If Rust signals needs_fallback we fall through to Python.
379
+ if (self._rust_contract_vm is not None
380
+ and self._use_bytecode_vm
381
+ and hasattr(action_obj, 'body')):
382
+ rust_receipt = self._try_rust_contract_vm(
383
+ contract_address, action_obj, state_adapter,
384
+ snapshot, env, args or {}, gas_limit, caller, logs,
385
+ )
386
+ if rust_receipt is not None:
387
+ return rust_receipt
388
+
389
+ # 4. Execute (Python path — Phase 3/0/tree-walk)
390
+ try:
391
+ vm = ZexusVM(
392
+ env=env,
393
+ builtins=builtins,
394
+ enable_gas_metering=True,
395
+ gas_limit=gas_limit,
396
+ debug=self._debug,
397
+ )
398
+
399
+ # Execute the action body through the evaluator
400
+ result = self._execute_action(vm, action_obj, env, args or {})
401
+
402
+ gas_used = vm.gas_metering.gas_used if vm.gas_metering else 0
403
+
404
+ # 5a. Commit
405
+ state_adapter.commit()
406
+
407
+ return ContractExecutionReceipt(
408
+ success=True,
409
+ return_value=result,
410
+ gas_used=gas_used,
411
+ gas_limit=gas_limit,
412
+ logs=list(logs),
413
+ state_changes=self._diff_state(snapshot, dict(state_adapter)),
414
+ )
415
+
416
+ except OutOfGasError as e:
417
+ # 5b. Rollback on OOG
418
+ state_adapter.rollback(snapshot)
419
+ return ContractExecutionReceipt(
420
+ success=False,
421
+ gas_used=gas_limit,
422
+ gas_limit=gas_limit,
423
+ error="OutOfGas",
424
+ revert_reason=str(e),
425
+ )
426
+
427
+ except Exception as e:
428
+ # 5b. Rollback on any error
429
+ state_adapter.rollback(snapshot)
430
+ return ContractExecutionReceipt(
431
+ success=False,
432
+ gas_used=0,
433
+ gas_limit=gas_limit,
434
+ error=type(e).__name__,
435
+ revert_reason=str(e),
436
+ logs=list(logs),
437
+ )
438
+
439
+ finally:
440
+ self._executing.discard(contract_address)
441
+ self._call_depth -= 1
442
+
443
+ # ------------------------------------------------------------------
444
+ # Internal helpers
445
+ # ------------------------------------------------------------------
446
+
447
+ def _build_env(
448
+ self,
449
+ state_adapter: ContractStateAdapter,
450
+ tx_ctx: TransactionContext,
451
+ contract: "SmartContract",
452
+ args: Dict[str, Any],
453
+ ) -> Dict[str, Any]:
454
+ """Assemble the VM ``env`` dict for a contract execution."""
455
+ from ..object import Map, String, Integer, Float, Boolean as BooleanObj
456
+
457
+ env: Dict[str, Any] = {}
458
+
459
+ # Wire the chain-backed state adapter as _blockchain_state
460
+ env["_blockchain_state"] = state_adapter
461
+
462
+ # Gas tracking (used by GAS_CHARGE opcode)
463
+ env["_gas_remaining"] = tx_ctx.gas_limit
464
+
465
+ # TX object — immutable context
466
+ tx_map = Map({
467
+ String("caller"): String(tx_ctx.caller),
468
+ String("timestamp"): Integer(int(tx_ctx.timestamp)),
469
+ String("block_hash"): String(tx_ctx.block_hash),
470
+ String("gas_limit"): Integer(tx_ctx.gas_limit),
471
+ String("gas_remaining"): Integer(tx_ctx.gas_remaining),
472
+ })
473
+ env["TX"] = tx_map
474
+
475
+ # Pre-populate contract storage into env for tree-walking evaluator
476
+ if hasattr(contract, 'storage'):
477
+ state = state_adapter # already seeded from chain
478
+ for key, val in state.items():
479
+ env[key] = self._wrap_value(val)
480
+
481
+ # Arguments (passed as env vars to the action)
482
+ for k, v in args.items():
483
+ env[k] = self._wrap_value(v)
484
+
485
+ # Contract reference
486
+ env["self"] = contract
487
+ env["_contract_address"] = contract.address
488
+
489
+ return env
490
+
491
+ def _build_builtins(
492
+ self,
493
+ tx_ctx: TransactionContext,
494
+ contract_address: str = "",
495
+ logs: Optional[List[Dict[str, Any]]] = None,
496
+ ) -> Dict[str, Any]:
497
+ """Build VM builtins, including the real ``verify_sig``."""
498
+ builtins: Dict[str, Any] = {}
499
+ _logs = logs if logs is not None else []
500
+
501
+ # Real signature verification via CryptoPlugin
502
+ def verify_sig(signature: Any, message: Any, public_key: Any) -> bool:
503
+ """Verify an ECDSA signature using the real CryptoPlugin."""
504
+ sig_str = str(signature.value) if hasattr(signature, 'value') else str(signature)
505
+ msg_str = str(message.value) if hasattr(message, 'value') else str(message)
506
+ key_str = str(public_key.value) if hasattr(public_key, 'value') else str(public_key)
507
+ try:
508
+ return CryptoPlugin.verify_signature(msg_str, sig_str, key_str)
509
+ except Exception:
510
+ return False
511
+
512
+ builtins["verify_sig"] = verify_sig
513
+
514
+ # Emit log/event
515
+ def emit_event(name: Any, data: Any = None) -> None:
516
+ """Emit a contract event (stored in receipt logs)."""
517
+ name_str = str(name.value) if hasattr(name, 'value') else str(name)
518
+ _logs.append({
519
+ "event": name_str,
520
+ "data": data,
521
+ "timestamp": time.time(),
522
+ "contract": contract_address, # emit from contract, not caller
523
+ })
524
+
525
+ builtins["emit"] = emit_event
526
+
527
+ # Balance check
528
+ def get_balance(address: Any) -> int:
529
+ """Get on-chain balance of an address."""
530
+ addr = str(address.value) if hasattr(address, 'value') else str(address)
531
+ return self._chain.get_account(addr).get("balance", 0)
532
+
533
+ builtins["get_balance"] = get_balance
534
+
535
+ # Transfer — with overflow protection
536
+ def transfer(to: Any, amount: Any) -> bool:
537
+ """Transfer value between accounts."""
538
+ to_str = str(to.value) if hasattr(to, 'value') else str(to)
539
+ amt = int(amount.value) if hasattr(amount, 'value') else int(amount)
540
+ if amt <= 0:
541
+ return False
542
+ caller_acct = self._chain.get_account(tx_ctx.caller)
543
+ sender_balance = caller_acct.get("balance", 0)
544
+ if sender_balance < amt:
545
+ return False
546
+ to_acct = self._chain.get_account(to_str)
547
+ to_balance = to_acct.get("balance", 0)
548
+ # Overflow check
549
+ if to_balance + amt < to_balance:
550
+ return False
551
+ caller_acct["balance"] = sender_balance - amt
552
+ to_acct["balance"] = to_balance + amt
553
+ return True
554
+
555
+ builtins["transfer"] = transfer
556
+
557
+ # Keccak-256 hash
558
+ def keccak256(data: Any) -> str:
559
+ """Keccak-256 hash via CryptoPlugin."""
560
+ d = str(data.value) if hasattr(data, 'value') else str(data)
561
+ return CryptoPlugin.keccak256(d)
562
+
563
+ builtins["keccak256"] = keccak256
564
+
565
+ # Block info
566
+ def block_number() -> int:
567
+ return self._chain.height
568
+
569
+ def block_timestamp() -> float:
570
+ tip = self._chain.tip
571
+ return tip.header.timestamp if tip else 0.0
572
+
573
+ builtins["block_number"] = block_number
574
+ builtins["block_timestamp"] = block_timestamp
575
+
576
+ # ── Cross-contract calls ──────────────────────────────────
577
+ vm_ref = self # capture for closures
578
+
579
+ def contract_call(target_address: Any, action: Any,
580
+ call_args: Any = None, value: Any = None) -> Any:
581
+ """Call another contract's action (state-mutating).
582
+
583
+ Parameters
584
+ ----------
585
+ target_address : str or String
586
+ Address of the contract to call.
587
+ action : str or String
588
+ Name of the action to invoke.
589
+ call_args : dict, optional
590
+ Arguments to pass to the action.
591
+ value : int, optional
592
+ Value to transfer with the call.
593
+
594
+ Returns the action's return value (unwrapped to Python).
595
+ Raises RuntimeError on failure or depth exceeded.
596
+ """
597
+ addr = str(target_address.value) if hasattr(target_address, 'value') else str(target_address)
598
+ act = str(action.value) if hasattr(action, 'value') else str(action)
599
+ args = {}
600
+ if call_args is not None:
601
+ if hasattr(call_args, 'pairs'):
602
+ args = {str(k.value) if hasattr(k, 'value') else str(k):
603
+ vm_ref._unwrap_value(v) for k, v in call_args.pairs.items()}
604
+ elif isinstance(call_args, dict):
605
+ args = call_args
606
+ val = 0
607
+ if value is not None:
608
+ val = int(value.value) if hasattr(value, 'value') else int(value)
609
+
610
+ if vm_ref._call_depth >= vm_ref._max_call_depth:
611
+ raise RuntimeError(f"Cross-contract call depth exceeded (max {vm_ref._max_call_depth})")
612
+
613
+ receipt = vm_ref.execute_contract(
614
+ contract_address=addr,
615
+ action=act,
616
+ args=args,
617
+ caller=contract_address or tx_ctx.caller,
618
+ gas_limit=tx_ctx.gas_remaining,
619
+ value=val,
620
+ )
621
+ if not receipt.success:
622
+ raise RuntimeError(f"Cross-contract call failed: {receipt.error or receipt.revert_reason}")
623
+ return receipt.return_value
624
+
625
+ def static_contract_call(target_address: Any, action: Any,
626
+ call_args: Any = None) -> Any:
627
+ """Read-only call to another contract (no state changes).
628
+
629
+ Same as contract_call but uses static_call internally.
630
+ """
631
+ addr = str(target_address.value) if hasattr(target_address, 'value') else str(target_address)
632
+ act = str(action.value) if hasattr(action, 'value') else str(action)
633
+ args = {}
634
+ if call_args is not None:
635
+ if hasattr(call_args, 'pairs'):
636
+ args = {str(k.value) if hasattr(k, 'value') else str(k):
637
+ vm_ref._unwrap_value(v) for k, v in call_args.pairs.items()}
638
+ elif isinstance(call_args, dict):
639
+ args = call_args
640
+
641
+ receipt = vm_ref.static_call(
642
+ contract_address=addr,
643
+ action=act,
644
+ args=args,
645
+ caller=contract_address or tx_ctx.caller,
646
+ )
647
+ if not receipt.success:
648
+ raise RuntimeError(f"Static call failed: {receipt.error or receipt.revert_reason}")
649
+ return receipt.return_value
650
+
651
+ def delegate_call(target_address: Any, action: Any,
652
+ call_args: Any = None) -> Any:
653
+ """Delegatecall: execute target's code in caller's storage context.
654
+
655
+ Like contract_call, but the target's action runs with the
656
+ *calling* contract's state adapter, so state writes go to
657
+ the caller's storage, not the target's.
658
+ """
659
+ addr = str(target_address.value) if hasattr(target_address, 'value') else str(target_address)
660
+ act = str(action.value) if hasattr(action, 'value') else str(action)
661
+ args = {}
662
+ if call_args is not None:
663
+ if hasattr(call_args, 'pairs'):
664
+ args = {str(k.value) if hasattr(k, 'value') else str(k):
665
+ vm_ref._unwrap_value(v) for k, v in call_args.pairs.items()}
666
+ elif isinstance(call_args, dict):
667
+ args = call_args
668
+
669
+ if vm_ref._call_depth >= vm_ref._max_call_depth:
670
+ raise RuntimeError(f"Delegatecall depth exceeded (max {vm_ref._max_call_depth})")
671
+
672
+ # Find the target contract's action
673
+ target_contract = vm_ref.get_contract(addr)
674
+ if target_contract is None:
675
+ raise RuntimeError(f"Contract not found: {addr}")
676
+
677
+ action_obj = None
678
+ if hasattr(target_contract, 'actions'):
679
+ for a in target_contract.actions:
680
+ a_name = a.name if hasattr(a, 'name') else str(a)
681
+ if a_name == act:
682
+ action_obj = a
683
+ break
684
+ if action_obj is None:
685
+ raise RuntimeError(f"Action '{act}' not found on contract {addr}")
686
+
687
+ # Execute with *caller's* state adapter (the key difference)
688
+ caller_addr = contract_address or tx_ctx.caller
689
+ state_adapter = ContractStateAdapter(vm_ref._chain, caller_addr)
690
+ snapshot = dict(state_adapter)
691
+
692
+ from ..vm.vm import VM as ZexusVM
693
+ vm = ZexusVM(debug=vm_ref._debug)
694
+ vm_ref._call_depth += 1
695
+ try:
696
+ env = vm_ref._build_env(state_adapter, tx_ctx, target_contract, args)
697
+ inner_builtins = vm_ref._build_builtins(tx_ctx, caller_addr, _logs)
698
+ for bk, bv in inner_builtins.items():
699
+ vm.env[bk] = bv
700
+ result = vm_ref._execute_action(vm, action_obj, env, args)
701
+ state_adapter.commit()
702
+ return vm_ref._unwrap_value(result) if result is not None else None
703
+ except Exception:
704
+ state_adapter.rollback(snapshot)
705
+ raise
706
+ finally:
707
+ vm_ref._call_depth -= 1
708
+
709
+ builtins["contract_call"] = contract_call
710
+ builtins["static_call"] = static_contract_call
711
+ builtins["delegate_call"] = delegate_call
712
+
713
+ return builtins
714
+
715
+ def _execute_action(
716
+ self,
717
+ vm: "ZexusVM",
718
+ action_obj: Any,
719
+ env: Dict[str, Any],
720
+ args: Dict[str, Any],
721
+ ) -> Any:
722
+ """Run a contract action's body through the evaluator.
723
+
724
+ When ``self._use_bytecode_vm`` is True the evaluator is created
725
+ with ``use_vm=True`` so it compiles the action body to bytecode
726
+ and executes it through the VM (Phase 0). If bytecoded execution
727
+ fails for any reason, we transparently fall back to tree-walking.
728
+
729
+ When ``self._use_bytecode_vm`` is False (default), we always use
730
+ tree-walking for maximum reliability.
731
+ """
732
+ from ..object import Environment, Action
733
+ from ..evaluator.core import Evaluator
734
+
735
+ # Build an evaluator Environment from the flat dict
736
+ eval_env = Environment()
737
+ for k, v in env.items():
738
+ eval_env.set(k, v)
739
+
740
+ # Add action parameters from args
741
+ if hasattr(action_obj, 'parameters') and action_obj.parameters:
742
+ for param in action_obj.parameters:
743
+ param_name = param.value if hasattr(param, 'value') else str(param)
744
+ if param_name in args:
745
+ eval_env.set(param_name, self._wrap_value(args[param_name]))
746
+
747
+ result = None
748
+ used_bytecode = False
749
+
750
+ # --- Phase 1: try pre-compiled .zxc for this action ---
751
+ if self._use_bytecode_vm and hasattr(action_obj, 'body'):
752
+ cached_bc = getattr(action_obj, '_cached_bytecode', None)
753
+ if cached_bc is None:
754
+ try:
755
+ from ..vm.binary_bytecode import load_zxc, save_zxc
756
+ import hashlib as _hl
757
+ _action_name = getattr(action_obj, 'name', None)
758
+ if _action_name:
759
+ _aname = _action_name.value if hasattr(_action_name, 'value') else str(_action_name)
760
+ _contract_addr = getattr(self, '_contract_address', '') or ''
761
+ _cache_key = _hl.md5(f"{_contract_addr}:{_aname}".encode()).hexdigest()[:16]
762
+ _cache_dir = os.path.join(os.path.expanduser("~"), ".zexus", "action_cache")
763
+ os.makedirs(_cache_dir, exist_ok=True)
764
+ _zxc_path = os.path.join(_cache_dir, f"{_cache_key}.zxc")
765
+ if os.path.exists(_zxc_path):
766
+ cached_bc = load_zxc(_zxc_path)
767
+ action_obj._cached_bytecode = cached_bc
768
+ action_obj._cached_zxc_path = _zxc_path
769
+ except Exception:
770
+ pass
771
+
772
+ # --- Phase 3: Rust VM execution for large contracts ---
773
+ if (self._rust_vm_executor is not None
774
+ and self._use_bytecode_vm
775
+ and hasattr(action_obj, 'body')):
776
+ rust_result = self._try_rust_vm_execution(
777
+ action_obj, env, args, vm, cached_bc
778
+ )
779
+ if rust_result is not None:
780
+ used_bytecode, result = rust_result
781
+
782
+ # --- Phase 0: bytecoded execution with fallback ---
783
+ if self._use_bytecode_vm and hasattr(action_obj, 'body'):
784
+ try:
785
+ evaluator = Evaluator(use_vm=True)
786
+
787
+ # Wire contract gas metering into the evaluator's VM
788
+ if evaluator.vm_instance and vm.gas_metering:
789
+ evaluator.vm_instance.gas_metering = vm.gas_metering
790
+ evaluator.vm_instance.enable_gas_metering = True
791
+
792
+ # Inject contract builtins into evaluator's VM
793
+ if evaluator.vm_instance:
794
+ vm_builtins = dict(evaluator.vm_instance.builtins or {})
795
+ vm_builtins.update(vm.builtins or {})
796
+ evaluator.vm_instance.builtins = vm_builtins
797
+
798
+ # Push blockchain state into the VM's env
799
+ evaluator.vm_instance.env["_blockchain_state"] = env.get("_blockchain_state")
800
+ evaluator.vm_instance.env["_gas_remaining"] = env.get("_gas_remaining")
801
+ evaluator.vm_instance.env["TX"] = env.get("TX")
802
+
803
+ result = evaluator.eval_with_vm_support(
804
+ action_obj.body, eval_env, debug_mode=self._debug
805
+ )
806
+ used_bytecode = True
807
+ self._vm_stats["bytecode_executions"] += 1
808
+
809
+ # Phase 1: persist compiled bytecode as .zxc for next call
810
+ if not getattr(action_obj, '_cached_zxc_path', None):
811
+ try:
812
+ _bc = getattr(evaluator, '_last_compiled_bytecode', None)
813
+ if _bc is None and hasattr(evaluator, 'vm_instance'):
814
+ _bc = getattr(evaluator.vm_instance, '_last_bytecode', None)
815
+ if _bc is not None:
816
+ from ..vm.binary_bytecode import save_zxc
817
+ import hashlib as _hl
818
+ _action_name = getattr(action_obj, 'name', None)
819
+ if _action_name:
820
+ _aname = _action_name.value if hasattr(_action_name, 'value') else str(_action_name)
821
+ _contract_addr = getattr(self, '_contract_address', '') or ''
822
+ _cache_key = _hl.md5(f"{_contract_addr}:{_aname}".encode()).hexdigest()[:16]
823
+ _cache_dir = os.path.join(os.path.expanduser("~"), ".zexus", "action_cache")
824
+ os.makedirs(_cache_dir, exist_ok=True)
825
+ save_zxc(os.path.join(_cache_dir, f"{_cache_key}.zxc"), _bc)
826
+ except Exception:
827
+ pass
828
+ if self._debug:
829
+ stats = evaluator.get_vm_stats()
830
+ logger.debug(
831
+ "Bytecoded execution: compiles=%d vm_runs=%d fallbacks=%d",
832
+ stats.get("bytecode_compiles", 0),
833
+ stats.get("vm_executions", 0),
834
+ stats.get("vm_fallbacks", 0),
835
+ )
836
+ except Exception as e:
837
+ # Bytecoded execution failed — fall back to tree-walk
838
+ self._vm_stats["bytecode_fallbacks"] += 1
839
+ logger.debug(
840
+ "Bytecoded execution failed, falling back to tree-walk: %s", e
841
+ )
842
+ used_bytecode = False
843
+ result = None
844
+ # Rebuild eval_env since the failed VM run may have corrupted it
845
+ eval_env = Environment()
846
+ for k, v in env.items():
847
+ eval_env.set(k, v)
848
+ if hasattr(action_obj, 'parameters') and action_obj.parameters:
849
+ for param in action_obj.parameters:
850
+ param_name = param.value if hasattr(param, 'value') else str(param)
851
+ if param_name in args:
852
+ eval_env.set(param_name, self._wrap_value(args[param_name]))
853
+
854
+ # --- Tree-walk execution (default or fallback) ---
855
+ if not used_bytecode and hasattr(action_obj, 'body'):
856
+ evaluator = Evaluator(use_vm=False)
857
+ self._vm_stats["treewalk_executions"] += 1
858
+ try:
859
+ result = evaluator.eval_node(action_obj.body, eval_env, [])
860
+ except Exception as e:
861
+ if "Requirement failed" in str(e):
862
+ raise # Re-raise REQUIRE failures
863
+ raise
864
+
865
+ # Sync modified vars back to _blockchain_state
866
+ state_adapter = env.get("_blockchain_state")
867
+ if state_adapter and hasattr(action_obj, 'body'):
868
+ # Check for any env vars that match storage keys
869
+ for key in list(state_adapter.keys()):
870
+ new_val = eval_env.get(key)
871
+ if new_val is not None:
872
+ state_adapter[key] = self._unwrap_value(new_val)
873
+
874
+ return result
875
+
876
+ def _try_rust_vm_execution(
877
+ self,
878
+ action_obj: Any,
879
+ env: Dict[str, Any],
880
+ args: Dict[str, Any],
881
+ vm: "ZexusVM",
882
+ cached_bc: Any,
883
+ ) -> Optional[Tuple[bool, Any]]:
884
+ """Attempt to run an action through the Rust VM.
885
+
886
+ Returns ``(True, result)`` on success, ``None`` if the Rust VM
887
+ is unavailable or signals a fallback. The caller should fall
888
+ through to Phase 0 / tree-walk when ``None`` is returned.
889
+ """
890
+ try:
891
+ from ..vm.binary_bytecode import serialize as _serialize_zxc
892
+
893
+ # We need serializable bytecode — either from the .zxc cache
894
+ # or by compiling now.
895
+ zxc_data = None
896
+ bc = cached_bc
897
+ if bc is None:
898
+ # Try to compile to bytecode so we can check instruction count
899
+ from ..evaluator.core import Evaluator
900
+ evaluator = Evaluator(use_vm=True)
901
+ try:
902
+ bc = evaluator.compile_to_bytecode(action_obj.body)
903
+ except Exception:
904
+ return None
905
+
906
+ # Check threshold
907
+ instr_count = len(getattr(bc, "instructions", []))
908
+ if instr_count < self._rust_vm_threshold:
909
+ return None # Too small — let Python handle it
910
+
911
+ # Serialize
912
+ zxc_data = _serialize_zxc(bc, include_checksum=True)
913
+
914
+ # Build state dict from ContractStateAdapter
915
+ state_adapter = env.get("_blockchain_state")
916
+ rust_state = {}
917
+ if state_adapter and isinstance(state_adapter, dict):
918
+ for k, v in state_adapter.items():
919
+ if isinstance(v, (int, float, str, bool, type(None))):
920
+ rust_state[k] = v
921
+
922
+ # Build env dict (simple values only)
923
+ from ..object import (
924
+ Integer as ZInteger, Float as ZFloat,
925
+ Boolean as ZBoolean, String as ZString, Null as ZNull,
926
+ )
927
+ rust_env = {}
928
+ for k, v in env.items():
929
+ if isinstance(v, (int, float, str, bool, type(None))):
930
+ rust_env[k] = v
931
+ elif isinstance(v, ZInteger):
932
+ rust_env[k] = v.value
933
+ elif isinstance(v, ZFloat):
934
+ rust_env[k] = v.value
935
+ elif isinstance(v, ZString):
936
+ rust_env[k] = v.value
937
+ elif isinstance(v, ZBoolean):
938
+ rust_env[k] = v.value
939
+ elif isinstance(v, ZNull):
940
+ rust_env[k] = None
941
+
942
+ # Add action parameters
943
+ if hasattr(action_obj, 'parameters') and action_obj.parameters:
944
+ for param in action_obj.parameters:
945
+ param_name = param.value if hasattr(param, 'value') else str(param)
946
+ if param_name in args:
947
+ v = args[param_name]
948
+ if isinstance(v, (int, float, str, bool, type(None))):
949
+ rust_env[param_name] = v
950
+
951
+ # Gas limit
952
+ gas_limit = 0
953
+ if vm.gas_metering:
954
+ remaining_fn = getattr(vm.gas_metering, "remaining", None)
955
+ if callable(remaining_fn):
956
+ try:
957
+ rem = remaining_fn()
958
+ if isinstance(rem, (int, float)) and rem > 0:
959
+ gas_limit = int(rem)
960
+ except Exception:
961
+ pass
962
+ if gas_limit == 0:
963
+ gl = getattr(vm.gas_metering, "gas_limit", 0) or 0
964
+ gu = getattr(vm.gas_metering, "gas_used", 0) or 0
965
+ if isinstance(gl, (int, float)) and isinstance(gu, (int, float)):
966
+ if gl > gu:
967
+ gas_limit = int(gl - gu)
968
+
969
+ # Execute
970
+ result_dict = self._rust_vm_executor.execute(
971
+ zxc_data,
972
+ env=rust_env or None,
973
+ state=rust_state or None,
974
+ gas_limit=gas_limit,
975
+ )
976
+
977
+ # Fallback?
978
+ if result_dict.get("needs_fallback", False):
979
+ self._vm_stats["rust_fallbacks"] += 1
980
+ return None
981
+
982
+ # Error?
983
+ error = result_dict.get("error")
984
+ if error:
985
+ if "OutOfGas" in str(error):
986
+ raise RuntimeError(str(error))
987
+ if "RequireFailed" in str(error):
988
+ raise RuntimeError(str(error))
989
+ # Other errors — fall back
990
+ self._vm_stats["rust_fallbacks"] += 1
991
+ return None
992
+
993
+ # Success — bridge gas back
994
+ if vm.gas_metering:
995
+ rust_gas = result_dict.get("gas_used", 0)
996
+ if rust_gas > 0:
997
+ current_used = getattr(vm.gas_metering, "gas_used", None)
998
+ if current_used is not None:
999
+ vm.gas_metering.gas_used = current_used + rust_gas
1000
+ add_fn = getattr(vm.gas_metering, "add_gas", None)
1001
+ if add_fn:
1002
+ add_fn(rust_gas)
1003
+
1004
+ # Merge state back into ContractStateAdapter
1005
+ rust_state_out = result_dict.get("state", {})
1006
+ if rust_state_out and state_adapter is not None:
1007
+ for k, v in rust_state_out.items():
1008
+ state_adapter[k] = v
1009
+
1010
+ self._vm_stats["rust_executions"] += 1
1011
+ if self._debug:
1012
+ logger.debug(
1013
+ "Rust VM execution: ops=%d gas=%d",
1014
+ result_dict.get("instructions_executed", 0),
1015
+ result_dict.get("gas_used", 0),
1016
+ )
1017
+
1018
+ return (True, result_dict.get("result"))
1019
+
1020
+ except Exception as e:
1021
+ self._vm_stats["rust_fallbacks"] += 1
1022
+ logger.debug("Rust VM execution failed, falling back: %s", e)
1023
+ return None
1024
+
1025
+ def _try_rust_contract_vm(
1026
+ self,
1027
+ contract_address: str,
1028
+ action_obj: Any,
1029
+ state_adapter: ContractStateAdapter,
1030
+ snapshot: Dict[str, Any],
1031
+ env: Dict[str, Any],
1032
+ args: Dict[str, Any],
1033
+ gas_limit: int,
1034
+ caller: str,
1035
+ logs: List[Dict[str, Any]],
1036
+ ) -> Optional[ContractExecutionReceipt]:
1037
+ """Phase 4: Attempt full contract execution via Rust ContractVM.
1038
+
1039
+ Returns a ``ContractExecutionReceipt`` on success, or ``None``
1040
+ if Rust can't handle it (falls back to Python).
1041
+ """
1042
+ try:
1043
+ from ..vm.binary_bytecode import serialize as _serialize_zxc
1044
+
1045
+ # Compile to bytecode
1046
+ bc = getattr(action_obj, '_cached_bytecode', None)
1047
+ if bc is None:
1048
+ from ..evaluator.core import Evaluator
1049
+ evaluator = Evaluator(use_vm=True)
1050
+ try:
1051
+ bc = evaluator.compile_to_bytecode(action_obj.body)
1052
+ except Exception:
1053
+ return None # Can't compile — fall back
1054
+
1055
+ # Serialize to .zxc
1056
+ zxc_data = _serialize_zxc(bc, include_checksum=True)
1057
+
1058
+ # Build state dict (simple values)
1059
+ rust_state = {}
1060
+ for k, v in state_adapter.items():
1061
+ if isinstance(v, (int, float, str, bool, type(None))):
1062
+ rust_state[k] = v
1063
+
1064
+ # Build env dict (simple values)
1065
+ from ..object import (
1066
+ Integer as ZInteger, Float as ZFloat,
1067
+ Boolean as ZBoolean, String as ZString, Null as ZNull,
1068
+ )
1069
+ rust_env = {}
1070
+ for k, v in env.items():
1071
+ if isinstance(v, (int, float, str, bool, type(None))):
1072
+ rust_env[k] = v
1073
+ elif isinstance(v, ZInteger):
1074
+ rust_env[k] = v.value
1075
+ elif isinstance(v, ZFloat):
1076
+ rust_env[k] = v.value
1077
+ elif isinstance(v, ZString):
1078
+ rust_env[k] = v.value
1079
+ elif isinstance(v, ZBoolean):
1080
+ rust_env[k] = v.value
1081
+ elif isinstance(v, ZNull):
1082
+ rust_env[k] = None
1083
+
1084
+ # Phase 6: Inject chain info for Rust builtins
1085
+ if "_block_number" not in rust_env:
1086
+ try:
1087
+ rust_env["_block_number"] = self._chain.height
1088
+ except Exception:
1089
+ rust_env["_block_number"] = 0
1090
+ if "_block_timestamp" not in rust_env:
1091
+ try:
1092
+ tip = self._chain.tip
1093
+ rust_env["_block_timestamp"] = (
1094
+ tip.header.timestamp if tip else 0.0
1095
+ )
1096
+ except Exception:
1097
+ rust_env["_block_timestamp"] = 0.0
1098
+
1099
+ # Build args dict (simple values)
1100
+ rust_args = {}
1101
+ for k, v in args.items():
1102
+ if isinstance(v, (int, float, str, bool, type(None))):
1103
+ rust_args[k] = v
1104
+
1105
+ # Execute via Rust ContractVM
1106
+ result_dict = self._rust_contract_vm.execute_contract(
1107
+ contract_address=contract_address,
1108
+ action_bytecode=zxc_data,
1109
+ state=rust_state or None,
1110
+ env=rust_env or None,
1111
+ args=rust_args or None,
1112
+ gas_limit=gas_limit,
1113
+ caller=caller,
1114
+ )
1115
+
1116
+ # Check for fallback
1117
+ if result_dict.get("needs_fallback", False):
1118
+ self._vm_stats["rust_fallbacks"] += 1
1119
+ if self._debug:
1120
+ logger.debug("Rust ContractVM needs fallback: %s",
1121
+ result_dict.get("error", ""))
1122
+ return None
1123
+
1124
+ # Build receipt
1125
+ success = result_dict.get("success", False)
1126
+ gas_used = result_dict.get("gas_used", 0)
1127
+
1128
+ if success:
1129
+ # Merge new state back to ContractStateAdapter
1130
+ new_state = result_dict.get("new_state", {})
1131
+ if new_state:
1132
+ state_adapter.clear()
1133
+ state_adapter.update(new_state)
1134
+ state_adapter.commit()
1135
+
1136
+ # Phase 6: Collect events emitted by Rust builtins
1137
+ rust_events = result_dict.get("events", [])
1138
+ import time as _time
1139
+ for ev in rust_events:
1140
+ ev_name = ev.get("event", "") if isinstance(ev, dict) else str(ev)
1141
+ ev_data = ev.get("data", None) if isinstance(ev, dict) else None
1142
+ logs.append({
1143
+ "event": ev_name,
1144
+ "data": ev_data,
1145
+ "timestamp": _time.time(),
1146
+ "contract": contract_address,
1147
+ })
1148
+
1149
+ self._vm_stats["rust_executions"] += 1
1150
+
1151
+ return ContractExecutionReceipt(
1152
+ success=True,
1153
+ return_value=result_dict.get("result"),
1154
+ gas_used=gas_used,
1155
+ gas_limit=gas_limit,
1156
+ logs=list(logs),
1157
+ state_changes=result_dict.get("state_changes", {}),
1158
+ )
1159
+ else:
1160
+ # Error — rollback
1161
+ state_adapter.rollback(snapshot)
1162
+ error = result_dict.get("error", "UnknownError")
1163
+
1164
+ if error == "OutOfGas":
1165
+ self._vm_stats["rust_fallbacks"] += 1
1166
+ return ContractExecutionReceipt(
1167
+ success=False,
1168
+ gas_used=gas_limit,
1169
+ gas_limit=gas_limit,
1170
+ error="OutOfGas",
1171
+ revert_reason=result_dict.get("revert_reason", ""),
1172
+ )
1173
+ elif error == "ReentrancyGuard":
1174
+ return ContractExecutionReceipt(
1175
+ success=False,
1176
+ error="ReentrancyGuard",
1177
+ revert_reason=result_dict.get("revert_reason", ""),
1178
+ gas_limit=gas_limit,
1179
+ )
1180
+ else:
1181
+ self._vm_stats["rust_fallbacks"] += 1
1182
+ return ContractExecutionReceipt(
1183
+ success=False,
1184
+ gas_used=gas_used,
1185
+ gas_limit=gas_limit,
1186
+ error=error,
1187
+ revert_reason=result_dict.get("revert_reason", ""),
1188
+ logs=list(logs),
1189
+ )
1190
+
1191
+ except Exception as e:
1192
+ self._vm_stats["rust_fallbacks"] += 1
1193
+ logger.debug("Rust ContractVM failed, falling back: %s", e)
1194
+ return None
1195
+
1196
+ def get_vm_execution_stats(self) -> Dict[str, Any]:
1197
+ """Return Phase 0-3 execution statistics."""
1198
+ total = (
1199
+ self._vm_stats["bytecode_executions"]
1200
+ + self._vm_stats["bytecode_fallbacks"]
1201
+ + self._vm_stats["treewalk_executions"]
1202
+ + self._vm_stats["rust_executions"]
1203
+ )
1204
+ # Phase 4 stats from Rust ContractVM
1205
+ rust_cvm_stats = {}
1206
+ if self._rust_contract_vm is not None:
1207
+ try:
1208
+ rust_cvm_stats = self._rust_contract_vm.get_stats()
1209
+ except Exception:
1210
+ pass
1211
+
1212
+ return {
1213
+ **self._vm_stats,
1214
+ "total_executions": total,
1215
+ "bytecode_rate": (
1216
+ self._vm_stats["bytecode_executions"] / total * 100
1217
+ if total > 0 else 0.0
1218
+ ),
1219
+ "rust_rate": (
1220
+ self._vm_stats["rust_executions"] / total * 100
1221
+ if total > 0 else 0.0
1222
+ ),
1223
+ "use_bytecode_vm": self._use_bytecode_vm,
1224
+ "rust_vm_available": self._rust_vm_executor is not None,
1225
+ "rust_vm_threshold": self._rust_vm_threshold,
1226
+ "rust_contract_vm_available": self._rust_contract_vm is not None,
1227
+ "rust_contract_vm_stats": rust_cvm_stats,
1228
+ }
1229
+
1230
+ # ------------------------------------------------------------------
1231
+ # Value wrapping / unwrapping
1232
+ # ------------------------------------------------------------------
1233
+
1234
+ @staticmethod
1235
+ def _wrap_value(val: Any) -> Any:
1236
+ """Wrap a Python value into a Zexus object."""
1237
+ from ..object import (
1238
+ Integer as ZInteger, Float as ZFloat,
1239
+ Boolean as ZBoolean, String as ZString,
1240
+ List as ZList, Map as ZMap, Null as ZNull,
1241
+ )
1242
+ if isinstance(val, (ZInteger, ZFloat, ZBoolean, ZString, ZList, ZMap, ZNull)):
1243
+ return val
1244
+ if isinstance(val, bool):
1245
+ return ZBoolean(val)
1246
+ if isinstance(val, int):
1247
+ return ZInteger(val)
1248
+ if isinstance(val, float):
1249
+ return ZFloat(val)
1250
+ if isinstance(val, str):
1251
+ return ZString(val)
1252
+ if isinstance(val, list):
1253
+ return ZList([ContractVM._wrap_value(e) for e in val])
1254
+ if isinstance(val, dict):
1255
+ return ZMap({
1256
+ ZString(str(k)): ContractVM._wrap_value(v)
1257
+ for k, v in val.items()
1258
+ })
1259
+ if val is None:
1260
+ return ZNull()
1261
+ return val
1262
+
1263
+ @staticmethod
1264
+ def _unwrap_value(val: Any) -> Any:
1265
+ """Unwrap a Zexus object to a plain Python value."""
1266
+ if hasattr(val, 'value'):
1267
+ return val.value
1268
+ if hasattr(val, 'elements'): # ZList
1269
+ return [ContractVM._unwrap_value(e) for e in val.elements]
1270
+ if hasattr(val, 'pairs'): # ZMap
1271
+ return {
1272
+ ContractVM._unwrap_value(k): ContractVM._unwrap_value(v)
1273
+ for k, v in val.pairs.items()
1274
+ }
1275
+ return val
1276
+
1277
+ @staticmethod
1278
+ def _diff_state(
1279
+ before: Dict[str, Any], after: Dict[str, Any]
1280
+ ) -> Dict[str, Any]:
1281
+ """Compute the difference between two state snapshots."""
1282
+ changes: Dict[str, Any] = {}
1283
+ all_keys = set(before.keys()) | set(after.keys())
1284
+ for key in all_keys:
1285
+ old = before.get(key)
1286
+ new = after.get(key)
1287
+ if old != new:
1288
+ changes[key] = {"before": old, "after": new}
1289
+ return changes
1290
+
1291
+ # ------------------------------------------------------------------
1292
+ # Static call (read-only, no state commit)
1293
+ # ------------------------------------------------------------------
1294
+
1295
+ def static_call(
1296
+ self,
1297
+ contract_address: str,
1298
+ action: str,
1299
+ args: Optional[Dict[str, Any]] = None,
1300
+ caller: str = "",
1301
+ gas_limit: Optional[int] = None,
1302
+ ) -> ContractExecutionReceipt:
1303
+ """Execute a read-only call that never commits state changes.
1304
+
1305
+ Useful for ``view`` functions that only read storage.
1306
+ """
1307
+ gas_limit = gas_limit or self._default_gas_limit
1308
+ logs: List[Dict[str, Any]] = []
1309
+
1310
+ contract = self._contracts.get(contract_address)
1311
+ if contract is None:
1312
+ return ContractExecutionReceipt(
1313
+ success=False,
1314
+ error=f"Contract not found at {contract_address}",
1315
+ gas_limit=gas_limit,
1316
+ )
1317
+
1318
+ action_obj = contract.actions.get(action)
1319
+ if action_obj is None:
1320
+ return ContractExecutionReceipt(
1321
+ success=False,
1322
+ error=f"Action '{action}' not found",
1323
+ gas_limit=gas_limit,
1324
+ )
1325
+
1326
+ tip = self._chain.tip
1327
+ tx_ctx = TransactionContext(
1328
+ caller=caller,
1329
+ timestamp=time.time(),
1330
+ block_hash=tip.hash if tip else "0" * 64,
1331
+ gas_limit=gas_limit,
1332
+ )
1333
+
1334
+ state_adapter = ContractStateAdapter(self._chain, contract_address)
1335
+ env = self._build_env(state_adapter, tx_ctx, contract, args or {})
1336
+ builtins = self._build_builtins(tx_ctx, contract_address, logs)
1337
+
1338
+ try:
1339
+ vm = ZexusVM(
1340
+ env=env,
1341
+ builtins=builtins,
1342
+ enable_gas_metering=True,
1343
+ gas_limit=gas_limit,
1344
+ debug=self._debug,
1345
+ )
1346
+ # Static calls use light gas metering (flat 1/op) since
1347
+ # they don't consume chain resources — read-only.
1348
+ vm.enable_gas_light = True
1349
+ result = self._execute_action(vm, action_obj, env, args or {})
1350
+ gas_used = vm.gas_metering.gas_used if vm.gas_metering else 0
1351
+
1352
+ # NOTE: No commit — state_adapter is discarded
1353
+ return ContractExecutionReceipt(
1354
+ success=True,
1355
+ return_value=result,
1356
+ gas_used=gas_used,
1357
+ gas_limit=gas_limit,
1358
+ logs=list(logs),
1359
+ )
1360
+ except Exception as e:
1361
+ return ContractExecutionReceipt(
1362
+ success=False,
1363
+ error=type(e).__name__,
1364
+ revert_reason=str(e),
1365
+ gas_limit=gas_limit,
1366
+ )
1367
+
1368
+ # ------------------------------------------------------------------
1369
+ # Batch execution (for block processing)
1370
+ # ------------------------------------------------------------------
1371
+
1372
+ def process_contract_transaction(
1373
+ self,
1374
+ tx: Transaction,
1375
+ ) -> TransactionReceipt:
1376
+ """Process a contract-call transaction and produce a receipt.
1377
+
1378
+ This is what ``BlockchainNode`` calls when processing a block
1379
+ that contains contract interactions.
1380
+
1381
+ The ``tx.data`` field is expected to be JSON-encoded::
1382
+
1383
+ {
1384
+ "contract": "<address>",
1385
+ "action": "<method>",
1386
+ "args": { ... }
1387
+ }
1388
+ """
1389
+ receipt = TransactionReceipt(
1390
+ tx_hash=tx.tx_hash,
1391
+ status=0,
1392
+ gas_used=0,
1393
+ )
1394
+
1395
+ # Parse tx.data
1396
+ try:
1397
+ call_data = json.loads(tx.data) if isinstance(tx.data, str) and tx.data else {}
1398
+ except json.JSONDecodeError:
1399
+ receipt.revert_reason = "Invalid contract call data"
1400
+ return receipt
1401
+
1402
+ contract_addr = call_data.get("contract", tx.recipient)
1403
+ action_name = call_data.get("action", "")
1404
+ action_args = call_data.get("args", {})
1405
+
1406
+ if not action_name:
1407
+ receipt.revert_reason = "Missing action name in tx.data"
1408
+ return receipt
1409
+
1410
+ exec_receipt = self.execute_contract(
1411
+ contract_address=contract_addr,
1412
+ action=action_name,
1413
+ args=action_args,
1414
+ caller=tx.sender,
1415
+ gas_limit=tx.gas_limit,
1416
+ value=tx.value,
1417
+ )
1418
+
1419
+ receipt.status = 1 if exec_receipt.success else 0
1420
+ receipt.gas_used = exec_receipt.gas_used
1421
+ receipt.logs = exec_receipt.logs
1422
+ receipt.revert_reason = exec_receipt.revert_reason
1423
+ receipt.contract_address = contract_addr
1424
+
1425
+ return receipt