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.
- package/README.md +57 -6
- 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 +34 -2
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/blockchain/accelerator.py +27 -0
- package/src/zexus/blockchain/contract_vm.py +409 -3
- package/src/zexus/blockchain/rust_bridge.py +64 -0
- 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 +69 -14
- package/src/zexus/parser/strategy_context.py +228 -5
- 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 +80 -20
- package/src/zexus/vm/fastops.c +1093 -2975
- package/src/zexus/vm/gas_metering.py +2 -2
- package/src/zexus/vm/memory_pool.py +21 -9
- package/src/zexus/vm/vm.py +527 -67
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus.egg-info/PKG-INFO +79 -12
- package/src/zexus.egg-info/SOURCES.txt +23 -1
- package/src/zexus.egg-info/requires.txt +26 -0
- 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
|
+
}
|