zexus 1.6.8 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +12 -5
  2. package/package.json +1 -1
  3. package/src/__init__.py +7 -0
  4. package/src/zexus/__init__.py +1 -1
  5. package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
  7. package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
  8. package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
  9. package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
  10. package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
  11. package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
  12. package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
  13. package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
  14. package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
  15. package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
  16. package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
  17. package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
  18. package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  19. package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
  20. package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
  22. package/src/zexus/advanced_types.py +17 -2
  23. package/src/zexus/blockchain/__init__.py +411 -0
  24. package/src/zexus/blockchain/accelerator.py +1160 -0
  25. package/src/zexus/blockchain/chain.py +660 -0
  26. package/src/zexus/blockchain/consensus.py +821 -0
  27. package/src/zexus/blockchain/contract_vm.py +1019 -0
  28. package/src/zexus/blockchain/crypto.py +79 -14
  29. package/src/zexus/blockchain/events.py +526 -0
  30. package/src/zexus/blockchain/loadtest.py +721 -0
  31. package/src/zexus/blockchain/monitoring.py +350 -0
  32. package/src/zexus/blockchain/mpt.py +716 -0
  33. package/src/zexus/blockchain/multichain.py +951 -0
  34. package/src/zexus/blockchain/multiprocess_executor.py +338 -0
  35. package/src/zexus/blockchain/network.py +886 -0
  36. package/src/zexus/blockchain/node.py +666 -0
  37. package/src/zexus/blockchain/rpc.py +1203 -0
  38. package/src/zexus/blockchain/rust_bridge.py +421 -0
  39. package/src/zexus/blockchain/storage.py +423 -0
  40. package/src/zexus/blockchain/tokens.py +750 -0
  41. package/src/zexus/blockchain/upgradeable.py +1004 -0
  42. package/src/zexus/blockchain/verification.py +1602 -0
  43. package/src/zexus/blockchain/wallet.py +621 -0
  44. package/src/zexus/capability_system.py +184 -9
  45. package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
  46. package/src/zexus/cli/main.py +383 -34
  47. package/src/zexus/cli/zpm.py +1 -1
  48. package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
  49. package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
  50. package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
  51. package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
  52. package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  53. package/src/zexus/compiler/bytecode.py +124 -7
  54. package/src/zexus/compiler/compat_runtime.py +6 -2
  55. package/src/zexus/compiler/lexer.py +16 -5
  56. package/src/zexus/compiler/parser.py +108 -7
  57. package/src/zexus/compiler/semantic.py +18 -19
  58. package/src/zexus/compiler/zexus_ast.py +26 -1
  59. package/src/zexus/concurrency_system.py +79 -0
  60. package/src/zexus/config.py +54 -0
  61. package/src/zexus/crypto_bridge.py +244 -8
  62. package/src/zexus/dap/__init__.py +10 -0
  63. package/src/zexus/dap/__main__.py +4 -0
  64. package/src/zexus/dap/dap_server.py +391 -0
  65. package/src/zexus/dap/debug_engine.py +298 -0
  66. package/src/zexus/environment.py +112 -9
  67. package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
  68. package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
  69. package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
  70. package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
  71. package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
  72. package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
  73. package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
  74. package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
  75. package/src/zexus/evaluator/bytecode_compiler.py +457 -37
  76. package/src/zexus/evaluator/core.py +644 -50
  77. package/src/zexus/evaluator/expressions.py +358 -62
  78. package/src/zexus/evaluator/functions.py +458 -20
  79. package/src/zexus/evaluator/resource_limiter.py +4 -4
  80. package/src/zexus/evaluator/statements.py +774 -122
  81. package/src/zexus/evaluator/unified_execution.py +573 -72
  82. package/src/zexus/evaluator/utils.py +14 -2
  83. package/src/zexus/evaluator_original.py +1 -1
  84. package/src/zexus/event_loop.py +186 -0
  85. package/src/zexus/lexer.py +742 -458
  86. package/src/zexus/lsp/__init__.py +1 -1
  87. package/src/zexus/lsp/definition_provider.py +163 -9
  88. package/src/zexus/lsp/server.py +22 -8
  89. package/src/zexus/lsp/symbol_provider.py +182 -9
  90. package/src/zexus/module_cache.py +239 -9
  91. package/src/zexus/module_manager.py +129 -1
  92. package/src/zexus/object.py +76 -6
  93. package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
  94. package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
  95. package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
  96. package/src/zexus/parser/parser.py +1349 -408
  97. package/src/zexus/parser/strategy_context.py +755 -58
  98. package/src/zexus/parser/strategy_structural.py +121 -21
  99. package/src/zexus/persistence.py +15 -1
  100. package/src/zexus/renderer/__init__.py +61 -0
  101. package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
  102. package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
  103. package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
  104. package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
  105. package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
  106. package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
  107. package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
  108. package/src/zexus/renderer/backend.py +261 -0
  109. package/src/zexus/renderer/canvas.py +78 -0
  110. package/src/zexus/renderer/color_system.py +201 -0
  111. package/src/zexus/renderer/graphics.py +31 -0
  112. package/src/zexus/renderer/layout.py +222 -0
  113. package/src/zexus/renderer/main_renderer.py +66 -0
  114. package/src/zexus/renderer/painter.py +30 -0
  115. package/src/zexus/renderer/tk_backend.py +208 -0
  116. package/src/zexus/renderer/web_backend.py +260 -0
  117. package/src/zexus/runtime/__init__.py +10 -2
  118. package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  119. package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
  120. package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
  121. package/src/zexus/runtime/file_flags.py +137 -0
  122. package/src/zexus/runtime/load_manager.py +368 -0
  123. package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
  124. package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
  125. package/src/zexus/security.py +424 -34
  126. package/src/zexus/stdlib/fs.py +23 -18
  127. package/src/zexus/stdlib/http.py +289 -186
  128. package/src/zexus/stdlib/sockets.py +207 -163
  129. package/src/zexus/stdlib/websockets.py +282 -0
  130. package/src/zexus/stdlib_integration.py +369 -2
  131. package/src/zexus/strategy_recovery.py +6 -3
  132. package/src/zexus/type_checker.py +423 -0
  133. package/src/zexus/virtual_filesystem.py +189 -2
  134. package/src/zexus/vm/__init__.py +113 -3
  135. package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
  136. package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
  137. package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
  138. package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
  139. package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
  140. package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
  141. package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
  142. package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
  143. package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
  144. package/src/zexus/vm/async_optimizer.py +80 -6
  145. package/src/zexus/vm/binary_bytecode.py +659 -0
  146. package/src/zexus/vm/bytecode.py +59 -11
  147. package/src/zexus/vm/bytecode_converter.py +26 -12
  148. package/src/zexus/vm/cabi.c +1985 -0
  149. package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
  150. package/src/zexus/vm/cabi.h +127 -0
  151. package/src/zexus/vm/cache.py +561 -17
  152. package/src/zexus/vm/compiler.py +818 -51
  153. package/src/zexus/vm/fastops.c +15743 -0
  154. package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
  155. package/src/zexus/vm/fastops.pyx +288 -0
  156. package/src/zexus/vm/gas_metering.py +50 -9
  157. package/src/zexus/vm/jit.py +364 -20
  158. package/src/zexus/vm/native_jit_backend.py +1816 -0
  159. package/src/zexus/vm/native_runtime.cpp +1388 -0
  160. package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
  161. package/src/zexus/vm/optimizer.py +161 -11
  162. package/src/zexus/vm/parallel_vm.py +140 -45
  163. package/src/zexus/vm/peephole_optimizer.py +82 -4
  164. package/src/zexus/vm/profiler.py +38 -18
  165. package/src/zexus/vm/register_allocator.py +16 -5
  166. package/src/zexus/vm/register_vm.py +8 -5
  167. package/src/zexus/vm/vm.py +3581 -531
  168. package/src/zexus/vm/wasm_compiler.py +658 -0
  169. package/src/zexus/zexus_ast.py +137 -11
  170. package/src/zexus/zexus_token.py +16 -5
  171. package/src/zexus/zpm/installer.py +55 -15
  172. package/src/zexus/zpm/package_manager.py +1 -1
  173. package/src/zexus/zpm/registry.py +257 -28
  174. package/src/zexus.egg-info/PKG-INFO +16 -6
  175. package/src/zexus.egg-info/SOURCES.txt +129 -17
  176. package/src/zexus.egg-info/entry_points.txt +1 -0
  177. package/src/zexus.egg-info/requires.txt +4 -0
@@ -0,0 +1,1019 @@
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
+ # SmartContract from security module
62
+ try:
63
+ from ..security import SmartContract
64
+ _CONTRACT_AVAILABLE = True
65
+ except ImportError:
66
+ _CONTRACT_AVAILABLE = False
67
+ SmartContract = None # type: ignore
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Contract State Adapter
72
+ # ---------------------------------------------------------------------------
73
+
74
+ class ContractStateAdapter(dict):
75
+ """A dict-like object that transparently delegates reads/writes to
76
+ ``Chain.contract_state[contract_address]``.
77
+
78
+ Every *write* is recorded in a pending journal so the caller can
79
+ commit or rollback atomically.
80
+ """
81
+
82
+ def __init__(self, chain: Chain, contract_address: str):
83
+ super().__init__()
84
+ self._chain = chain
85
+ self._contract_address = contract_address
86
+ # Seed from chain (make a shallow copy so mutations don't leak back)
87
+ stored = chain.contract_state.get(contract_address, {})
88
+ super().update(copy.deepcopy(stored))
89
+
90
+ # Reads ---------------------------------------------------------------
91
+
92
+ def __getitem__(self, key: str) -> Any:
93
+ return super().__getitem__(key)
94
+
95
+ def get(self, key: str, default: Any = None) -> Any:
96
+ return super().get(key, default)
97
+
98
+ # Writes (journalled) -------------------------------------------------
99
+
100
+ def __setitem__(self, key: str, value: Any):
101
+ super().__setitem__(key, value)
102
+
103
+ def update(self, other=(), **kwargs):
104
+ super().update(other, **kwargs)
105
+
106
+ # Commit / Rollback ---------------------------------------------------
107
+
108
+ def commit(self):
109
+ """Flush all current state back to ``chain.contract_state``."""
110
+ self._chain.contract_state[self._contract_address] = dict(self)
111
+
112
+ def rollback(self, snapshot: Dict[str, Any]):
113
+ """Restore from a previously captured snapshot."""
114
+ self.clear()
115
+ super().update(snapshot)
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Execution Receipt
120
+ # ---------------------------------------------------------------------------
121
+
122
+ @dataclass
123
+ class ContractExecutionReceipt:
124
+ """Result of executing a contract action through the VM."""
125
+ success: bool = True
126
+ return_value: Any = None
127
+ gas_used: int = 0
128
+ gas_limit: int = 0
129
+ logs: List[Dict[str, Any]] = field(default_factory=list)
130
+ error: str = ""
131
+ revert_reason: str = ""
132
+ state_changes: Dict[str, Any] = field(default_factory=dict)
133
+
134
+ def to_dict(self) -> Dict[str, Any]:
135
+ return {
136
+ "success": self.success,
137
+ "return_value": str(self.return_value),
138
+ "gas_used": self.gas_used,
139
+ "gas_limit": self.gas_limit,
140
+ "logs": self.logs,
141
+ "error": self.error,
142
+ "revert_reason": self.revert_reason,
143
+ "state_changes": self.state_changes,
144
+ }
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # ContractVM — the bridge
149
+ # ---------------------------------------------------------------------------
150
+
151
+ class ContractVM:
152
+ """Bridge between the Zexus VM and the real blockchain infrastructure.
153
+
154
+ Responsibilities
155
+ ----------------
156
+ 1. Provide a real ``_blockchain_state`` backed by ``Chain.contract_state``
157
+ so that STATE_READ / STATE_WRITE opcodes persist to the chain.
158
+ 2. Inject a proper ``verify_sig`` builtin so VERIFY_SIGNATURE uses
159
+ ``CryptoPlugin.verify_signature`` instead of the insecure SHA-256
160
+ fallback.
161
+ 3. Enforce gas metering for every opcode in both sync *and* async paths.
162
+ 4. Wire TX_BEGIN / TX_COMMIT / TX_REVERT to atomic chain-state updates.
163
+ 5. Execute ``SmartContract`` actions through the VM with a full
164
+ ``TransactionContext``.
165
+ """
166
+
167
+ def __init__(
168
+ self,
169
+ chain: Chain,
170
+ gas_limit: int = 10_000_000,
171
+ debug: bool = False,
172
+ use_bytecode_vm: bool = False,
173
+ ):
174
+ if not _VM_AVAILABLE:
175
+ raise RuntimeError(
176
+ "ContractVM requires the Zexus VM. "
177
+ "Ensure src/zexus/vm/ is present and importable."
178
+ )
179
+ self._chain = chain
180
+ self._default_gas_limit = gas_limit
181
+ self._debug = debug
182
+ self._use_bytecode_vm = use_bytecode_vm
183
+
184
+ # Deployed contract registry: address -> SmartContract
185
+ self._contracts: Dict[str, SmartContract] = {}
186
+
187
+ # Reentrancy guard — tracks contracts currently being executed
188
+ self._executing: set = set()
189
+
190
+ # Cross-contract call depth tracking
191
+ self._call_depth: int = 0
192
+ self._max_call_depth: int = 10
193
+
194
+ # Phase 0 stats: track bytecode vs tree-walk executions
195
+ self._vm_stats = {
196
+ "bytecode_executions": 0,
197
+ "bytecode_fallbacks": 0,
198
+ "treewalk_executions": 0,
199
+ }
200
+
201
+ # ------------------------------------------------------------------
202
+ # Contract lifecycle
203
+ # ------------------------------------------------------------------
204
+
205
+ def deploy_contract(
206
+ self,
207
+ contract: "SmartContract",
208
+ deployer: str,
209
+ gas_limit: Optional[int] = None,
210
+ initial_value: int = 0,
211
+ ) -> ContractExecutionReceipt:
212
+ """Deploy a SmartContract onto the chain.
213
+
214
+ - Assigns the contract an on-chain address.
215
+ - Stores initial bytecode / storage in ``chain.contract_state``.
216
+ - Runs the constructor (if any) inside the VM.
217
+ """
218
+ gas_limit = gas_limit or self._default_gas_limit
219
+ address = contract.address
220
+
221
+ # Register on-chain account
222
+ acct = self._chain.get_account(address)
223
+ acct["balance"] = initial_value
224
+ acct["code"] = contract.name # Store contract "type" as code
225
+ acct["nonce"] = 0
226
+
227
+ # Save initial storage
228
+ initial_storage: Dict[str, Any] = {}
229
+ if hasattr(contract, 'storage') and hasattr(contract.storage, 'current_state'):
230
+ initial_storage = dict(contract.storage.current_state)
231
+ elif hasattr(contract, 'storage') and hasattr(contract.storage, 'data'):
232
+ initial_storage = dict(contract.storage.data)
233
+ self._chain.contract_state[address] = initial_storage
234
+
235
+ # Register locally
236
+ self._contracts[address] = contract
237
+
238
+ receipt = ContractExecutionReceipt(
239
+ success=True,
240
+ gas_limit=gas_limit,
241
+ state_changes={"deployed": address, "storage_keys": list(initial_storage.keys())},
242
+ )
243
+
244
+ logger.info("Contract '%s' deployed at %s", contract.name, address)
245
+ return receipt
246
+
247
+ def get_contract(self, address: str) -> Optional["SmartContract"]:
248
+ """Look up a deployed contract by address."""
249
+ return self._contracts.get(address)
250
+
251
+ # ------------------------------------------------------------------
252
+ # Contract execution
253
+ # ------------------------------------------------------------------
254
+
255
+ def execute_contract(
256
+ self,
257
+ contract_address: str,
258
+ action: str,
259
+ args: Optional[Dict[str, Any]] = None,
260
+ caller: str = "",
261
+ gas_limit: Optional[int] = None,
262
+ value: int = 0,
263
+ ) -> ContractExecutionReceipt:
264
+ """Execute a contract action inside the VM.
265
+
266
+ This is the main entry-point used by ``BlockchainNode`` when
267
+ processing a contract-call transaction.
268
+
269
+ Steps
270
+ -----
271
+ 1. Build a ``TransactionContext`` with the caller, gas limit, etc.
272
+ 2. Create a ``ContractStateAdapter`` backed by the chain.
273
+ 3. Construct a fresh VM with the state adapter as
274
+ ``env["_blockchain_state"]`` and real ``verify_sig``.
275
+ 4. Execute the contract's action body via the VM.
276
+ 5. On success → commit state; on failure → rollback.
277
+ 6. Return a ``ContractExecutionReceipt``.
278
+ """
279
+ gas_limit = gas_limit or self._default_gas_limit
280
+ # Per-execution log list — avoids sharing state across concurrent calls
281
+ logs: List[Dict[str, Any]] = []
282
+
283
+ contract = self._contracts.get(contract_address)
284
+ if contract is None:
285
+ return ContractExecutionReceipt(
286
+ success=False,
287
+ error=f"Contract not found at {contract_address}",
288
+ gas_limit=gas_limit,
289
+ )
290
+
291
+ action_obj = contract.actions.get(action)
292
+ if action_obj is None:
293
+ return ContractExecutionReceipt(
294
+ success=False,
295
+ error=f"Action '{action}' not found on contract '{contract.name}'",
296
+ gas_limit=gas_limit,
297
+ )
298
+
299
+ # Reentrancy guard
300
+ if contract_address in self._executing:
301
+ return ContractExecutionReceipt(
302
+ success=False,
303
+ error="ReentrancyGuard",
304
+ revert_reason=f"Reentrant call to contract {contract_address}",
305
+ gas_limit=gas_limit,
306
+ )
307
+
308
+ # Call-depth guard (cross-contract calls)
309
+ if self._call_depth >= self._max_call_depth:
310
+ return ContractExecutionReceipt(
311
+ success=False,
312
+ error="CallDepthExceeded",
313
+ revert_reason=f"Call depth {self._call_depth} exceeds max {self._max_call_depth}",
314
+ gas_limit=gas_limit,
315
+ )
316
+
317
+ self._executing.add(contract_address)
318
+ self._call_depth += 1
319
+
320
+ # 1. TX context
321
+ tip = self._chain.tip
322
+ tx_ctx = TransactionContext(
323
+ caller=caller,
324
+ timestamp=time.time(),
325
+ block_hash=tip.hash if tip else "0" * 64,
326
+ gas_limit=gas_limit,
327
+ )
328
+
329
+ # 2. Chain-backed state adapter
330
+ state_adapter = ContractStateAdapter(self._chain, contract_address)
331
+ snapshot = dict(state_adapter) # for rollback
332
+
333
+ # 3. Build VM environment + builtins
334
+ env = self._build_env(state_adapter, tx_ctx, contract, args or {})
335
+ builtins = self._build_builtins(tx_ctx, contract_address, logs)
336
+
337
+ # 4. Execute
338
+ try:
339
+ vm = ZexusVM(
340
+ env=env,
341
+ builtins=builtins,
342
+ enable_gas_metering=True,
343
+ gas_limit=gas_limit,
344
+ debug=self._debug,
345
+ )
346
+
347
+ # Execute the action body through the evaluator
348
+ result = self._execute_action(vm, action_obj, env, args or {})
349
+
350
+ gas_used = vm.gas_metering.gas_used if vm.gas_metering else 0
351
+
352
+ # 5a. Commit
353
+ state_adapter.commit()
354
+
355
+ return ContractExecutionReceipt(
356
+ success=True,
357
+ return_value=result,
358
+ gas_used=gas_used,
359
+ gas_limit=gas_limit,
360
+ logs=list(logs),
361
+ state_changes=self._diff_state(snapshot, dict(state_adapter)),
362
+ )
363
+
364
+ except OutOfGasError as e:
365
+ # 5b. Rollback on OOG
366
+ state_adapter.rollback(snapshot)
367
+ return ContractExecutionReceipt(
368
+ success=False,
369
+ gas_used=gas_limit,
370
+ gas_limit=gas_limit,
371
+ error="OutOfGas",
372
+ revert_reason=str(e),
373
+ )
374
+
375
+ except Exception as e:
376
+ # 5b. Rollback on any error
377
+ state_adapter.rollback(snapshot)
378
+ return ContractExecutionReceipt(
379
+ success=False,
380
+ gas_used=0,
381
+ gas_limit=gas_limit,
382
+ error=type(e).__name__,
383
+ revert_reason=str(e),
384
+ logs=list(logs),
385
+ )
386
+
387
+ finally:
388
+ self._executing.discard(contract_address)
389
+ self._call_depth -= 1
390
+
391
+ # ------------------------------------------------------------------
392
+ # Internal helpers
393
+ # ------------------------------------------------------------------
394
+
395
+ def _build_env(
396
+ self,
397
+ state_adapter: ContractStateAdapter,
398
+ tx_ctx: TransactionContext,
399
+ contract: "SmartContract",
400
+ args: Dict[str, Any],
401
+ ) -> Dict[str, Any]:
402
+ """Assemble the VM ``env`` dict for a contract execution."""
403
+ from ..object import Map, String, Integer, Float, Boolean as BooleanObj
404
+
405
+ env: Dict[str, Any] = {}
406
+
407
+ # Wire the chain-backed state adapter as _blockchain_state
408
+ env["_blockchain_state"] = state_adapter
409
+
410
+ # Gas tracking (used by GAS_CHARGE opcode)
411
+ env["_gas_remaining"] = tx_ctx.gas_limit
412
+
413
+ # TX object — immutable context
414
+ tx_map = Map({
415
+ String("caller"): String(tx_ctx.caller),
416
+ String("timestamp"): Integer(int(tx_ctx.timestamp)),
417
+ String("block_hash"): String(tx_ctx.block_hash),
418
+ String("gas_limit"): Integer(tx_ctx.gas_limit),
419
+ String("gas_remaining"): Integer(tx_ctx.gas_remaining),
420
+ })
421
+ env["TX"] = tx_map
422
+
423
+ # Pre-populate contract storage into env for tree-walking evaluator
424
+ if hasattr(contract, 'storage'):
425
+ state = state_adapter # already seeded from chain
426
+ for key, val in state.items():
427
+ env[key] = self._wrap_value(val)
428
+
429
+ # Arguments (passed as env vars to the action)
430
+ for k, v in args.items():
431
+ env[k] = self._wrap_value(v)
432
+
433
+ # Contract reference
434
+ env["self"] = contract
435
+ env["_contract_address"] = contract.address
436
+
437
+ return env
438
+
439
+ def _build_builtins(
440
+ self,
441
+ tx_ctx: TransactionContext,
442
+ contract_address: str = "",
443
+ logs: Optional[List[Dict[str, Any]]] = None,
444
+ ) -> Dict[str, Any]:
445
+ """Build VM builtins, including the real ``verify_sig``."""
446
+ builtins: Dict[str, Any] = {}
447
+ _logs = logs if logs is not None else []
448
+
449
+ # Real signature verification via CryptoPlugin
450
+ def verify_sig(signature: Any, message: Any, public_key: Any) -> bool:
451
+ """Verify an ECDSA signature using the real CryptoPlugin."""
452
+ sig_str = str(signature.value) if hasattr(signature, 'value') else str(signature)
453
+ msg_str = str(message.value) if hasattr(message, 'value') else str(message)
454
+ key_str = str(public_key.value) if hasattr(public_key, 'value') else str(public_key)
455
+ try:
456
+ return CryptoPlugin.verify_signature(msg_str, sig_str, key_str)
457
+ except Exception:
458
+ return False
459
+
460
+ builtins["verify_sig"] = verify_sig
461
+
462
+ # Emit log/event
463
+ def emit_event(name: Any, data: Any = None) -> None:
464
+ """Emit a contract event (stored in receipt logs)."""
465
+ name_str = str(name.value) if hasattr(name, 'value') else str(name)
466
+ _logs.append({
467
+ "event": name_str,
468
+ "data": data,
469
+ "timestamp": time.time(),
470
+ "contract": contract_address, # emit from contract, not caller
471
+ })
472
+
473
+ builtins["emit"] = emit_event
474
+
475
+ # Balance check
476
+ def get_balance(address: Any) -> int:
477
+ """Get on-chain balance of an address."""
478
+ addr = str(address.value) if hasattr(address, 'value') else str(address)
479
+ return self._chain.get_account(addr).get("balance", 0)
480
+
481
+ builtins["get_balance"] = get_balance
482
+
483
+ # Transfer — with overflow protection
484
+ def transfer(to: Any, amount: Any) -> bool:
485
+ """Transfer value between accounts."""
486
+ to_str = str(to.value) if hasattr(to, 'value') else str(to)
487
+ amt = int(amount.value) if hasattr(amount, 'value') else int(amount)
488
+ if amt <= 0:
489
+ return False
490
+ caller_acct = self._chain.get_account(tx_ctx.caller)
491
+ sender_balance = caller_acct.get("balance", 0)
492
+ if sender_balance < amt:
493
+ return False
494
+ to_acct = self._chain.get_account(to_str)
495
+ to_balance = to_acct.get("balance", 0)
496
+ # Overflow check
497
+ if to_balance + amt < to_balance:
498
+ return False
499
+ caller_acct["balance"] = sender_balance - amt
500
+ to_acct["balance"] = to_balance + amt
501
+ return True
502
+
503
+ builtins["transfer"] = transfer
504
+
505
+ # Keccak-256 hash
506
+ def keccak256(data: Any) -> str:
507
+ """Keccak-256 hash via CryptoPlugin."""
508
+ d = str(data.value) if hasattr(data, 'value') else str(data)
509
+ return CryptoPlugin.keccak256(d)
510
+
511
+ builtins["keccak256"] = keccak256
512
+
513
+ # Block info
514
+ def block_number() -> int:
515
+ return self._chain.height
516
+
517
+ def block_timestamp() -> float:
518
+ tip = self._chain.tip
519
+ return tip.header.timestamp if tip else 0.0
520
+
521
+ builtins["block_number"] = block_number
522
+ builtins["block_timestamp"] = block_timestamp
523
+
524
+ # ── Cross-contract calls ──────────────────────────────────
525
+ vm_ref = self # capture for closures
526
+
527
+ def contract_call(target_address: Any, action: Any,
528
+ call_args: Any = None, value: Any = None) -> Any:
529
+ """Call another contract's action (state-mutating).
530
+
531
+ Parameters
532
+ ----------
533
+ target_address : str or String
534
+ Address of the contract to call.
535
+ action : str or String
536
+ Name of the action to invoke.
537
+ call_args : dict, optional
538
+ Arguments to pass to the action.
539
+ value : int, optional
540
+ Value to transfer with the call.
541
+
542
+ Returns the action's return value (unwrapped to Python).
543
+ Raises RuntimeError on failure or depth exceeded.
544
+ """
545
+ addr = str(target_address.value) if hasattr(target_address, 'value') else str(target_address)
546
+ act = str(action.value) if hasattr(action, 'value') else str(action)
547
+ args = {}
548
+ if call_args is not None:
549
+ if hasattr(call_args, 'pairs'):
550
+ args = {str(k.value) if hasattr(k, 'value') else str(k):
551
+ vm_ref._unwrap_value(v) for k, v in call_args.pairs.items()}
552
+ elif isinstance(call_args, dict):
553
+ args = call_args
554
+ val = 0
555
+ if value is not None:
556
+ val = int(value.value) if hasattr(value, 'value') else int(value)
557
+
558
+ if vm_ref._call_depth >= vm_ref._max_call_depth:
559
+ raise RuntimeError(f"Cross-contract call depth exceeded (max {vm_ref._max_call_depth})")
560
+
561
+ receipt = vm_ref.execute_contract(
562
+ contract_address=addr,
563
+ action=act,
564
+ args=args,
565
+ caller=contract_address or tx_ctx.caller,
566
+ gas_limit=tx_ctx.gas_remaining,
567
+ value=val,
568
+ )
569
+ if not receipt.success:
570
+ raise RuntimeError(f"Cross-contract call failed: {receipt.error or receipt.revert_reason}")
571
+ return receipt.return_value
572
+
573
+ def static_contract_call(target_address: Any, action: Any,
574
+ call_args: Any = None) -> Any:
575
+ """Read-only call to another contract (no state changes).
576
+
577
+ Same as contract_call but uses static_call internally.
578
+ """
579
+ addr = str(target_address.value) if hasattr(target_address, 'value') else str(target_address)
580
+ act = str(action.value) if hasattr(action, 'value') else str(action)
581
+ args = {}
582
+ if call_args is not None:
583
+ if hasattr(call_args, 'pairs'):
584
+ args = {str(k.value) if hasattr(k, 'value') else str(k):
585
+ vm_ref._unwrap_value(v) for k, v in call_args.pairs.items()}
586
+ elif isinstance(call_args, dict):
587
+ args = call_args
588
+
589
+ receipt = vm_ref.static_call(
590
+ contract_address=addr,
591
+ action=act,
592
+ args=args,
593
+ caller=contract_address or tx_ctx.caller,
594
+ )
595
+ if not receipt.success:
596
+ raise RuntimeError(f"Static call failed: {receipt.error or receipt.revert_reason}")
597
+ return receipt.return_value
598
+
599
+ def delegate_call(target_address: Any, action: Any,
600
+ call_args: Any = None) -> Any:
601
+ """Delegatecall: execute target's code in caller's storage context.
602
+
603
+ Like contract_call, but the target's action runs with the
604
+ *calling* contract's state adapter, so state writes go to
605
+ the caller's storage, not the target's.
606
+ """
607
+ addr = str(target_address.value) if hasattr(target_address, 'value') else str(target_address)
608
+ act = str(action.value) if hasattr(action, 'value') else str(action)
609
+ args = {}
610
+ if call_args is not None:
611
+ if hasattr(call_args, 'pairs'):
612
+ args = {str(k.value) if hasattr(k, 'value') else str(k):
613
+ vm_ref._unwrap_value(v) for k, v in call_args.pairs.items()}
614
+ elif isinstance(call_args, dict):
615
+ args = call_args
616
+
617
+ if vm_ref._call_depth >= vm_ref._max_call_depth:
618
+ raise RuntimeError(f"Delegatecall depth exceeded (max {vm_ref._max_call_depth})")
619
+
620
+ # Find the target contract's action
621
+ target_contract = vm_ref.get_contract(addr)
622
+ if target_contract is None:
623
+ raise RuntimeError(f"Contract not found: {addr}")
624
+
625
+ action_obj = None
626
+ if hasattr(target_contract, 'actions'):
627
+ for a in target_contract.actions:
628
+ a_name = a.name if hasattr(a, 'name') else str(a)
629
+ if a_name == act:
630
+ action_obj = a
631
+ break
632
+ if action_obj is None:
633
+ raise RuntimeError(f"Action '{act}' not found on contract {addr}")
634
+
635
+ # Execute with *caller's* state adapter (the key difference)
636
+ caller_addr = contract_address or tx_ctx.caller
637
+ state_adapter = ContractStateAdapter(vm_ref._chain, caller_addr)
638
+ snapshot = dict(state_adapter)
639
+
640
+ from ..vm.vm import VM as ZexusVM
641
+ vm = ZexusVM(debug=vm_ref._debug)
642
+ vm_ref._call_depth += 1
643
+ try:
644
+ env = vm_ref._build_env(state_adapter, tx_ctx, target_contract, args)
645
+ inner_builtins = vm_ref._build_builtins(tx_ctx, caller_addr, _logs)
646
+ for bk, bv in inner_builtins.items():
647
+ vm.env[bk] = bv
648
+ result = vm_ref._execute_action(vm, action_obj, env, args)
649
+ state_adapter.commit()
650
+ return vm_ref._unwrap_value(result) if result is not None else None
651
+ except Exception:
652
+ state_adapter.rollback(snapshot)
653
+ raise
654
+ finally:
655
+ vm_ref._call_depth -= 1
656
+
657
+ builtins["contract_call"] = contract_call
658
+ builtins["static_call"] = static_contract_call
659
+ builtins["delegate_call"] = delegate_call
660
+
661
+ return builtins
662
+
663
+ def _execute_action(
664
+ self,
665
+ vm: "ZexusVM",
666
+ action_obj: Any,
667
+ env: Dict[str, Any],
668
+ args: Dict[str, Any],
669
+ ) -> Any:
670
+ """Run a contract action's body through the evaluator.
671
+
672
+ When ``self._use_bytecode_vm`` is True the evaluator is created
673
+ with ``use_vm=True`` so it compiles the action body to bytecode
674
+ and executes it through the VM (Phase 0). If bytecoded execution
675
+ fails for any reason, we transparently fall back to tree-walking.
676
+
677
+ When ``self._use_bytecode_vm`` is False (default), we always use
678
+ tree-walking for maximum reliability.
679
+ """
680
+ from ..object import Environment, Action
681
+ from ..evaluator.core import Evaluator
682
+
683
+ # Build an evaluator Environment from the flat dict
684
+ eval_env = Environment()
685
+ for k, v in env.items():
686
+ eval_env.set(k, v)
687
+
688
+ # Add action parameters from args
689
+ if hasattr(action_obj, 'parameters') and action_obj.parameters:
690
+ for param in action_obj.parameters:
691
+ param_name = param.value if hasattr(param, 'value') else str(param)
692
+ if param_name in args:
693
+ eval_env.set(param_name, self._wrap_value(args[param_name]))
694
+
695
+ result = None
696
+ used_bytecode = False
697
+
698
+ # --- Phase 1: try pre-compiled .zxc for this action ---
699
+ if self._use_bytecode_vm and hasattr(action_obj, 'body'):
700
+ cached_bc = getattr(action_obj, '_cached_bytecode', None)
701
+ if cached_bc is None:
702
+ try:
703
+ from ..vm.binary_bytecode import load_zxc, save_zxc
704
+ import hashlib as _hl
705
+ _action_name = getattr(action_obj, 'name', None)
706
+ if _action_name:
707
+ _aname = _action_name.value if hasattr(_action_name, 'value') else str(_action_name)
708
+ _contract_addr = getattr(self, '_contract_address', '') or ''
709
+ _cache_key = _hl.md5(f"{_contract_addr}:{_aname}".encode()).hexdigest()[:16]
710
+ _cache_dir = os.path.join(os.path.expanduser("~"), ".zexus", "action_cache")
711
+ os.makedirs(_cache_dir, exist_ok=True)
712
+ _zxc_path = os.path.join(_cache_dir, f"{_cache_key}.zxc")
713
+ if os.path.exists(_zxc_path):
714
+ cached_bc = load_zxc(_zxc_path)
715
+ action_obj._cached_bytecode = cached_bc
716
+ action_obj._cached_zxc_path = _zxc_path
717
+ except Exception:
718
+ pass
719
+
720
+ # --- Phase 0: bytecoded execution with fallback ---
721
+ if self._use_bytecode_vm and hasattr(action_obj, 'body'):
722
+ try:
723
+ evaluator = Evaluator(use_vm=True)
724
+
725
+ # Wire contract gas metering into the evaluator's VM
726
+ if evaluator.vm_instance and vm.gas_metering:
727
+ evaluator.vm_instance.gas_metering = vm.gas_metering
728
+ evaluator.vm_instance.enable_gas_metering = True
729
+
730
+ # Inject contract builtins into evaluator's VM
731
+ if evaluator.vm_instance:
732
+ vm_builtins = dict(evaluator.vm_instance.builtins or {})
733
+ vm_builtins.update(vm.builtins or {})
734
+ evaluator.vm_instance.builtins = vm_builtins
735
+
736
+ # Push blockchain state into the VM's env
737
+ evaluator.vm_instance.env["_blockchain_state"] = env.get("_blockchain_state")
738
+ evaluator.vm_instance.env["_gas_remaining"] = env.get("_gas_remaining")
739
+ evaluator.vm_instance.env["TX"] = env.get("TX")
740
+
741
+ result = evaluator.eval_with_vm_support(
742
+ action_obj.body, eval_env, debug_mode=self._debug
743
+ )
744
+ used_bytecode = True
745
+ self._vm_stats["bytecode_executions"] += 1
746
+
747
+ # Phase 1: persist compiled bytecode as .zxc for next call
748
+ if not getattr(action_obj, '_cached_zxc_path', None):
749
+ try:
750
+ _bc = getattr(evaluator, '_last_compiled_bytecode', None)
751
+ if _bc is None and hasattr(evaluator, 'vm_instance'):
752
+ _bc = getattr(evaluator.vm_instance, '_last_bytecode', None)
753
+ if _bc is not None:
754
+ from ..vm.binary_bytecode import save_zxc
755
+ import hashlib as _hl
756
+ _action_name = getattr(action_obj, 'name', None)
757
+ if _action_name:
758
+ _aname = _action_name.value if hasattr(_action_name, 'value') else str(_action_name)
759
+ _contract_addr = getattr(self, '_contract_address', '') or ''
760
+ _cache_key = _hl.md5(f"{_contract_addr}:{_aname}".encode()).hexdigest()[:16]
761
+ _cache_dir = os.path.join(os.path.expanduser("~"), ".zexus", "action_cache")
762
+ os.makedirs(_cache_dir, exist_ok=True)
763
+ save_zxc(os.path.join(_cache_dir, f"{_cache_key}.zxc"), _bc)
764
+ except Exception:
765
+ pass
766
+ if self._debug:
767
+ stats = evaluator.get_vm_stats()
768
+ logger.debug(
769
+ "Bytecoded execution: compiles=%d vm_runs=%d fallbacks=%d",
770
+ stats.get("bytecode_compiles", 0),
771
+ stats.get("vm_executions", 0),
772
+ stats.get("vm_fallbacks", 0),
773
+ )
774
+ except Exception as e:
775
+ # Bytecoded execution failed — fall back to tree-walk
776
+ self._vm_stats["bytecode_fallbacks"] += 1
777
+ logger.debug(
778
+ "Bytecoded execution failed, falling back to tree-walk: %s", e
779
+ )
780
+ used_bytecode = False
781
+ result = None
782
+ # Rebuild eval_env since the failed VM run may have corrupted it
783
+ eval_env = Environment()
784
+ for k, v in env.items():
785
+ eval_env.set(k, v)
786
+ if hasattr(action_obj, 'parameters') and action_obj.parameters:
787
+ for param in action_obj.parameters:
788
+ param_name = param.value if hasattr(param, 'value') else str(param)
789
+ if param_name in args:
790
+ eval_env.set(param_name, self._wrap_value(args[param_name]))
791
+
792
+ # --- Tree-walk execution (default or fallback) ---
793
+ if not used_bytecode and hasattr(action_obj, 'body'):
794
+ evaluator = Evaluator(use_vm=False)
795
+ self._vm_stats["treewalk_executions"] += 1
796
+ try:
797
+ result = evaluator.eval_node(action_obj.body, eval_env, [])
798
+ except Exception as e:
799
+ if "Requirement failed" in str(e):
800
+ raise # Re-raise REQUIRE failures
801
+ raise
802
+
803
+ # Sync modified vars back to _blockchain_state
804
+ state_adapter = env.get("_blockchain_state")
805
+ if state_adapter and hasattr(action_obj, 'body'):
806
+ # Check for any env vars that match storage keys
807
+ for key in list(state_adapter.keys()):
808
+ new_val = eval_env.get(key)
809
+ if new_val is not None:
810
+ state_adapter[key] = self._unwrap_value(new_val)
811
+
812
+ return result
813
+
814
+ def get_vm_execution_stats(self) -> Dict[str, Any]:
815
+ """Return Phase 0 execution statistics."""
816
+ total = sum(self._vm_stats.values())
817
+ return {
818
+ **self._vm_stats,
819
+ "total_executions": total,
820
+ "bytecode_rate": (
821
+ self._vm_stats["bytecode_executions"] / total * 100
822
+ if total > 0 else 0.0
823
+ ),
824
+ "use_bytecode_vm": self._use_bytecode_vm,
825
+ }
826
+
827
+ # ------------------------------------------------------------------
828
+ # Value wrapping / unwrapping
829
+ # ------------------------------------------------------------------
830
+
831
+ @staticmethod
832
+ def _wrap_value(val: Any) -> Any:
833
+ """Wrap a Python value into a Zexus object."""
834
+ from ..object import (
835
+ Integer as ZInteger, Float as ZFloat,
836
+ Boolean as ZBoolean, String as ZString,
837
+ List as ZList, Map as ZMap, Null as ZNull,
838
+ )
839
+ if isinstance(val, (ZInteger, ZFloat, ZBoolean, ZString, ZList, ZMap, ZNull)):
840
+ return val
841
+ if isinstance(val, bool):
842
+ return ZBoolean(val)
843
+ if isinstance(val, int):
844
+ return ZInteger(val)
845
+ if isinstance(val, float):
846
+ return ZFloat(val)
847
+ if isinstance(val, str):
848
+ return ZString(val)
849
+ if isinstance(val, list):
850
+ return ZList([ContractVM._wrap_value(e) for e in val])
851
+ if isinstance(val, dict):
852
+ return ZMap({
853
+ ZString(str(k)): ContractVM._wrap_value(v)
854
+ for k, v in val.items()
855
+ })
856
+ if val is None:
857
+ return ZNull()
858
+ return val
859
+
860
+ @staticmethod
861
+ def _unwrap_value(val: Any) -> Any:
862
+ """Unwrap a Zexus object to a plain Python value."""
863
+ if hasattr(val, 'value'):
864
+ return val.value
865
+ if hasattr(val, 'elements'): # ZList
866
+ return [ContractVM._unwrap_value(e) for e in val.elements]
867
+ if hasattr(val, 'pairs'): # ZMap
868
+ return {
869
+ ContractVM._unwrap_value(k): ContractVM._unwrap_value(v)
870
+ for k, v in val.pairs.items()
871
+ }
872
+ return val
873
+
874
+ @staticmethod
875
+ def _diff_state(
876
+ before: Dict[str, Any], after: Dict[str, Any]
877
+ ) -> Dict[str, Any]:
878
+ """Compute the difference between two state snapshots."""
879
+ changes: Dict[str, Any] = {}
880
+ all_keys = set(before.keys()) | set(after.keys())
881
+ for key in all_keys:
882
+ old = before.get(key)
883
+ new = after.get(key)
884
+ if old != new:
885
+ changes[key] = {"before": old, "after": new}
886
+ return changes
887
+
888
+ # ------------------------------------------------------------------
889
+ # Static call (read-only, no state commit)
890
+ # ------------------------------------------------------------------
891
+
892
+ def static_call(
893
+ self,
894
+ contract_address: str,
895
+ action: str,
896
+ args: Optional[Dict[str, Any]] = None,
897
+ caller: str = "",
898
+ gas_limit: Optional[int] = None,
899
+ ) -> ContractExecutionReceipt:
900
+ """Execute a read-only call that never commits state changes.
901
+
902
+ Useful for ``view`` functions that only read storage.
903
+ """
904
+ gas_limit = gas_limit or self._default_gas_limit
905
+ logs: List[Dict[str, Any]] = []
906
+
907
+ contract = self._contracts.get(contract_address)
908
+ if contract is None:
909
+ return ContractExecutionReceipt(
910
+ success=False,
911
+ error=f"Contract not found at {contract_address}",
912
+ gas_limit=gas_limit,
913
+ )
914
+
915
+ action_obj = contract.actions.get(action)
916
+ if action_obj is None:
917
+ return ContractExecutionReceipt(
918
+ success=False,
919
+ error=f"Action '{action}' not found",
920
+ gas_limit=gas_limit,
921
+ )
922
+
923
+ tip = self._chain.tip
924
+ tx_ctx = TransactionContext(
925
+ caller=caller,
926
+ timestamp=time.time(),
927
+ block_hash=tip.hash if tip else "0" * 64,
928
+ gas_limit=gas_limit,
929
+ )
930
+
931
+ state_adapter = ContractStateAdapter(self._chain, contract_address)
932
+ env = self._build_env(state_adapter, tx_ctx, contract, args or {})
933
+ builtins = self._build_builtins(tx_ctx, contract_address, logs)
934
+
935
+ try:
936
+ vm = ZexusVM(
937
+ env=env,
938
+ builtins=builtins,
939
+ enable_gas_metering=True,
940
+ gas_limit=gas_limit,
941
+ debug=self._debug,
942
+ )
943
+ result = self._execute_action(vm, action_obj, env, args or {})
944
+ gas_used = vm.gas_metering.gas_used if vm.gas_metering else 0
945
+
946
+ # NOTE: No commit — state_adapter is discarded
947
+ return ContractExecutionReceipt(
948
+ success=True,
949
+ return_value=result,
950
+ gas_used=gas_used,
951
+ gas_limit=gas_limit,
952
+ logs=list(logs),
953
+ )
954
+ except Exception as e:
955
+ return ContractExecutionReceipt(
956
+ success=False,
957
+ error=type(e).__name__,
958
+ revert_reason=str(e),
959
+ gas_limit=gas_limit,
960
+ )
961
+
962
+ # ------------------------------------------------------------------
963
+ # Batch execution (for block processing)
964
+ # ------------------------------------------------------------------
965
+
966
+ def process_contract_transaction(
967
+ self,
968
+ tx: Transaction,
969
+ ) -> TransactionReceipt:
970
+ """Process a contract-call transaction and produce a receipt.
971
+
972
+ This is what ``BlockchainNode`` calls when processing a block
973
+ that contains contract interactions.
974
+
975
+ The ``tx.data`` field is expected to be JSON-encoded::
976
+
977
+ {
978
+ "contract": "<address>",
979
+ "action": "<method>",
980
+ "args": { ... }
981
+ }
982
+ """
983
+ receipt = TransactionReceipt(
984
+ tx_hash=tx.tx_hash,
985
+ status=0,
986
+ gas_used=0,
987
+ )
988
+
989
+ # Parse tx.data
990
+ try:
991
+ call_data = json.loads(tx.data) if isinstance(tx.data, str) and tx.data else {}
992
+ except json.JSONDecodeError:
993
+ receipt.revert_reason = "Invalid contract call data"
994
+ return receipt
995
+
996
+ contract_addr = call_data.get("contract", tx.recipient)
997
+ action_name = call_data.get("action", "")
998
+ action_args = call_data.get("args", {})
999
+
1000
+ if not action_name:
1001
+ receipt.revert_reason = "Missing action name in tx.data"
1002
+ return receipt
1003
+
1004
+ exec_receipt = self.execute_contract(
1005
+ contract_address=contract_addr,
1006
+ action=action_name,
1007
+ args=action_args,
1008
+ caller=tx.sender,
1009
+ gas_limit=tx.gas_limit,
1010
+ value=tx.value,
1011
+ )
1012
+
1013
+ receipt.status = 1 if exec_receipt.success else 0
1014
+ receipt.gas_used = exec_receipt.gas_used
1015
+ receipt.logs = exec_receipt.logs
1016
+ receipt.revert_reason = exec_receipt.revert_reason
1017
+ receipt.contract_address = contract_addr
1018
+
1019
+ return receipt