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,847 @@
1
+ // ─────────────────────────────────────────────────────────────────────
2
+ // Batch Transaction Executor — Rayon-parallel
3
+ // ─────────────────────────────────────────────────────────────────────
4
+ //
5
+ // Executes a block's worth of transactions, grouping by target contract
6
+ // so that non-overlapping groups run in parallel on all CPU cores.
7
+ //
8
+ // Three execution modes:
9
+ //
10
+ // **GIL-free native** (Phase 5, `execute_batch_native`):
11
+ // Pure-Rust execution — transactions carry pre-compiled .zxc
12
+ // bytecode. Each is executed by a fresh RustVM instance on a
13
+ // Rayon thread. Zero GIL acquisitions during execution.
14
+ // Near-linear CPU scaling.
15
+ //
16
+ // **Batched-GIL** (Phase 0 legacy, `execute_batch`):
17
+ // Acquires the Python GIL *once per contract group* instead of
18
+ // once per transaction. Kept for contracts that need Python
19
+ // fallback (use CALL_NAME, etc.).
20
+ //
21
+ // **Per-tx GIL** (legacy, `execute_batch_pertx`):
22
+ // Acquires the GIL per transaction. Kept for diagnostic use.
23
+
24
+ use pyo3::prelude::*;
25
+ use pyo3::types::{PyBytes, PyDict, PyList};
26
+ use pyo3::ToPyObject;
27
+ use rayon::prelude::*;
28
+ use serde::{Deserialize, Serialize};
29
+ use std::collections::HashMap;
30
+ use std::sync::atomic::{AtomicU64, Ordering};
31
+
32
+ use crate::binary_bytecode;
33
+ use crate::rust_vm::{RustVM, ZxValue, VmError};
34
+
35
+ // ── Result types ──────────────────────────────────────────────────────
36
+
37
+ #[derive(Clone, Debug, Serialize, Deserialize)]
38
+ pub struct TxReceipt {
39
+ pub success: bool,
40
+ pub gas_used: u64,
41
+ pub error: String,
42
+ }
43
+
44
+ #[pyclass]
45
+ #[derive(Clone, Debug)]
46
+ pub struct TxBatchResult {
47
+ #[pyo3(get)]
48
+ pub total: usize,
49
+ #[pyo3(get)]
50
+ pub succeeded: usize,
51
+ #[pyo3(get)]
52
+ pub failed: usize,
53
+ #[pyo3(get)]
54
+ pub gas_used: u64,
55
+ #[pyo3(get)]
56
+ pub elapsed_secs: f64,
57
+ #[pyo3(get)]
58
+ pub receipts: Vec<String>, // JSON-encoded receipts
59
+ }
60
+
61
+ #[pymethods]
62
+ impl TxBatchResult {
63
+ #[getter]
64
+ fn throughput(&self) -> f64 {
65
+ if self.elapsed_secs > 0.0 {
66
+ self.total as f64 / self.elapsed_secs
67
+ } else {
68
+ 0.0
69
+ }
70
+ }
71
+
72
+ fn to_dict(&self) -> HashMap<String, String> {
73
+ let mut m = HashMap::new();
74
+ m.insert("total".into(), self.total.to_string());
75
+ m.insert("succeeded".into(), self.succeeded.to_string());
76
+ m.insert("failed".into(), self.failed.to_string());
77
+ m.insert("gas_used".into(), self.gas_used.to_string());
78
+ m.insert("elapsed".into(), format!("{:.4}", self.elapsed_secs));
79
+ m.insert("throughput".into(), format!("{:.2}", self.throughput()));
80
+ m
81
+ }
82
+
83
+ fn __repr__(&self) -> String {
84
+ format!(
85
+ "TxBatchResult(total={}, ok={}, fail={}, gas={}, {:.1} tx/s)",
86
+ self.total, self.succeeded, self.failed, self.gas_used, self.throughput()
87
+ )
88
+ }
89
+ }
90
+
91
+ // ── Transaction representation ────────────────────────────────────────
92
+
93
+ #[derive(Clone, Debug, Serialize, Deserialize)]
94
+ struct TxInput {
95
+ contract: String,
96
+ action: String,
97
+ args: serde_json::Value,
98
+ caller: String,
99
+ gas_limit: Option<u64>,
100
+ }
101
+
102
+ // ── Native transaction (GIL-free, Phase 5) ────────────────────────────
103
+
104
+ /// A transaction prepared for pure-Rust execution.
105
+ /// Contains pre-compiled .zxc bytecode so no Python interaction is needed.
106
+ #[derive(Clone, Debug)]
107
+ struct NativeTxInput {
108
+ contract_address: String,
109
+ caller: String,
110
+ bytecode: Vec<u8>,
111
+ state: HashMap<String, ZxValue>,
112
+ gas_limit: u64,
113
+ gas_discount: f64,
114
+ /// Position in the original transaction list (for ordering results)
115
+ index: usize,
116
+ }
117
+
118
+ /// Result of a single native transaction execution.
119
+ #[derive(Clone, Debug, Serialize, Deserialize)]
120
+ struct NativeTxReceipt {
121
+ success: bool,
122
+ gas_used: u64,
123
+ gas_saved: u64,
124
+ instructions: u64,
125
+ error: String,
126
+ #[serde(skip)]
127
+ state_changes: HashMap<String, ZxValue>,
128
+ #[serde(skip)]
129
+ new_state: HashMap<String, ZxValue>,
130
+ /// Events emitted during execution (Phase 6).
131
+ #[serde(skip)]
132
+ events: Vec<(String, ZxValue)>,
133
+ needs_fallback: bool,
134
+ index: usize,
135
+ }
136
+
137
+ /// Execute a single contract in pure Rust — no GIL, no Python.
138
+ fn execute_native_tx(input: &NativeTxInput) -> NativeTxReceipt {
139
+ // Deserialize bytecode
140
+ let module = match binary_bytecode::deserialize_zxc(&input.bytecode, true) {
141
+ Ok(m) => m,
142
+ Err(e) => {
143
+ return NativeTxReceipt {
144
+ success: false,
145
+ gas_used: 0,
146
+ gas_saved: 0,
147
+ instructions: 0,
148
+ error: format!("Deserialization: {}", e),
149
+ state_changes: HashMap::new(),
150
+ new_state: HashMap::new(),
151
+ events: Vec::new(),
152
+ needs_fallback: true,
153
+ index: input.index,
154
+ };
155
+ }
156
+ };
157
+
158
+ // Create VM
159
+ let mut vm = RustVM::from_module(&module);
160
+ vm.set_gas_limit(input.gas_limit);
161
+ vm.set_gas_discount(input.gas_discount);
162
+ vm.set_blockchain_state(input.state.clone());
163
+ vm.env_set("_caller", ZxValue::Str(input.caller.clone()));
164
+ vm.env_set(
165
+ "_contract_address",
166
+ ZxValue::Str(input.contract_address.clone()),
167
+ );
168
+
169
+ // Execute
170
+ let result = vm.execute();
171
+ let (instr_count, gas_used, _) = vm.get_stats();
172
+ let gas_full = ((gas_used as f64) / input.gas_discount).round() as u64;
173
+ let gas_saved = gas_full.saturating_sub(gas_used);
174
+ let events = vm.get_events().to_vec();
175
+
176
+ match result {
177
+ Ok(_) => {
178
+ let new_state = vm.get_blockchain_state().clone();
179
+ // Compute state diff
180
+ let mut changes = HashMap::new();
181
+ for (k, v) in &new_state {
182
+ let changed = match input.state.get(k) {
183
+ Some(old) => !zx_eq(old, v),
184
+ None => true,
185
+ };
186
+ if changed {
187
+ changes.insert(k.clone(), v.clone());
188
+ }
189
+ }
190
+ for k in input.state.keys() {
191
+ if !new_state.contains_key(k) {
192
+ changes.insert(k.clone(), ZxValue::Null);
193
+ }
194
+ }
195
+
196
+ NativeTxReceipt {
197
+ success: true,
198
+ gas_used,
199
+ gas_saved,
200
+ instructions: instr_count,
201
+ error: String::new(),
202
+ state_changes: changes,
203
+ new_state,
204
+ events,
205
+ needs_fallback: false,
206
+ index: input.index,
207
+ }
208
+ }
209
+ Err(VmError::NeedsPythonFallback) => NativeTxReceipt {
210
+ success: false,
211
+ gas_used,
212
+ gas_saved: 0,
213
+ instructions: instr_count,
214
+ error: "NeedsPythonFallback".into(),
215
+ state_changes: HashMap::new(),
216
+ new_state: HashMap::new(),
217
+ events,
218
+ needs_fallback: true,
219
+ index: input.index,
220
+ },
221
+ Err(VmError::OutOfGas { used, limit, opcode }) => NativeTxReceipt {
222
+ success: false,
223
+ gas_used: limit,
224
+ gas_saved: 0,
225
+ instructions: instr_count,
226
+ error: format!("OutOfGas: used={}, limit={}, op={}", used, limit, opcode),
227
+ state_changes: HashMap::new(),
228
+ new_state: HashMap::new(),
229
+ events,
230
+ needs_fallback: false,
231
+ index: input.index,
232
+ },
233
+ Err(e) => NativeTxReceipt {
234
+ success: false,
235
+ gas_used,
236
+ gas_saved: 0,
237
+ instructions: instr_count,
238
+ error: format!("{}", e),
239
+ state_changes: HashMap::new(),
240
+ new_state: HashMap::new(),
241
+ events,
242
+ needs_fallback: false,
243
+ index: input.index,
244
+ },
245
+ }
246
+ }
247
+
248
+ /// Quick ZxValue equality check for state diffs (GIL-free).
249
+ fn zx_eq(a: &ZxValue, b: &ZxValue) -> bool {
250
+ match (a, b) {
251
+ (ZxValue::Null, ZxValue::Null) => true,
252
+ (ZxValue::Bool(x), ZxValue::Bool(y)) => x == y,
253
+ (ZxValue::Int(x), ZxValue::Int(y)) => x == y,
254
+ (ZxValue::Float(x), ZxValue::Float(y)) => (x - y).abs() < f64::EPSILON,
255
+ (ZxValue::Str(x), ZxValue::Str(y)) => x == y,
256
+ (ZxValue::Int(x), ZxValue::Float(y)) => (*x as f64 - y).abs() < f64::EPSILON,
257
+ (ZxValue::Float(x), ZxValue::Int(y)) => (x - *y as f64).abs() < f64::EPSILON,
258
+ _ => false,
259
+ }
260
+ }
261
+
262
+ // ── Batch executor ────────────────────────────────────────────────────
263
+
264
+ #[pyclass]
265
+ pub struct RustBatchExecutor {
266
+ max_workers: usize,
267
+ gas_discount: f64,
268
+ default_gas_limit: u64,
269
+ // Accumulated stats across native batches
270
+ native_total: u64,
271
+ native_succeeded: u64,
272
+ native_failed: u64,
273
+ native_fallbacks: u64,
274
+ native_total_gas: u64,
275
+ native_total_saved: u64,
276
+ }
277
+
278
+ #[pymethods]
279
+ impl RustBatchExecutor {
280
+ #[new]
281
+ #[pyo3(signature = (max_workers = 0, gas_discount = 0.6, default_gas_limit = 10_000_000))]
282
+ fn new(max_workers: usize, gas_discount: f64, default_gas_limit: u64) -> Self {
283
+ let workers = if max_workers == 0 {
284
+ rayon::current_num_threads()
285
+ } else {
286
+ max_workers
287
+ };
288
+ // Configure Rayon's global thread pool
289
+ let _ = rayon::ThreadPoolBuilder::new()
290
+ .num_threads(workers)
291
+ .build_global();
292
+ RustBatchExecutor {
293
+ max_workers: workers,
294
+ gas_discount: gas_discount.clamp(0.01, 1.0),
295
+ default_gas_limit,
296
+ native_total: 0,
297
+ native_succeeded: 0,
298
+ native_failed: 0,
299
+ native_fallbacks: 0,
300
+ native_total_gas: 0,
301
+ native_total_saved: 0,
302
+ }
303
+ }
304
+
305
+ /// Execute a batch of transactions — **batched-GIL** mode.
306
+ ///
307
+ /// `transactions` — list of dicts with keys: contract, action, args, caller, gas_limit
308
+ /// `vm_callback` — a Python callable `fn(contract, action, args_json, caller, gas_limit) -> dict`
309
+ /// that executes one transaction via the Zexus ContractVM and returns
310
+ /// `{"success": bool, "gas_used": int, "error": str}`.
311
+ ///
312
+ /// Non-conflicting contract groups are dispatched in parallel via Rayon.
313
+ /// The GIL is acquired **once per group** — all txs in a group execute
314
+ /// inside a single GIL hold, eliminating per-tx GIL contention.
315
+ fn execute_batch(
316
+ &self,
317
+ py: Python<'_>,
318
+ transactions: Vec<HashMap<String, String>>,
319
+ vm_callback: PyObject,
320
+ ) -> PyResult<TxBatchResult> {
321
+ let start = std::time::Instant::now();
322
+ let total = transactions.len();
323
+
324
+ // Parse into TxInput structs
325
+ let txs: Vec<TxInput> = transactions
326
+ .iter()
327
+ .map(|m| TxInput {
328
+ contract: m.get("contract").cloned().unwrap_or_default(),
329
+ action: m.get("action").cloned().unwrap_or_default(),
330
+ args: m
331
+ .get("args")
332
+ .and_then(|s| serde_json::from_str(s).ok())
333
+ .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
334
+ caller: m.get("caller").cloned().unwrap_or_default(),
335
+ gas_limit: m.get("gas_limit").and_then(|s| s.parse().ok()),
336
+ })
337
+ .collect();
338
+
339
+ // Group by contract
340
+ let mut groups: HashMap<String, Vec<TxInput>> = HashMap::new();
341
+ for tx in txs {
342
+ groups
343
+ .entry(tx.contract.clone())
344
+ .or_default()
345
+ .push(tx);
346
+ }
347
+
348
+ // Execute — parallel across contract groups.
349
+ // BATCHED-GIL: acquire the GIL once per group, execute all txs
350
+ // in that group inside the single hold, then release.
351
+ let groups_vec: Vec<(String, Vec<TxInput>)> = groups.into_iter().collect();
352
+
353
+ let all_receipts: Vec<TxReceipt> = py.allow_threads(|| {
354
+ groups_vec
355
+ .par_iter()
356
+ .flat_map(|(contract_addr, txs)| {
357
+ // ONE GIL acquisition for the entire group
358
+ Python::with_gil(|py| {
359
+ let callback = vm_callback.bind(py);
360
+ let mut group_receipts = Vec::with_capacity(txs.len());
361
+
362
+ for tx in txs {
363
+ let args_json =
364
+ serde_json::to_string(&tx.args).unwrap_or_default();
365
+ let gas = tx.gas_limit.unwrap_or(0);
366
+
367
+ let call_result = callback.call1((
368
+ contract_addr.as_str(),
369
+ tx.action.as_str(),
370
+ args_json.as_str(),
371
+ tx.caller.as_str(),
372
+ gas,
373
+ ));
374
+
375
+ let receipt = match call_result {
376
+ Ok(result) => {
377
+ let success: bool = result
378
+ .get_item("success")
379
+ .and_then(|v| v.extract())
380
+ .unwrap_or(false);
381
+ let gas_used: u64 = result
382
+ .get_item("gas_used")
383
+ .and_then(|v| v.extract())
384
+ .unwrap_or(0);
385
+ let error: String = result
386
+ .get_item("error")
387
+ .and_then(|v| v.extract())
388
+ .unwrap_or_default();
389
+ TxReceipt { success, gas_used, error }
390
+ }
391
+ Err(e) => TxReceipt {
392
+ success: false,
393
+ gas_used: 0,
394
+ error: format!("Rust->Python callback error: {}", e),
395
+ },
396
+ };
397
+ group_receipts.push(receipt);
398
+ }
399
+ group_receipts
400
+ })
401
+ })
402
+ .collect()
403
+ });
404
+
405
+ // Aggregate
406
+ let mut succeeded = 0usize;
407
+ let mut failed = 0usize;
408
+ let mut gas_total = 0u64;
409
+ let mut receipt_jsons = Vec::with_capacity(all_receipts.len());
410
+
411
+ for r in &all_receipts {
412
+ if r.success {
413
+ succeeded += 1;
414
+ } else {
415
+ failed += 1;
416
+ }
417
+ gas_total += r.gas_used;
418
+ receipt_jsons.push(serde_json::to_string(r).unwrap_or_default());
419
+ }
420
+
421
+ Ok(TxBatchResult {
422
+ total,
423
+ succeeded,
424
+ failed,
425
+ gas_used: gas_total,
426
+ elapsed_secs: start.elapsed().as_secs_f64(),
427
+ receipts: receipt_jsons,
428
+ })
429
+ }
430
+
431
+ /// Legacy per-tx GIL mode — acquires GIL for every single transaction.
432
+ /// Kept for diagnostic/comparison benchmarks.
433
+ fn execute_batch_pertx(
434
+ &self,
435
+ py: Python<'_>,
436
+ transactions: Vec<HashMap<String, String>>,
437
+ vm_callback: PyObject,
438
+ ) -> PyResult<TxBatchResult> {
439
+ let start = std::time::Instant::now();
440
+ let total = transactions.len();
441
+
442
+ let txs: Vec<TxInput> = transactions
443
+ .iter()
444
+ .map(|m| TxInput {
445
+ contract: m.get("contract").cloned().unwrap_or_default(),
446
+ action: m.get("action").cloned().unwrap_or_default(),
447
+ args: m
448
+ .get("args")
449
+ .and_then(|s| serde_json::from_str(s).ok())
450
+ .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
451
+ caller: m.get("caller").cloned().unwrap_or_default(),
452
+ gas_limit: m.get("gas_limit").and_then(|s| s.parse().ok()),
453
+ })
454
+ .collect();
455
+
456
+ let mut groups: HashMap<String, Vec<TxInput>> = HashMap::new();
457
+ for tx in txs {
458
+ groups.entry(tx.contract.clone()).or_default().push(tx);
459
+ }
460
+
461
+ let groups_vec: Vec<(String, Vec<TxInput>)> = groups.into_iter().collect();
462
+
463
+ let all_receipts: Vec<TxReceipt> = py.allow_threads(|| {
464
+ groups_vec
465
+ .par_iter()
466
+ .flat_map(|(contract_addr, txs)| {
467
+ let mut group_receipts = Vec::with_capacity(txs.len());
468
+ for tx in txs {
469
+ // Per-tx GIL acquisition (legacy)
470
+ let receipt = Python::with_gil(|py| {
471
+ let args_json =
472
+ serde_json::to_string(&tx.args).unwrap_or_default();
473
+ let gas = tx.gas_limit.unwrap_or(0);
474
+ let callback = vm_callback.bind(py);
475
+ let call_result = callback.call1((
476
+ contract_addr.as_str(),
477
+ tx.action.as_str(),
478
+ args_json.as_str(),
479
+ tx.caller.as_str(),
480
+ gas,
481
+ ));
482
+ match call_result {
483
+ Ok(result) => {
484
+ let success: bool = result
485
+ .get_item("success")
486
+ .and_then(|v| v.extract())
487
+ .unwrap_or(false);
488
+ let gas_used: u64 = result
489
+ .get_item("gas_used")
490
+ .and_then(|v| v.extract())
491
+ .unwrap_or(0);
492
+ let error: String = result
493
+ .get_item("error")
494
+ .and_then(|v| v.extract())
495
+ .unwrap_or_default();
496
+ TxReceipt { success, gas_used, error }
497
+ }
498
+ Err(e) => TxReceipt {
499
+ success: false,
500
+ gas_used: 0,
501
+ error: format!("Rust->Python callback error: {}", e),
502
+ },
503
+ }
504
+ });
505
+ group_receipts.push(receipt);
506
+ }
507
+ group_receipts
508
+ })
509
+ .collect()
510
+ });
511
+
512
+ let mut succeeded = 0usize;
513
+ let mut failed = 0usize;
514
+ let mut gas_total = 0u64;
515
+ let mut receipt_jsons = Vec::with_capacity(all_receipts.len());
516
+ for r in &all_receipts {
517
+ if r.success { succeeded += 1; } else { failed += 1; }
518
+ gas_total += r.gas_used;
519
+ receipt_jsons.push(serde_json::to_string(r).unwrap_or_default());
520
+ }
521
+
522
+ Ok(TxBatchResult {
523
+ total, succeeded, failed,
524
+ gas_used: gas_total,
525
+ elapsed_secs: start.elapsed().as_secs_f64(),
526
+ receipts: receipt_jsons,
527
+ })
528
+ }
529
+
530
+ /// Pure-Rust parallel hash batch — no Python callback needed.
531
+ /// Useful for computing tx hashes or block hashes in bulk.
532
+ fn hash_batch(&self, py: Python<'_>, data: Vec<Vec<u8>>) -> Vec<String> {
533
+ use sha2::{Digest, Sha256};
534
+ py.allow_threads(|| {
535
+ data.par_iter()
536
+ .map(|d| {
537
+ let hash = Sha256::digest(d);
538
+ hex::encode(hash)
539
+ })
540
+ .collect()
541
+ })
542
+ }
543
+
544
+ // ── Phase 5: GIL-free native batch execution ──────────────────
545
+
546
+ /// Execute a batch of pre-compiled transactions entirely in Rust.
547
+ ///
548
+ /// **Zero GIL acquisitions during execution.**
549
+ ///
550
+ /// Each transaction dict must contain:
551
+ /// - `bytecode`: bytes — .zxc serialized bytecode
552
+ /// - `contract_address`: str
553
+ /// - `caller`: str
554
+ /// - `gas_limit`: int (optional, defaults to executor's default)
555
+ ///
556
+ /// Optional per-transaction overrides:
557
+ /// - `state`: dict — contract state snapshot
558
+ /// - `gas_discount`: float — per-tx discount override
559
+ ///
560
+ /// Non-conflicting contract groups run in parallel via Rayon.
561
+ /// Returns a `TxBatchResult` with JSON receipts plus a `native_stats` dict.
562
+ fn execute_batch_native(
563
+ &mut self,
564
+ py: Python<'_>,
565
+ transactions: &Bound<'_, PyList>,
566
+ ) -> PyResult<PyObject> {
567
+ let start = std::time::Instant::now();
568
+ let total = transactions.len();
569
+
570
+ // ── Parse into NativeTxInputs (requires GIL — one-time) ──
571
+ let mut inputs: Vec<NativeTxInput> = Vec::with_capacity(total);
572
+ for (i, item) in transactions.iter().enumerate() {
573
+ let d: &Bound<'_, PyDict> = item.downcast::<PyDict>()?;
574
+
575
+ let bc_obj = d.get_item("bytecode")?
576
+ .ok_or_else(|| pyo3::exceptions::PyValueError::new_err(
577
+ format!("Transaction {} missing 'bytecode'", i),
578
+ ))?;
579
+ let bc_bytes: &Bound<'_, PyBytes> = bc_obj.downcast::<PyBytes>()?;
580
+ let bytecode = bc_bytes.as_bytes().to_vec();
581
+
582
+ let contract_address: String = d.get_item("contract_address")?
583
+ .map(|v| v.extract::<String>().unwrap_or_default())
584
+ .unwrap_or_default();
585
+
586
+ let caller: String = d.get_item("caller")?
587
+ .map(|v| v.extract::<String>().unwrap_or_default())
588
+ .unwrap_or_default();
589
+
590
+ let gas_limit: u64 = d.get_item("gas_limit")?
591
+ .map(|v| v.extract::<u64>().unwrap_or(self.default_gas_limit))
592
+ .unwrap_or(self.default_gas_limit);
593
+
594
+ let gas_discount: f64 = d.get_item("gas_discount")?
595
+ .map(|v| v.extract::<f64>().unwrap_or(self.gas_discount))
596
+ .unwrap_or(self.gas_discount);
597
+
598
+ // Per-transaction state (optional)
599
+ let state: HashMap<String, ZxValue> = match d.get_item("state")? {
600
+ Some(v) => {
601
+ if let Ok(sd) = v.downcast::<PyDict>() {
602
+ let mut m: HashMap<String, ZxValue> = HashMap::new();
603
+ for (k, val) in sd.iter() {
604
+ let key: String = k.extract::<String>().unwrap_or_default();
605
+ let obj: PyObject = val.to_object(py);
606
+ let zv = crate::rust_vm::py_to_zx(py, &obj);
607
+ m.insert(key, zv);
608
+ }
609
+ m
610
+ } else {
611
+ HashMap::new()
612
+ }
613
+ }
614
+ None => HashMap::new(),
615
+ };
616
+
617
+ inputs.push(NativeTxInput {
618
+ contract_address,
619
+ caller,
620
+ bytecode,
621
+ state,
622
+ gas_limit,
623
+ gas_discount,
624
+ index: i,
625
+ });
626
+ }
627
+
628
+ // ── Group by contract address for conflict-free parallelism ──
629
+ let mut groups: HashMap<String, Vec<NativeTxInput>> = HashMap::new();
630
+ for tx in inputs {
631
+ groups.entry(tx.contract_address.clone()).or_default().push(tx);
632
+ }
633
+ let groups_vec: Vec<(String, Vec<NativeTxInput>)> = groups.into_iter().collect();
634
+
635
+ // ── Execute in parallel — ZERO GIL ──
636
+ let gas_counter = AtomicU64::new(0);
637
+ let saved_counter = AtomicU64::new(0);
638
+
639
+ let mut all_receipts: Vec<NativeTxReceipt> = py.allow_threads(|| {
640
+ let results: Vec<NativeTxReceipt> = groups_vec
641
+ .par_iter()
642
+ .flat_map(|(_addr, txs)| {
643
+ // Within a contract group, execute sequentially
644
+ // (state ordering matters for same-contract txs)
645
+ let mut group_results = Vec::with_capacity(txs.len());
646
+ let mut running_state: Option<HashMap<String, ZxValue>> = None;
647
+
648
+ for tx in txs {
649
+ // Chain state: if a prior tx in this group modified state,
650
+ // use the updated state for subsequent txs
651
+ let mut input = tx.clone();
652
+ if let Some(ref s) = running_state {
653
+ // Merge running state into tx state
654
+ for (k, v) in s {
655
+ input.state.insert(k.clone(), v.clone());
656
+ }
657
+ }
658
+
659
+ let receipt = execute_native_tx(&input);
660
+
661
+ if receipt.success {
662
+ // Update running state for next tx in group
663
+ let mut new_state = input.state.clone();
664
+ for (k, v) in &receipt.state_changes {
665
+ new_state.insert(k.clone(), v.clone());
666
+ }
667
+ running_state = Some(new_state);
668
+ }
669
+
670
+ gas_counter.fetch_add(receipt.gas_used, Ordering::Relaxed);
671
+ saved_counter.fetch_add(receipt.gas_saved, Ordering::Relaxed);
672
+ group_results.push(receipt);
673
+ }
674
+ group_results
675
+ })
676
+ .collect();
677
+ results
678
+ });
679
+
680
+ // ── Sort by original index to preserve ordering ──
681
+ all_receipts.sort_by_key(|r| r.index);
682
+
683
+ // ── Aggregate results ──
684
+ let elapsed = start.elapsed().as_secs_f64();
685
+ let mut succeeded = 0usize;
686
+ let mut failed = 0usize;
687
+ let mut fallbacks = 0usize;
688
+ let gas_total = gas_counter.load(Ordering::Relaxed);
689
+ let gas_saved_total = saved_counter.load(Ordering::Relaxed);
690
+ let mut receipt_jsons = Vec::with_capacity(all_receipts.len());
691
+
692
+ for r in &all_receipts {
693
+ if r.success {
694
+ succeeded += 1;
695
+ } else {
696
+ failed += 1;
697
+ if r.needs_fallback {
698
+ fallbacks += 1;
699
+ }
700
+ }
701
+ // JSON receipt (lightweight, for compatibility with TxBatchResult)
702
+ let json_str = serde_json::to_string(&serde_json::json!({
703
+ "success": r.success,
704
+ "gas_used": r.gas_used,
705
+ "gas_saved": r.gas_saved,
706
+ "instructions": r.instructions,
707
+ "error": r.error,
708
+ "needs_fallback": r.needs_fallback,
709
+ }))
710
+ .unwrap_or_default();
711
+ receipt_jsons.push(json_str);
712
+ }
713
+
714
+ // Update cumulative stats
715
+ self.native_total += total as u64;
716
+ self.native_succeeded += succeeded as u64;
717
+ self.native_failed += failed as u64;
718
+ self.native_fallbacks += fallbacks as u64;
719
+ self.native_total_gas += gas_total;
720
+ self.native_total_saved += gas_saved_total;
721
+
722
+ // Build result dict with extra native stats
723
+ let result = TxBatchResult {
724
+ total,
725
+ succeeded,
726
+ failed,
727
+ gas_used: gas_total,
728
+ elapsed_secs: elapsed,
729
+ receipts: receipt_jsons,
730
+ };
731
+
732
+ let d = PyDict::new_bound(py);
733
+ let _ = d.set_item("total", result.total);
734
+ let _ = d.set_item("succeeded", result.succeeded);
735
+ let _ = d.set_item("failed", result.failed);
736
+ let _ = d.set_item("gas_used", result.gas_used);
737
+ let _ = d.set_item("elapsed_secs", result.elapsed_secs);
738
+ let _ = d.set_item("throughput", result.throughput());
739
+ let _ = d.set_item("gas_saved", gas_saved_total);
740
+ let _ = d.set_item("fallbacks", fallbacks);
741
+ let _ = d.set_item("gil_acquisitions", 0u64);
742
+ let _ = d.set_item("mode", "native_gil_free");
743
+ let _ = d.set_item("workers", self.max_workers);
744
+ let _ = d.set_item("gas_discount", self.gas_discount);
745
+
746
+ // Receipts as Python list
747
+ let receipt_list = PyList::empty_bound(py);
748
+ for r_json in &result.receipts {
749
+ let _ = receipt_list.append(r_json);
750
+ }
751
+ let _ = d.set_item("receipts", receipt_list);
752
+
753
+ // State changes per contract (for chain commit)
754
+ let changes_dict = PyDict::new_bound(py);
755
+ for r in &all_receipts {
756
+ if r.success && !r.state_changes.is_empty() {
757
+ // Find contract address from the receipt's index
758
+ let addr = &inputs_addrs(total, &groups_vec, r.index);
759
+ let sc = PyDict::new_bound(py);
760
+ for (k, v) in &r.state_changes {
761
+ let _ = sc.set_item(k, crate::rust_vm::zx_to_py(py, v));
762
+ }
763
+ let _ = changes_dict.set_item(addr.as_str(), sc);
764
+ }
765
+ }
766
+ let _ = d.set_item("state_changes", changes_dict);
767
+
768
+ // Events collected across all receipts (Phase 6)
769
+ let events_list = PyList::empty_bound(py);
770
+ for r in &all_receipts {
771
+ for (event_name, event_data) in &r.events {
772
+ let ev = PyDict::new_bound(py);
773
+ let _ = ev.set_item("event", event_name.as_str());
774
+ let _ = ev.set_item("data", crate::rust_vm::zx_to_py(py, event_data));
775
+ let _ = ev.set_item("index", r.index);
776
+ let _ = events_list.append(ev);
777
+ }
778
+ }
779
+ let _ = d.set_item("events", events_list);
780
+
781
+ Ok(d.to_object(py))
782
+ }
783
+
784
+ /// Get/set gas discount for native batch execution.
785
+ #[getter]
786
+ fn gas_discount(&self) -> f64 {
787
+ self.gas_discount
788
+ }
789
+
790
+ #[setter]
791
+ fn set_gas_discount(&mut self, discount: f64) {
792
+ self.gas_discount = discount.clamp(0.01, 1.0);
793
+ }
794
+
795
+ /// Get cumulative native execution stats.
796
+ fn get_native_stats(&self, py: Python<'_>) -> PyResult<PyObject> {
797
+ let d = PyDict::new_bound(py);
798
+ let _ = d.set_item("total", self.native_total);
799
+ let _ = d.set_item("succeeded", self.native_succeeded);
800
+ let _ = d.set_item("failed", self.native_failed);
801
+ let _ = d.set_item("fallbacks", self.native_fallbacks);
802
+ let _ = d.set_item("total_gas", self.native_total_gas);
803
+ let _ = d.set_item("total_gas_saved", self.native_total_saved);
804
+ let tps = if self.native_total > 0 {
805
+ self.native_total as f64 // Not meaningful here, tracked per-batch
806
+ } else {
807
+ 0.0
808
+ };
809
+ let _ = d.set_item("gil_acquisitions", 0u64);
810
+ let _ = d.set_item("mode", "native_gil_free");
811
+ Ok(d.to_object(py))
812
+ }
813
+
814
+ /// Reset native stats.
815
+ fn reset_native_stats(&mut self) {
816
+ self.native_total = 0;
817
+ self.native_succeeded = 0;
818
+ self.native_failed = 0;
819
+ self.native_fallbacks = 0;
820
+ self.native_total_gas = 0;
821
+ self.native_total_saved = 0;
822
+ }
823
+
824
+ fn __repr__(&self) -> String {
825
+ format!(
826
+ "RustBatchExecutor(workers={}, discount={:.2}, native_txs={})",
827
+ self.max_workers, self.gas_discount, self.native_total,
828
+ )
829
+ }
830
+ }
831
+
832
+ // ── Helper: recover contract address from flattened groups by index ──
833
+
834
+ fn inputs_addrs(
835
+ _total: usize,
836
+ groups: &[(String, Vec<NativeTxInput>)],
837
+ index: usize,
838
+ ) -> String {
839
+ for (addr, txs) in groups {
840
+ for tx in txs {
841
+ if tx.index == index {
842
+ return addr.clone();
843
+ }
844
+ }
845
+ }
846
+ String::new()
847
+ }