zexus 1.7.2 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +57 -6
  2. package/package.json +2 -1
  3. package/rust_core/Cargo.lock +603 -0
  4. package/rust_core/Cargo.toml +26 -0
  5. package/rust_core/README.md +15 -0
  6. package/rust_core/pyproject.toml +25 -0
  7. package/rust_core/src/binary_bytecode.rs +543 -0
  8. package/rust_core/src/contract_vm.rs +643 -0
  9. package/rust_core/src/executor.rs +847 -0
  10. package/rust_core/src/hasher.rs +90 -0
  11. package/rust_core/src/lib.rs +71 -0
  12. package/rust_core/src/merkle.rs +128 -0
  13. package/rust_core/src/rust_vm.rs +2313 -0
  14. package/rust_core/src/signature.rs +79 -0
  15. package/rust_core/src/state_adapter.rs +281 -0
  16. package/rust_core/src/validator.rs +116 -0
  17. package/scripts/postinstall.js +34 -2
  18. package/src/zexus/__init__.py +1 -1
  19. package/src/zexus/blockchain/accelerator.py +27 -0
  20. package/src/zexus/blockchain/contract_vm.py +409 -3
  21. package/src/zexus/blockchain/rust_bridge.py +64 -0
  22. package/src/zexus/cli/main.py +1 -1
  23. package/src/zexus/cli/zpm.py +1 -1
  24. package/src/zexus/evaluator/bytecode_compiler.py +150 -52
  25. package/src/zexus/evaluator/core.py +151 -809
  26. package/src/zexus/evaluator/expressions.py +27 -22
  27. package/src/zexus/evaluator/functions.py +171 -126
  28. package/src/zexus/evaluator/statements.py +55 -112
  29. package/src/zexus/module_cache.py +20 -9
  30. package/src/zexus/object.py +330 -38
  31. package/src/zexus/parser/parser.py +69 -14
  32. package/src/zexus/parser/strategy_context.py +228 -5
  33. package/src/zexus/parser/strategy_structural.py +2 -2
  34. package/src/zexus/persistence.py +46 -17
  35. package/src/zexus/security.py +140 -234
  36. package/src/zexus/type_checker.py +44 -5
  37. package/src/zexus/vm/binary_bytecode.py +7 -3
  38. package/src/zexus/vm/bytecode.py +6 -0
  39. package/src/zexus/vm/cache.py +24 -46
  40. package/src/zexus/vm/compiler.py +80 -20
  41. package/src/zexus/vm/fastops.c +1093 -2975
  42. package/src/zexus/vm/gas_metering.py +2 -2
  43. package/src/zexus/vm/memory_pool.py +21 -9
  44. package/src/zexus/vm/vm.py +527 -67
  45. package/src/zexus/zpm/package_manager.py +1 -1
  46. package/src/zexus.egg-info/PKG-INFO +79 -12
  47. package/src/zexus.egg-info/SOURCES.txt +23 -1
  48. package/src/zexus.egg-info/requires.txt +26 -0
  49. package/src/zexus.egg-info/entry_points.txt +0 -4
@@ -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
+ }