zexus 1.6.8 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1160 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1019 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +421 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/capability_system.py +184 -9
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +383 -34
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/bytecode.py +124 -7
- package/src/zexus/compiler/compat_runtime.py +6 -2
- package/src/zexus/compiler/lexer.py +16 -5
- package/src/zexus/compiler/parser.py +108 -7
- package/src/zexus/compiler/semantic.py +18 -19
- package/src/zexus/compiler/zexus_ast.py +26 -1
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +112 -9
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +457 -37
- package/src/zexus/evaluator/core.py +644 -50
- package/src/zexus/evaluator/expressions.py +358 -62
- package/src/zexus/evaluator/functions.py +458 -20
- package/src/zexus/evaluator/resource_limiter.py +4 -4
- package/src/zexus/evaluator/statements.py +774 -122
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/evaluator_original.py +1 -1
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -458
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +239 -9
- package/src/zexus/module_manager.py +129 -1
- package/src/zexus/object.py +76 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +1349 -408
- package/src/zexus/parser/strategy_context.py +755 -58
- package/src/zexus/parser/strategy_structural.py +121 -21
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +61 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/backend.py +261 -0
- package/src/zexus/renderer/canvas.py +78 -0
- package/src/zexus/renderer/color_system.py +201 -0
- package/src/zexus/renderer/graphics.py +31 -0
- package/src/zexus/renderer/layout.py +222 -0
- package/src/zexus/renderer/main_renderer.py +66 -0
- package/src/zexus/renderer/painter.py +30 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__init__.py +10 -2
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/runtime/load_manager.py +368 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +80 -6
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +59 -11
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +561 -17
- package/src/zexus/vm/compiler.py +818 -51
- package/src/zexus/vm/fastops.c +15743 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +50 -9
- package/src/zexus/vm/jit.py +364 -20
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +140 -45
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3581 -531
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +137 -11
- package/src/zexus/zexus_token.py +16 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +16 -6
- package/src/zexus.egg-info/SOURCES.txt +129 -17
- package/src/zexus.egg-info/entry_points.txt +1 -0
- package/src/zexus.egg-info/requires.txt +4 -0
|
@@ -0,0 +1,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
|