zexus 1.8.0 → 1.8.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 +34 -6
- package/bin/zexus +12 -2
- package/bin/zpics +12 -2
- package/bin/zpm +12 -2
- package/bin/zx +12 -2
- package/bin/zx-deploy +12 -2
- package/bin/zx-dev +12 -2
- package/bin/zx-run +12 -2
- package/package.json +2 -1
- package/rust_core/Cargo.lock +603 -0
- package/rust_core/Cargo.toml +26 -0
- package/rust_core/README.md +15 -0
- package/rust_core/pyproject.toml +25 -0
- package/rust_core/src/binary_bytecode.rs +543 -0
- package/rust_core/src/contract_vm.rs +643 -0
- package/rust_core/src/executor.rs +847 -0
- package/rust_core/src/hasher.rs +90 -0
- package/rust_core/src/lib.rs +71 -0
- package/rust_core/src/merkle.rs +128 -0
- package/rust_core/src/rust_vm.rs +2313 -0
- package/rust_core/src/signature.rs +79 -0
- package/rust_core/src/state_adapter.rs +281 -0
- package/rust_core/src/validator.rs +116 -0
- package/scripts/postinstall.js +204 -21
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/cli/main.py +1 -1
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/evaluator/bytecode_compiler.py +150 -52
- package/src/zexus/evaluator/core.py +151 -809
- package/src/zexus/evaluator/expressions.py +27 -22
- package/src/zexus/evaluator/functions.py +171 -126
- package/src/zexus/evaluator/statements.py +55 -112
- package/src/zexus/module_cache.py +20 -9
- package/src/zexus/object.py +330 -38
- package/src/zexus/parser/parser.py +103 -23
- package/src/zexus/parser/strategy_context.py +318 -6
- package/src/zexus/parser/strategy_structural.py +2 -2
- package/src/zexus/persistence.py +46 -17
- package/src/zexus/security.py +140 -234
- package/src/zexus/type_checker.py +44 -5
- package/src/zexus/vm/binary_bytecode.py +7 -3
- package/src/zexus/vm/bytecode.py +6 -0
- package/src/zexus/vm/cache.py +24 -46
- package/src/zexus/vm/compiler.py +549 -68
- package/src/zexus/vm/memory_pool.py +21 -9
- package/src/zexus/vm/vm.py +609 -95
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +56 -12
- package/src/zexus.egg-info/SOURCES.txt +14 -0
- package/src/zexus.egg-info/entry_points.txt +5 -1
- package/src/zexus.egg-info/requires.txt +26 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Zexus Blockchain — Rust ContractVM Orchestration (Phase 4)
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Moves the contract execution orchestration layer to Rust:
|
|
6
|
+
// • Reentrancy detection
|
|
7
|
+
// • Call-depth enforcement
|
|
8
|
+
// • Gas metering + Rust discount
|
|
9
|
+
// • State snapshot / commit / rollback
|
|
10
|
+
// • Receipt generation
|
|
11
|
+
// • Cross-contract call scaffolding
|
|
12
|
+
//
|
|
13
|
+
// Python passes pre-serialised .zxc bytecode, a state dict, an env
|
|
14
|
+
// dict, and configuration. Rust performs the full execution lifecycle
|
|
15
|
+
// and returns a receipt dict. The only Python involvement after this
|
|
16
|
+
// is final persistence of state to Chain.
|
|
17
|
+
|
|
18
|
+
use pyo3::prelude::*;
|
|
19
|
+
use pyo3::types::{PyBytes, PyDict, PyList};
|
|
20
|
+
use pyo3::ToPyObject;
|
|
21
|
+
use std::collections::{HashMap, HashSet};
|
|
22
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
23
|
+
|
|
24
|
+
use crate::binary_bytecode;
|
|
25
|
+
use crate::rust_vm::{RustVM, RustVMExecutor, ZxValue};
|
|
26
|
+
|
|
27
|
+
// helpers to convert between Python dicts and Rust HashMaps
|
|
28
|
+
fn py_dict_to_hashmap(py: Python<'_>, d: &Bound<'_, PyDict>) -> HashMap<String, ZxValue> {
|
|
29
|
+
let mut m = HashMap::new();
|
|
30
|
+
for (k, v) in d.iter() {
|
|
31
|
+
let key = k.extract::<String>().unwrap_or_default();
|
|
32
|
+
let val = py_to_zx(py, &v.to_object(py));
|
|
33
|
+
m.insert(key, val);
|
|
34
|
+
}
|
|
35
|
+
m
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn hashmap_to_py_dict(py: Python<'_>, m: &HashMap<String, ZxValue>) -> Py<PyDict> {
|
|
39
|
+
let d = PyDict::new_bound(py);
|
|
40
|
+
for (k, v) in m {
|
|
41
|
+
let _ = d.set_item(k, zx_to_py(py, v));
|
|
42
|
+
}
|
|
43
|
+
d.into()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn py_to_zx(py: Python<'_>, obj: &PyObject) -> ZxValue {
|
|
47
|
+
crate::rust_vm::py_to_zx(py, obj)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn zx_to_py(py: Python<'_>, val: &ZxValue) -> PyObject {
|
|
51
|
+
crate::rust_vm::zx_to_py(py, val)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Execution Receipt (Rust-side) ─────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
#[derive(Debug, Clone)]
|
|
57
|
+
struct ContractReceipt {
|
|
58
|
+
success: bool,
|
|
59
|
+
result: ZxValue,
|
|
60
|
+
gas_used: u64,
|
|
61
|
+
gas_limit: u64,
|
|
62
|
+
error: String,
|
|
63
|
+
revert_reason: String,
|
|
64
|
+
state_changes: HashMap<String, ZxValue>,
|
|
65
|
+
instructions_executed: u64,
|
|
66
|
+
output: Vec<String>,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl ContractReceipt {
|
|
70
|
+
fn new_success(
|
|
71
|
+
result: ZxValue,
|
|
72
|
+
gas_used: u64,
|
|
73
|
+
gas_limit: u64,
|
|
74
|
+
state_changes: HashMap<String, ZxValue>,
|
|
75
|
+
instructions_executed: u64,
|
|
76
|
+
output: Vec<String>,
|
|
77
|
+
) -> Self {
|
|
78
|
+
ContractReceipt {
|
|
79
|
+
success: true,
|
|
80
|
+
result,
|
|
81
|
+
gas_used,
|
|
82
|
+
gas_limit,
|
|
83
|
+
error: String::new(),
|
|
84
|
+
revert_reason: String::new(),
|
|
85
|
+
state_changes,
|
|
86
|
+
instructions_executed,
|
|
87
|
+
output,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn new_error(gas_limit: u64, gas_used: u64, error: &str, revert_reason: &str) -> Self {
|
|
92
|
+
ContractReceipt {
|
|
93
|
+
success: false,
|
|
94
|
+
result: ZxValue::Null,
|
|
95
|
+
gas_used,
|
|
96
|
+
gas_limit,
|
|
97
|
+
error: error.to_string(),
|
|
98
|
+
revert_reason: revert_reason.to_string(),
|
|
99
|
+
state_changes: HashMap::new(),
|
|
100
|
+
instructions_executed: 0,
|
|
101
|
+
output: Vec::new(),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn to_py_dict(&self, py: Python<'_>) -> PyObject {
|
|
106
|
+
let d = PyDict::new_bound(py);
|
|
107
|
+
let _ = d.set_item("success", self.success);
|
|
108
|
+
let _ = d.set_item("result", zx_to_py(py, &self.result));
|
|
109
|
+
let _ = d.set_item("gas_used", self.gas_used);
|
|
110
|
+
let _ = d.set_item("gas_limit", self.gas_limit);
|
|
111
|
+
let _ = d.set_item("error", &self.error);
|
|
112
|
+
let _ = d.set_item("revert_reason", &self.revert_reason);
|
|
113
|
+
|
|
114
|
+
let changes = PyDict::new_bound(py);
|
|
115
|
+
for (k, v) in &self.state_changes {
|
|
116
|
+
let _ = changes.set_item(k, zx_to_py(py, v));
|
|
117
|
+
}
|
|
118
|
+
let _ = d.set_item("state_changes", changes);
|
|
119
|
+
let _ = d.set_item("instructions_executed", self.instructions_executed);
|
|
120
|
+
|
|
121
|
+
let output_list = PyList::empty_bound(py);
|
|
122
|
+
for line in &self.output {
|
|
123
|
+
let _ = output_list.append(line);
|
|
124
|
+
}
|
|
125
|
+
let _ = d.set_item("output", output_list);
|
|
126
|
+
|
|
127
|
+
d.to_object(py)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── RustContractVM ────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/// Rust-side contract VM orchestrator.
|
|
134
|
+
///
|
|
135
|
+
/// Handles the full contract execution lifecycle in Rust:
|
|
136
|
+
/// 1. Reentrancy detection
|
|
137
|
+
/// 2. Call-depth enforcement (max 10)
|
|
138
|
+
/// 3. State snapshot + rollback on failure
|
|
139
|
+
/// 4. Rust bytecode VM execution with gas discount
|
|
140
|
+
/// 5. Receipt generation
|
|
141
|
+
/// 6. State diff computation
|
|
142
|
+
///
|
|
143
|
+
/// Python wrapper calls this with pre-serialized data and merges
|
|
144
|
+
/// results back to chain state.
|
|
145
|
+
#[pyclass]
|
|
146
|
+
pub struct RustContractVM {
|
|
147
|
+
/// Gas discount factor (default 0.6 = 40% cheaper in Rust)
|
|
148
|
+
gas_discount: f64,
|
|
149
|
+
/// Default gas limit
|
|
150
|
+
default_gas_limit: u64,
|
|
151
|
+
/// Max call depth for cross-contract calls
|
|
152
|
+
max_call_depth: u32,
|
|
153
|
+
/// Currently executing contracts (reentrancy guard)
|
|
154
|
+
executing: HashSet<String>,
|
|
155
|
+
/// Current call depth
|
|
156
|
+
call_depth: u32,
|
|
157
|
+
/// Execution statistics
|
|
158
|
+
stats: ContractVMStats,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[derive(Debug, Clone, Default)]
|
|
162
|
+
struct ContractVMStats {
|
|
163
|
+
total_executions: u64,
|
|
164
|
+
successful: u64,
|
|
165
|
+
failed: u64,
|
|
166
|
+
total_gas_used: u64,
|
|
167
|
+
total_gas_saved: u64,
|
|
168
|
+
total_instructions: u64,
|
|
169
|
+
reentrancy_blocked: u64,
|
|
170
|
+
depth_exceeded: u64,
|
|
171
|
+
out_of_gas: u64,
|
|
172
|
+
fallback_to_python: u64,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[pymethods]
|
|
176
|
+
impl RustContractVM {
|
|
177
|
+
#[new]
|
|
178
|
+
#[pyo3(signature = (gas_discount=0.6, default_gas_limit=10_000_000, max_call_depth=10))]
|
|
179
|
+
fn new(gas_discount: f64, default_gas_limit: u64, max_call_depth: u32) -> Self {
|
|
180
|
+
RustContractVM {
|
|
181
|
+
gas_discount: gas_discount.clamp(0.01, 1.0),
|
|
182
|
+
default_gas_limit,
|
|
183
|
+
max_call_depth,
|
|
184
|
+
executing: HashSet::new(),
|
|
185
|
+
call_depth: 0,
|
|
186
|
+
stats: ContractVMStats::default(),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Get/set gas discount
|
|
191
|
+
#[getter]
|
|
192
|
+
fn gas_discount(&self) -> f64 {
|
|
193
|
+
self.gas_discount
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[setter]
|
|
197
|
+
fn set_gas_discount(&mut self, discount: f64) {
|
|
198
|
+
self.gas_discount = discount.clamp(0.01, 1.0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Execute a contract action in Rust.
|
|
202
|
+
///
|
|
203
|
+
/// Args:
|
|
204
|
+
/// contract_address: str — address of the contract
|
|
205
|
+
/// action_bytecode: bytes — .zxc serialized bytecode for the action
|
|
206
|
+
/// state: dict — current contract state (snapshot from chain)
|
|
207
|
+
/// env: dict — environment variables (TX, _blockchain_state, etc.)
|
|
208
|
+
/// args: dict — action arguments
|
|
209
|
+
/// gas_limit: int — gas budget (0 = use default)
|
|
210
|
+
/// caller: str — caller address
|
|
211
|
+
///
|
|
212
|
+
/// Returns:
|
|
213
|
+
/// dict with keys: success, result, gas_used, gas_limit, error,
|
|
214
|
+
/// revert_reason, state_changes, instructions_executed,
|
|
215
|
+
/// output, needs_fallback, gas_discount,
|
|
216
|
+
/// gas_saved (amount saved by Rust discount)
|
|
217
|
+
#[pyo3(signature = (contract_address, action_bytecode, state=None, env=None, args=None, gas_limit=0, caller=""))]
|
|
218
|
+
fn execute_contract(
|
|
219
|
+
&mut self,
|
|
220
|
+
py: Python<'_>,
|
|
221
|
+
contract_address: &str,
|
|
222
|
+
action_bytecode: &Bound<'_, PyBytes>,
|
|
223
|
+
state: Option<&Bound<'_, PyDict>>,
|
|
224
|
+
env: Option<&Bound<'_, PyDict>>,
|
|
225
|
+
args: Option<&Bound<'_, PyDict>>,
|
|
226
|
+
gas_limit: u64,
|
|
227
|
+
caller: &str,
|
|
228
|
+
) -> PyResult<PyObject> {
|
|
229
|
+
let gas_limit = if gas_limit > 0 {
|
|
230
|
+
gas_limit
|
|
231
|
+
} else {
|
|
232
|
+
self.default_gas_limit
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
self.stats.total_executions += 1;
|
|
236
|
+
|
|
237
|
+
// ── 1. Reentrancy guard ──
|
|
238
|
+
if self.executing.contains(contract_address) {
|
|
239
|
+
self.stats.reentrancy_blocked += 1;
|
|
240
|
+
self.stats.failed += 1;
|
|
241
|
+
let receipt = ContractReceipt::new_error(
|
|
242
|
+
gas_limit,
|
|
243
|
+
0,
|
|
244
|
+
"ReentrancyGuard",
|
|
245
|
+
&format!("Reentrant call to contract {}", contract_address),
|
|
246
|
+
);
|
|
247
|
+
return Ok(receipt.to_py_dict(py));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── 2. Call-depth guard ──
|
|
251
|
+
if self.call_depth >= self.max_call_depth {
|
|
252
|
+
self.stats.depth_exceeded += 1;
|
|
253
|
+
self.stats.failed += 1;
|
|
254
|
+
let receipt = ContractReceipt::new_error(
|
|
255
|
+
gas_limit,
|
|
256
|
+
0,
|
|
257
|
+
"CallDepthExceeded",
|
|
258
|
+
&format!(
|
|
259
|
+
"Call depth {} exceeds max {}",
|
|
260
|
+
self.call_depth, self.max_call_depth
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
return Ok(receipt.to_py_dict(py));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── 3. State snapshot ──
|
|
267
|
+
let state_snapshot: HashMap<String, ZxValue> = if let Some(py_state) = state {
|
|
268
|
+
py_dict_to_hashmap(py, py_state)
|
|
269
|
+
} else {
|
|
270
|
+
HashMap::new()
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Mark as executing
|
|
274
|
+
self.executing.insert(contract_address.to_string());
|
|
275
|
+
self.call_depth += 1;
|
|
276
|
+
|
|
277
|
+
// ── 4. Deserialize bytecode ──
|
|
278
|
+
let raw = action_bytecode.as_bytes();
|
|
279
|
+
let module = match binary_bytecode::deserialize_zxc(raw, true) {
|
|
280
|
+
Ok(m) => m,
|
|
281
|
+
Err(e) => {
|
|
282
|
+
self.executing.remove(contract_address);
|
|
283
|
+
self.call_depth -= 1;
|
|
284
|
+
self.stats.fallback_to_python += 1;
|
|
285
|
+
// Return needs_fallback so Python can handle it
|
|
286
|
+
let d = PyDict::new_bound(py);
|
|
287
|
+
let _ = d.set_item("needs_fallback", true);
|
|
288
|
+
let _ = d.set_item("error", format!("Deserialization error: {}", e));
|
|
289
|
+
let _ = d.set_item("success", false);
|
|
290
|
+
let _ = d.set_item("gas_used", 0u64);
|
|
291
|
+
let _ = d.set_item("gas_limit", gas_limit);
|
|
292
|
+
return Ok(d.to_object(py));
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// ── 5. Create and configure VM ──
|
|
297
|
+
let mut vm = RustVM::from_module(&module);
|
|
298
|
+
|
|
299
|
+
if gas_limit > 0 {
|
|
300
|
+
vm.set_gas_limit(gas_limit);
|
|
301
|
+
}
|
|
302
|
+
vm.set_gas_discount(self.gas_discount);
|
|
303
|
+
|
|
304
|
+
// Set environment
|
|
305
|
+
if let Some(py_env) = env {
|
|
306
|
+
let rust_env = py_dict_to_hashmap(py, py_env);
|
|
307
|
+
vm.set_env(rust_env);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Set args into environment
|
|
311
|
+
if let Some(py_args) = args {
|
|
312
|
+
for (k, v) in py_args.iter() {
|
|
313
|
+
let key = k.extract::<String>().unwrap_or_default();
|
|
314
|
+
let val = py_to_zx(py, &v.to_object(py));
|
|
315
|
+
vm.env_set(&key, val);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Set blockchain state
|
|
320
|
+
vm.set_blockchain_state(state_snapshot.clone());
|
|
321
|
+
|
|
322
|
+
// Add caller and timestamp to environment
|
|
323
|
+
vm.env_set("_caller", ZxValue::Str(caller.to_string()));
|
|
324
|
+
vm.env_set(
|
|
325
|
+
"_contract_address",
|
|
326
|
+
ZxValue::Str(contract_address.to_string()),
|
|
327
|
+
);
|
|
328
|
+
let ts = SystemTime::now()
|
|
329
|
+
.duration_since(UNIX_EPOCH)
|
|
330
|
+
.map(|d| d.as_secs_f64())
|
|
331
|
+
.unwrap_or(0.0);
|
|
332
|
+
vm.env_set("_timestamp", ZxValue::Float(ts));
|
|
333
|
+
|
|
334
|
+
// ── 6. Execute ──
|
|
335
|
+
let exec_result = vm.execute();
|
|
336
|
+
|
|
337
|
+
// ── 7. Process result ──
|
|
338
|
+
let (instr_count, gas_used, _) = vm.get_stats();
|
|
339
|
+
let gas_full_price = ((gas_used as f64) / self.gas_discount).round() as u64;
|
|
340
|
+
let gas_saved = gas_full_price.saturating_sub(gas_used);
|
|
341
|
+
|
|
342
|
+
// Cleanup guards
|
|
343
|
+
self.executing.remove(contract_address);
|
|
344
|
+
self.call_depth -= 1;
|
|
345
|
+
|
|
346
|
+
match exec_result {
|
|
347
|
+
Ok(result_val) => {
|
|
348
|
+
// Success — compute state diff
|
|
349
|
+
let new_state = vm.get_blockchain_state().clone();
|
|
350
|
+
let state_changes = diff_state(&state_snapshot, &new_state);
|
|
351
|
+
|
|
352
|
+
self.stats.successful += 1;
|
|
353
|
+
self.stats.total_gas_used += gas_used;
|
|
354
|
+
self.stats.total_gas_saved += gas_saved;
|
|
355
|
+
self.stats.total_instructions += instr_count;
|
|
356
|
+
|
|
357
|
+
let receipt = ContractReceipt::new_success(
|
|
358
|
+
result_val,
|
|
359
|
+
gas_used,
|
|
360
|
+
gas_limit,
|
|
361
|
+
state_changes,
|
|
362
|
+
instr_count,
|
|
363
|
+
vm.get_output().to_vec(),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
let d_py = receipt.to_py_dict(py);
|
|
367
|
+
// Add extra fields
|
|
368
|
+
let d = d_py.downcast_bound::<PyDict>(py).unwrap();
|
|
369
|
+
let _ = d.set_item("needs_fallback", false);
|
|
370
|
+
let _ = d.set_item("gas_discount", self.gas_discount);
|
|
371
|
+
let _ = d.set_item("gas_saved", gas_saved);
|
|
372
|
+
|
|
373
|
+
// Return new state for Python to commit
|
|
374
|
+
let new_state_py = PyDict::new_bound(py);
|
|
375
|
+
for (k, v) in vm.get_blockchain_state() {
|
|
376
|
+
let _ = new_state_py.set_item(k, zx_to_py(py, v));
|
|
377
|
+
}
|
|
378
|
+
let _ = d.set_item("new_state", new_state_py);
|
|
379
|
+
|
|
380
|
+
// Return env for syncing back
|
|
381
|
+
let new_env_py = PyDict::new_bound(py);
|
|
382
|
+
for (k, v) in vm.get_env() {
|
|
383
|
+
let _ = new_env_py.set_item(k, zx_to_py(py, v));
|
|
384
|
+
}
|
|
385
|
+
let _ = d.set_item("env", new_env_py);
|
|
386
|
+
|
|
387
|
+
// Phase 6: Return events emitted by Rust builtins
|
|
388
|
+
let events_list = PyList::empty_bound(py);
|
|
389
|
+
for (event_name, event_data) in vm.get_events() {
|
|
390
|
+
let ev = PyDict::new_bound(py);
|
|
391
|
+
let _ = ev.set_item("event", event_name.as_str());
|
|
392
|
+
let _ = ev.set_item("data", zx_to_py(py, event_data));
|
|
393
|
+
let _ = events_list.append(ev);
|
|
394
|
+
}
|
|
395
|
+
let _ = d.set_item("events", events_list);
|
|
396
|
+
|
|
397
|
+
Ok(d_py)
|
|
398
|
+
}
|
|
399
|
+
Err(crate::rust_vm::VmError::OutOfGas {
|
|
400
|
+
used,
|
|
401
|
+
limit,
|
|
402
|
+
opcode,
|
|
403
|
+
}) => {
|
|
404
|
+
// Out of gas — rollback (state_snapshot is not applied)
|
|
405
|
+
self.stats.failed += 1;
|
|
406
|
+
self.stats.out_of_gas += 1;
|
|
407
|
+
self.stats.total_gas_used += gas_limit;
|
|
408
|
+
|
|
409
|
+
let receipt = ContractReceipt::new_error(
|
|
410
|
+
gas_limit,
|
|
411
|
+
gas_limit,
|
|
412
|
+
"OutOfGas",
|
|
413
|
+
&format!("Out of gas: used={}, limit={}, op={}", used, limit, opcode),
|
|
414
|
+
);
|
|
415
|
+
let d_py = receipt.to_py_dict(py);
|
|
416
|
+
let d = d_py.downcast_bound::<PyDict>(py).unwrap();
|
|
417
|
+
let _ = d.set_item("needs_fallback", false);
|
|
418
|
+
let _ = d.set_item("gas_discount", self.gas_discount);
|
|
419
|
+
let _ = d.set_item("gas_saved", 0u64);
|
|
420
|
+
|
|
421
|
+
Ok(d_py)
|
|
422
|
+
}
|
|
423
|
+
Err(crate::rust_vm::VmError::RequireFailed(msg)) => {
|
|
424
|
+
self.stats.failed += 1;
|
|
425
|
+
self.stats.total_gas_used += gas_used;
|
|
426
|
+
|
|
427
|
+
let receipt = ContractReceipt::new_error(
|
|
428
|
+
gas_limit,
|
|
429
|
+
gas_used,
|
|
430
|
+
"RequireFailed",
|
|
431
|
+
&msg,
|
|
432
|
+
);
|
|
433
|
+
let d_py = receipt.to_py_dict(py);
|
|
434
|
+
let d = d_py.downcast_bound::<PyDict>(py).unwrap();
|
|
435
|
+
let _ = d.set_item("needs_fallback", false);
|
|
436
|
+
let _ = d.set_item("gas_discount", self.gas_discount);
|
|
437
|
+
let _ = d.set_item("gas_saved", gas_saved);
|
|
438
|
+
|
|
439
|
+
Ok(d_py)
|
|
440
|
+
}
|
|
441
|
+
Err(crate::rust_vm::VmError::NeedsPythonFallback) => {
|
|
442
|
+
// A Python-only opcode was hit — tell Python to handle it
|
|
443
|
+
self.stats.fallback_to_python += 1;
|
|
444
|
+
|
|
445
|
+
let d = PyDict::new_bound(py);
|
|
446
|
+
let _ = d.set_item("success", false);
|
|
447
|
+
let _ = d.set_item("needs_fallback", true);
|
|
448
|
+
let _ = d.set_item("gas_used", gas_used);
|
|
449
|
+
let _ = d.set_item("gas_limit", gas_limit);
|
|
450
|
+
let _ = d.set_item("error", "NeedsPythonFallback");
|
|
451
|
+
let _ = d.set_item("revert_reason", "");
|
|
452
|
+
let _ = d.set_item("gas_discount", self.gas_discount);
|
|
453
|
+
let _ = d.set_item("gas_saved", 0u64);
|
|
454
|
+
|
|
455
|
+
Ok(d.to_object(py))
|
|
456
|
+
}
|
|
457
|
+
Err(e) => {
|
|
458
|
+
self.stats.failed += 1;
|
|
459
|
+
self.stats.total_gas_used += gas_used;
|
|
460
|
+
|
|
461
|
+
let receipt = ContractReceipt::new_error(
|
|
462
|
+
gas_limit,
|
|
463
|
+
gas_used,
|
|
464
|
+
"RuntimeError",
|
|
465
|
+
&format!("{}", e),
|
|
466
|
+
);
|
|
467
|
+
let d_py = receipt.to_py_dict(py);
|
|
468
|
+
let d = d_py.downcast_bound::<PyDict>(py).unwrap();
|
|
469
|
+
let _ = d.set_item("needs_fallback", false);
|
|
470
|
+
let _ = d.set_item("gas_discount", self.gas_discount);
|
|
471
|
+
let _ = d.set_item("gas_saved", gas_saved);
|
|
472
|
+
|
|
473
|
+
Ok(d_py)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Execute a batch of contract actions in sequence.
|
|
479
|
+
/// Returns a list of receipt dicts.
|
|
480
|
+
#[pyo3(signature = (calls))]
|
|
481
|
+
fn execute_batch(
|
|
482
|
+
&mut self,
|
|
483
|
+
py: Python<'_>,
|
|
484
|
+
calls: &Bound<'_, PyList>,
|
|
485
|
+
) -> PyResult<PyObject> {
|
|
486
|
+
let results = PyList::empty_bound(py);
|
|
487
|
+
|
|
488
|
+
for item in calls.iter() {
|
|
489
|
+
let call_dict = item.downcast::<PyDict>()?;
|
|
490
|
+
|
|
491
|
+
let addr = call_dict
|
|
492
|
+
.get_item("contract_address")?
|
|
493
|
+
.map(|v| v.extract::<String>().unwrap_or_default())
|
|
494
|
+
.unwrap_or_default();
|
|
495
|
+
|
|
496
|
+
let bytecode_obj = call_dict
|
|
497
|
+
.get_item("bytecode")?
|
|
498
|
+
.ok_or_else(|| {
|
|
499
|
+
pyo3::exceptions::PyValueError::new_err("Missing 'bytecode' in call dict")
|
|
500
|
+
})?;
|
|
501
|
+
let bytecode = bytecode_obj.downcast::<PyBytes>()?;
|
|
502
|
+
|
|
503
|
+
// Extract optional dicts — downcast to PyDict directly
|
|
504
|
+
let state_opt: Option<Bound<'_, PyDict>> = call_dict
|
|
505
|
+
.get_item("state")?
|
|
506
|
+
.and_then(|v| v.downcast::<PyDict>().ok().cloned());
|
|
507
|
+
|
|
508
|
+
let env_opt: Option<Bound<'_, PyDict>> = call_dict
|
|
509
|
+
.get_item("env")?
|
|
510
|
+
.and_then(|v| v.downcast::<PyDict>().ok().cloned());
|
|
511
|
+
|
|
512
|
+
let args_opt: Option<Bound<'_, PyDict>> = call_dict
|
|
513
|
+
.get_item("args")?
|
|
514
|
+
.and_then(|v| v.downcast::<PyDict>().ok().cloned());
|
|
515
|
+
|
|
516
|
+
let gas_limit = call_dict
|
|
517
|
+
.get_item("gas_limit")?
|
|
518
|
+
.map(|v| v.extract::<u64>().unwrap_or(0))
|
|
519
|
+
.unwrap_or(0);
|
|
520
|
+
|
|
521
|
+
let caller = call_dict
|
|
522
|
+
.get_item("caller")?
|
|
523
|
+
.map(|v| v.extract::<String>().unwrap_or_default())
|
|
524
|
+
.unwrap_or_default();
|
|
525
|
+
|
|
526
|
+
let result = self.execute_contract(
|
|
527
|
+
py,
|
|
528
|
+
&addr,
|
|
529
|
+
bytecode,
|
|
530
|
+
state_opt.as_ref(),
|
|
531
|
+
env_opt.as_ref(),
|
|
532
|
+
args_opt.as_ref(),
|
|
533
|
+
gas_limit,
|
|
534
|
+
&caller,
|
|
535
|
+
)?;
|
|
536
|
+
results.append(result)?;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
Ok(results.to_object(py))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/// Get execution statistics.
|
|
543
|
+
fn get_stats(&self, py: Python<'_>) -> PyResult<PyObject> {
|
|
544
|
+
let d = PyDict::new_bound(py);
|
|
545
|
+
let _ = d.set_item("total_executions", self.stats.total_executions);
|
|
546
|
+
let _ = d.set_item("successful", self.stats.successful);
|
|
547
|
+
let _ = d.set_item("failed", self.stats.failed);
|
|
548
|
+
let _ = d.set_item("total_gas_used", self.stats.total_gas_used);
|
|
549
|
+
let _ = d.set_item("total_gas_saved", self.stats.total_gas_saved);
|
|
550
|
+
let _ = d.set_item("total_instructions", self.stats.total_instructions);
|
|
551
|
+
let _ = d.set_item("reentrancy_blocked", self.stats.reentrancy_blocked);
|
|
552
|
+
let _ = d.set_item("depth_exceeded", self.stats.depth_exceeded);
|
|
553
|
+
let _ = d.set_item("out_of_gas", self.stats.out_of_gas);
|
|
554
|
+
let _ = d.set_item("fallback_to_python", self.stats.fallback_to_python);
|
|
555
|
+
let _ = d.set_item("gas_discount", self.gas_discount);
|
|
556
|
+
let _ = d.set_item("default_gas_limit", self.default_gas_limit);
|
|
557
|
+
let _ = d.set_item("max_call_depth", self.max_call_depth);
|
|
558
|
+
let _ = d.set_item("current_call_depth", self.call_depth);
|
|
559
|
+
let success_rate = if self.stats.total_executions > 0 {
|
|
560
|
+
self.stats.successful as f64 / self.stats.total_executions as f64 * 100.0
|
|
561
|
+
} else {
|
|
562
|
+
0.0
|
|
563
|
+
};
|
|
564
|
+
let _ = d.set_item("success_rate", success_rate);
|
|
565
|
+
let avg_gas = if self.stats.successful > 0 {
|
|
566
|
+
self.stats.total_gas_used / self.stats.successful
|
|
567
|
+
} else {
|
|
568
|
+
0
|
|
569
|
+
};
|
|
570
|
+
let _ = d.set_item("avg_gas_per_execution", avg_gas);
|
|
571
|
+
|
|
572
|
+
Ok(d.to_object(py))
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/// Reset execution statistics.
|
|
576
|
+
fn reset_stats(&mut self) {
|
|
577
|
+
self.stats = ContractVMStats::default();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// Check if a contract is currently executing (reentrancy check).
|
|
581
|
+
fn is_executing(&self, contract_address: &str) -> bool {
|
|
582
|
+
self.executing.contains(contract_address)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/// Get current call depth.
|
|
586
|
+
#[getter]
|
|
587
|
+
fn call_depth(&self) -> u32 {
|
|
588
|
+
self.call_depth
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/// Get/set max call depth.
|
|
592
|
+
#[getter]
|
|
593
|
+
fn max_call_depth(&self) -> u32 {
|
|
594
|
+
self.max_call_depth
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
#[setter]
|
|
598
|
+
fn set_max_call_depth(&mut self, depth: u32) {
|
|
599
|
+
self.max_call_depth = depth.max(1);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── State diff computation ────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
fn diff_state(
|
|
606
|
+
before: &HashMap<String, ZxValue>,
|
|
607
|
+
after: &HashMap<String, ZxValue>,
|
|
608
|
+
) -> HashMap<String, ZxValue> {
|
|
609
|
+
let mut changes = HashMap::new();
|
|
610
|
+
// Check modified and new keys
|
|
611
|
+
for (k, v) in after {
|
|
612
|
+
match before.get(k) {
|
|
613
|
+
Some(old) => {
|
|
614
|
+
if !zx_values_equal(old, v) {
|
|
615
|
+
changes.insert(k.clone(), v.clone());
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
None => {
|
|
619
|
+
changes.insert(k.clone(), v.clone());
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// Check deleted keys
|
|
624
|
+
for k in before.keys() {
|
|
625
|
+
if !after.contains_key(k) {
|
|
626
|
+
changes.insert(k.clone(), ZxValue::Null);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
changes
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
fn zx_values_equal(a: &ZxValue, b: &ZxValue) -> bool {
|
|
633
|
+
match (a, b) {
|
|
634
|
+
(ZxValue::Null, ZxValue::Null) => true,
|
|
635
|
+
(ZxValue::Bool(x), ZxValue::Bool(y)) => x == y,
|
|
636
|
+
(ZxValue::Int(x), ZxValue::Int(y)) => x == y,
|
|
637
|
+
(ZxValue::Float(x), ZxValue::Float(y)) => (x - y).abs() < f64::EPSILON,
|
|
638
|
+
(ZxValue::Str(x), ZxValue::Str(y)) => x == y,
|
|
639
|
+
(ZxValue::Int(x), ZxValue::Float(y)) => (*x as f64 - y).abs() < f64::EPSILON,
|
|
640
|
+
(ZxValue::Float(x), ZxValue::Int(y)) => (x - *y as f64).abs() < f64::EPSILON,
|
|
641
|
+
_ => false, // Lists/Maps/PyObj: treat as changed for safety
|
|
642
|
+
}
|
|
643
|
+
}
|