1#![cfg(all(target_os = "linux", target_arch = "x86_64"))]
26
27use criterion::{BenchmarkId, Criterion, Throughput};
28use javm_cap::NUM_REGS;
29use javm_cap::image::{Image, PinnedCap};
30use javm_cap::slot::Key;
31use javm_cap::{Cap, CapHash};
32use nub::{HyperlightNubGuard, InvocationResult, Nub, SCRATCHPAD_HEAD_LEN};
33
34const EXIT_HOSTCALL: u32 = 4;
38
39pub const INITIAL_GAS: u64 = 100_000_000_000;
41
42pub struct BuiltCaps {
49 pub data_caps: Vec<(CapHash, Cap)>,
52 pub image_cap: Cap,
54 pub image_hash: CapHash,
55 pub cnode_cap: Cap,
57 pub cnode_hash: CapHash,
58 pub instance_cap: Cap,
60 pub instance_hash: CapHash,
61 pub endpoint_idx: u8,
62}
63
64impl BuiltCaps {
65 pub fn for_image(image: &Image, endpoint_idx: u8) -> Self {
68 let endpoint = image
69 .endpoints
70 .get(&Key::from(endpoint_idx))
71 .unwrap_or_else(|| panic!("endpoint {endpoint_idx} not declared"));
72
73 let mut data_caps: Vec<(CapHash, Cap)> = Vec::new();
76 let mut pinned_hashes: Vec<(Key, CapHash)> = Vec::new();
77 let mut initial_hashes: Vec<(Key, CapHash)> = Vec::new();
78
79 for (slot, pinned) in &image.pinned_slots {
80 let (h, cap) = match pinned {
81 PinnedCap::Data { desc } => {
82 let cap = Cap::data_from_desc(&image.arena, desc);
83 let h = ssz::hash_tree_root(&cap);
84 (h, Some(cap))
85 }
86 PinnedCap::Image { content_hash } => {
87 (*content_hash, None)
90 }
91 };
92 pinned_hashes.push((slot.clone(), h));
93 if let Some(c) = cap {
94 data_caps.push((h, c));
95 }
96 }
97 for (slot, desc) in &image.initial_slots {
98 let cap = Cap::data_from_desc(&image.arena, desc);
99 let h = ssz::hash_tree_root(&cap);
100 initial_hashes.push((slot.clone(), h));
101 data_caps.push((h, cap));
102 }
103
104 let image_cap = Cap::image_with_slots(image, &pinned_hashes, &initial_hashes)
106 .expect("image_with_slots");
107 let image_hash = ssz::hash_tree_root(&image_cap);
108
109 let cnode_cap = Cap::empty_cnode();
111 let cnode_hash = ssz::hash_tree_root(&cnode_cap);
112
113 let mem = image.instance_mem_backing();
115
116 let mut regs = [0u64; NUM_REGS];
117 for (&i, &v) in &endpoint.initial_regs {
118 if let Some(slot) = regs.get_mut(i as usize) {
119 *slot = v;
120 }
121 }
122
123 let instance_cap =
124 Cap::instance_with_mem([0u8; 32], image_hash, cnode_hash, mem, regs, 0, 0);
125 let instance_hash = ssz::hash_tree_root(&instance_cap);
126
127 BuiltCaps {
128 data_caps,
129 image_cap,
130 image_hash,
131 cnode_cap,
132 cnode_hash,
133 instance_cap,
134 instance_hash,
135 endpoint_idx,
136 }
137 }
138
139 pub fn put_into(&self, nub: &Nub) {
142 for (h, cap) in &self.data_caps {
143 nub.put_cap_with_hash(*h, cap)
144 .unwrap_or_else(|e| panic!("put_cap_with_hash data: {e}"));
145 }
146 nub.put_cap_with_hash(self.image_hash, &self.image_cap)
147 .unwrap_or_else(|e| panic!("put_cap_with_hash image: {e}"));
148 nub.put_cap_with_hash(self.cnode_hash, &self.cnode_cap)
149 .unwrap_or_else(|e| panic!("put_cap_with_hash cnode: {e}"));
150 nub.put_cap_with_hash(self.instance_hash, &self.instance_cap)
151 .unwrap_or_else(|e| panic!("put_cap_with_hash instance: {e}"));
152 }
153}
154
155pub type NubGuard = HyperlightNubGuard;
157
158pub fn nub_hyperlight_lock() -> NubGuard {
162 Nub::hyperlight().expect("Hyperlight sandbox")
163}
164
165pub fn invoke(nub: &Nub, built: &BuiltCaps) -> (u64, u64) {
170 let result = nub
171 .invoke_cached(built.instance_hash, built.endpoint_idx, [0; 4], INITIAL_GAS)
172 .unwrap_or_else(|e| panic!("invoke_cached: {e}"));
173 finish(&result)
174}
175
176pub fn run_interpreter(built: &BuiltCaps) -> (u64, u64) {
181 let nub = Nub::new_local();
182 built.put_into(&nub);
183 let result = nub
184 .invoke_cached(built.instance_hash, built.endpoint_idx, [0; 4], INITIAL_GAS)
185 .unwrap_or_else(|e| panic!("interpreter invoke_cached: {e}"));
186 finish(&result)
187}
188
189pub fn run_recompiler(built: &BuiltCaps) -> (u64, u64) {
194 let nub = nub_hyperlight_lock();
195 built.put_into(&nub);
196 let result = nub
197 .invoke_cached(built.instance_hash, built.endpoint_idx, [0; 4], INITIAL_GAS)
198 .unwrap_or_else(|e| panic!("recompiler invoke_cached: {e}"));
199 finish(&result)
200}
201
202fn finish(result: &InvocationResult) -> (u64, u64) {
203 assert_eq!(
204 result.exit_reason, EXIT_HOSTCALL,
205 "unexpected exit_reason {} (exit_arg={})",
206 result.exit_reason, result.exit_arg,
207 );
208 assert_eq!(
209 result.exit_arg, 0,
210 "expected HostCall(0) trampoline halt, got HostCall({})",
211 result.exit_arg,
212 );
213 let gas_used = INITIAL_GAS.saturating_sub(result.gas_remaining);
214 (result.return_value, gas_used)
215}
216
217pub const ABORT_SENTINEL: u32 = u32::MAX;
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub struct RawRun {
241 pub exit_reason: u32,
242 pub exit_arg: u32,
243 pub return_value: u64,
244 pub gas_used: u64,
245 pub scratchpad_head: [u8; SCRATCHPAD_HEAD_LEN],
246}
247
248impl RawRun {
249 fn from_result(r: &InvocationResult) -> Self {
250 RawRun {
251 exit_reason: r.exit_reason,
252 exit_arg: r.exit_arg,
253 return_value: r.return_value,
254 gas_used: INITIAL_GAS.saturating_sub(r.gas_remaining),
255 scratchpad_head: r.scratchpad_head,
256 }
257 }
258
259 fn aborted() -> Self {
260 RawRun {
261 exit_reason: ABORT_SENTINEL,
262 exit_arg: 0,
263 return_value: 0,
264 gas_used: 0,
265 scratchpad_head: [0u8; SCRATCHPAD_HEAD_LEN],
266 }
267 }
268}
269
270pub fn run_interpreter_raw(built: &BuiltCaps) -> RawRun {
275 let nub = Nub::new_local();
276 built.put_into(&nub);
277 match nub.invoke_cached(built.instance_hash, built.endpoint_idx, [0; 4], INITIAL_GAS) {
278 Ok(r) => RawRun::from_result(&r),
279 Err(_) => RawRun::aborted(),
280 }
281}
282
283pub fn run_recompiler_raw(built: &BuiltCaps) -> RawRun {
292 let nub = nub_hyperlight_lock();
293 built.put_into(&nub);
294 match nub.invoke_cached(built.instance_hash, built.endpoint_idx, [0; 4], INITIAL_GAS) {
295 Ok(r) => RawRun::from_result(&r),
296 Err(_) => RawRun::aborted(),
297 }
298}
299
300const SLOT_IMAGE_RECURSE: u32 = 3;
311
312pub struct SubVmTop {
314 pub top_instance: CapHash,
315 pub image_hash: CapHash,
316}
317
318fn publish_image(nub: &mut Nub, image: &Image) -> CapHash {
327 let mut data_caps: Vec<(CapHash, Cap)> = Vec::new();
328 let mut pinned_hashes: Vec<(Key, CapHash)> = Vec::new();
329 let mut initial_hashes: Vec<(Key, CapHash)> = Vec::new();
330 for (slot, pinned) in &image.pinned_slots {
331 let (h, maybe_cap) = match pinned {
332 PinnedCap::Data { desc } => {
333 let cap = Cap::data_from_desc(&image.arena, desc);
334 let h = ssz::hash_tree_root(&cap);
335 (h, Some(cap))
336 }
337 PinnedCap::Image { content_hash } => (*content_hash, None),
338 };
339 pinned_hashes.push((slot.clone(), h));
340 if let Some(cap) = maybe_cap {
341 data_caps.push((h, cap));
342 }
343 }
344 for (slot, desc) in &image.initial_slots {
345 let cap = Cap::data_from_desc(&image.arena, desc);
346 let h = ssz::hash_tree_root(&cap);
347 initial_hashes.push((slot.clone(), h));
348 data_caps.push((h, cap));
349 }
350 for (h, cap) in &data_caps {
351 nub.put_cap_with_hash(*h, cap).expect("put data");
352 }
353 let image_cap =
354 Cap::image_with_slots(image, &pinned_hashes, &initial_hashes).expect("image_with_slots");
355 let image_hash = ssz::hash_tree_root(&image_cap);
356 nub.put_cap_with_hash(image_hash, &image_cap)
357 .expect("put image");
358 image_hash
359}
360
361fn publish_instance(
366 nub: &mut Nub,
367 image: &Image,
368 image_hash: CapHash,
369 cnode: javm_cap::CNodeCap,
370) -> CapHash {
371 const ENDPOINT_IDX: u8 = 0;
372
373 let cnode_cap = Cap::CNode(cnode);
374 let cnode_hash = ssz::hash_tree_root(&cnode_cap);
375 nub.put_cap_with_hash(cnode_hash, &cnode_cap)
376 .expect("put cnode");
377
378 let endpoint = image
379 .endpoints
380 .get(&Key::from(ENDPOINT_IDX))
381 .expect("endpoint 0");
382 let mut regs = [0u64; NUM_REGS];
383 for (&i, &v) in &endpoint.initial_regs {
384 if let Some(slot) = regs.get_mut(i as usize) {
385 *slot = v;
386 }
387 }
388
389 let mem = image.instance_mem_backing();
390 let inst_cap = Cap::instance_with_mem([0u8; 32], image_hash, cnode_hash, mem, regs, 0, 0);
391 let inst_hash = ssz::hash_tree_root(&inst_cap);
392 nub.put_cap_with_hash(inst_hash, &inst_cap)
393 .expect("put instance");
394 inst_hash
395}
396
397pub fn build_sub_vm_top(nub: &mut Nub, blob: &[u8]) -> SubVmTop {
401 use javm_cap::{CNodeCap, CapHashOrRef};
402 use ssz::Decode;
403
404 let image = Image::from_ssz_bytes(blob).expect("decode sub-vm image");
405 let image_hash = publish_image(nub, &image);
406
407 let mut cn = CNodeCap::new();
411 cn.set(
412 &Key::from(SLOT_IMAGE_RECURSE as u8),
413 Some(CapHashOrRef::Hash(image_hash)),
414 )
415 .expect("set image slot");
416
417 let inst_hash = publish_instance(nub, &image, image_hash, cn);
418 SubVmTop {
419 top_instance: inst_hash,
420 image_hash,
421 }
422}
423
424pub fn invoke_sub_vm(nub: &Nub, top: &SubVmTop, depth: u64) {
427 let result = nub
428 .invoke_cached(top.top_instance, 0, [depth, 0, 0, 0], INITIAL_GAS)
429 .expect("invoke_cached");
430 assert!(
431 result.exit_reason == EXIT_HOSTCALL && result.exit_arg == 0,
432 "sub-VM exited non-cleanly: reason={} arg={} ret={} gas={}",
433 result.exit_reason,
434 result.exit_arg,
435 result.return_value,
436 result.gas_remaining,
437 );
438}
439
440pub fn invoke_sub_vm_gas(nub: &Nub, top: &SubVmTop, depth: u64) -> (u64, u64) {
445 let result = nub
446 .invoke_cached(top.top_instance, 0, [depth, 0, 0, 0], INITIAL_GAS)
447 .expect("invoke_cached");
448 assert!(
449 result.exit_reason == EXIT_HOSTCALL && result.exit_arg == 0,
450 "sub-VM exited non-cleanly: reason={} arg={} ret={} gas={}",
451 result.exit_reason,
452 result.exit_arg,
453 result.return_value,
454 result.gas_remaining,
455 );
456 (
457 result.return_value,
458 INITIAL_GAS.saturating_sub(result.gas_remaining),
459 )
460}
461
462pub fn invoke_sub_vm_expect(nub: &Nub, top: &SubVmTop, depth: u64, expected_return: u64) {
466 let result = nub
467 .invoke_cached(top.top_instance, 0, [depth, 0, 0, 0], INITIAL_GAS)
468 .expect("invoke_cached");
469 assert!(
470 result.exit_reason == EXIT_HOSTCALL && result.exit_arg == 0,
471 "sub-VM exited non-cleanly: reason={} arg={} ret={} gas={}",
472 result.exit_reason,
473 result.exit_arg,
474 result.return_value,
475 result.gas_remaining,
476 );
477 assert_eq!(
478 result.return_value, expected_return,
479 "sub-VM depth {depth} returned {} (expected {expected_return})",
480 result.return_value,
481 );
482}
483
484pub fn hex_short(h: &CapHash) -> String {
486 use std::fmt::Write as _;
487 let mut s = String::with_capacity(16);
488 for b in &h[..8] {
489 let _ = write!(s, "{b:02x}");
490 }
491 s
492}
493
494pub fn run_recurse_bench(c: &mut Criterion, blob: &[u8], label: &str) {
500 let top = build_sub_vm_top(&mut nub_hyperlight_lock(), blob);
501 eprintln!(
502 "[{label}] image_hash={} top_instance={}",
503 hex_short(&top.image_hash),
504 hex_short(&top.top_instance),
505 );
506
507 {
510 let nub = nub_hyperlight_lock();
511 invoke_sub_vm(&nub, &top, 0);
512 invoke_sub_vm(&nub, &top, 1);
513 }
514 eprintln!("[{label}] depth 0/1 ok");
515
516 let mut g = c.benchmark_group(label);
517 g.sample_size(20);
518 for &depth in &[10u64, 100, 1_000] {
519 g.throughput(Throughput::Elements(depth));
520 g.bench_with_input(BenchmarkId::from_parameter(depth), &depth, |b, &d| {
521 b.iter(|| invoke_sub_vm(&nub_hyperlight_lock(), &top, d))
522 });
523 }
524 g.finish();
525}
526
527pub struct PtCacheTop {
533 pub top_instance: CapHash,
535 pub image_hash: CapHash,
537}
538
539pub fn build_pt_cache_top(nub: &mut Nub, blob: &[u8]) -> PtCacheTop {
543 use javm_cap::CNodeCap;
544 use ssz::Decode;
545
546 let image = Image::from_ssz_bytes(blob).expect("decode pt-cache image");
547 let image_hash = publish_image(nub, &image);
548 let top_instance = publish_instance(nub, &image, image_hash, CNodeCap::new());
551 PtCacheTop {
552 top_instance,
553 image_hash,
554 }
555}
556
557fn pt_cache_expected_return(n: u64) -> u64 {
560 let mut acc = 0u64;
561 for i in 0..n {
562 acc = acc.wrapping_add(i);
563 }
564 acc
565}
566
567pub fn invoke_pt_cache(nub: &Nub, top: &PtCacheTop, n: u64) -> (u64, u64) {
572 let result = nub
573 .invoke_cached(top.top_instance, 0, [n, 0, 0, 0], INITIAL_GAS)
574 .expect("invoke_cached");
575 assert!(
576 result.exit_reason == EXIT_HOSTCALL && result.exit_arg == 0,
577 "pt-cache exited non-cleanly: reason={} arg={} ret={} gas={}",
578 result.exit_reason,
579 result.exit_arg,
580 result.return_value,
581 result.gas_remaining,
582 );
583 assert_eq!(
584 result.return_value,
585 pt_cache_expected_return(n),
586 "pt-cache n={n} summed echoes {} (expected {})",
587 result.return_value,
588 pt_cache_expected_return(n),
589 );
590 (
591 result.return_value,
592 INITIAL_GAS.saturating_sub(result.gas_remaining),
593 )
594}
595
596pub fn run_pt_cache_bench(c: &mut Criterion, blob: &[u8], label: &str) {
602 let top = build_pt_cache_top(&mut nub_hyperlight_lock(), blob);
603 eprintln!(
604 "[{label}] image={} top={}",
605 hex_short(&top.image_hash),
606 hex_short(&top.top_instance),
607 );
608
609 {
610 let nub = nub_hyperlight_lock();
611 invoke_pt_cache(&nub, &top, 1);
612 invoke_pt_cache(&nub, &top, 2);
613 }
614 eprintln!("[{label}] n 1/2 ok");
615
616 let mut g = c.benchmark_group(label);
617 g.sample_size(20);
618 for &n in &[10u64, 100, 1_000] {
619 g.throughput(Throughput::Elements(n));
620 g.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, &n| {
621 b.iter(|| invoke_pt_cache(&nub_hyperlight_lock(), &top, n))
622 });
623 }
624 g.finish();
625}