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.
- package/README.md +26 -3
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1187 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1425 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +485 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +300 -20
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/lexer.py +10 -5
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +10 -1
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +441 -37
- package/src/zexus/evaluator/core.py +560 -49
- package/src/zexus/evaluator/expressions.py +122 -49
- package/src/zexus/evaluator/functions.py +417 -16
- package/src/zexus/evaluator/statements.py +521 -118
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -486
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +237 -9
- package/src/zexus/object.py +64 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +786 -285
- package/src/zexus/parser/strategy_context.py +407 -66
- package/src/zexus/parser/strategy_structural.py +117 -19
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +15 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +14 -1
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +28 -1
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +557 -17
- package/src/zexus/vm/compiler.py +703 -5
- package/src/zexus/vm/fastops.c +13861 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +52 -11
- package/src/zexus/vm/jit.py +83 -2
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +118 -42
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3589 -588
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +63 -11
- package/src/zexus/zexus_token.py +13 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +30 -4
- package/src/zexus.egg-info/SOURCES.txt +133 -9
- package/src/zexus.egg-info/entry_points.txt +1 -0
- 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
|