Skip to main content

nub_bench/
lib.rs

1//! Shared harness for nub's benchmarks and end-to-end tests.
2//!
3//! Every program in `nub/programs` is cross-compiled and linked by this
4//! crate's `build.rs`, then exposed here as a decoded
5//! [`ProgramBlob`]. Both the criterion benches and the conformance
6//! tests drive the same list, so a new program is one line away from
7//! being both measured and checked.
8
9use nub_program::ProgramBlob;
10
11/// Gas ceiling for a benchmark run. `i64::MAX`, not `u64::MAX`: the
12/// JIT's gas counter is an `i64` and it detects exhaustion by sign, so
13/// a `u64::MAX` budget would present as already-negative.
14pub const BENCH_GAS: u64 = i64::MAX as u64;
15
16/// Clean-halt exit reason: `nub-rt`'s endpoint trampoline ends in a
17/// bare `ecall`, which the linker rewrites to `custom-0 ecalli imm=0`
18/// and the engine surfaces as `HostCall(0)`.
19pub const EXIT_HOST_CALL: u32 = 4;
20
21/// One benchmark program: its name, its linked blob, and the
22/// `(return_value, gas_used)` its endpoint 0 must produce.
23///
24/// The pinned pair is the durable invariant across every refactor of
25/// the linker or the program pipeline. It is deliberately duplicated
26/// with jar's `javm-bench/tests/workloads.rs`: both engines and both
27/// program formats must agree on the same numbers, so the duplication
28/// is the cross-check.
29pub struct Program {
30    pub name: &'static str,
31    pub blob: &'static [u8],
32    pub expected_value: u64,
33    pub expected_gas: u64,
34}
35
36impl Program {
37    /// Decode the blob, panicking with the program's name on failure.
38    pub fn decode(&self) -> ProgramBlob {
39        ProgramBlob::from_bytes(self.blob)
40            .unwrap_or_else(|e| panic!("[{}] decode ProgramBlob: {e}", self.name))
41    }
42}
43
44/// Every program, in a stable order.
45pub const PROGRAMS: &[Program] = &[
46    Program {
47        name: "prime_sieve",
48        blob: include_bytes!(env!("PRIME_SIEVE_BLOB")),
49        expected_value: 0x2578,
50        expected_gas: 8_972_959,
51    },
52    Program {
53        name: "ed25519",
54        blob: include_bytes!(env!("ED25519_BLOB")),
55        expected_value: 0x1,
56        expected_gas: 2_360_953,
57    },
58    Program {
59        name: "keccak",
60        blob: include_bytes!(env!("KECCAK_BLOB")),
61        expected_value: 0x39e5_0259,
62        expected_gas: 100_934,
63    },
64    Program {
65        name: "blake2b",
66        blob: include_bytes!(env!("BLAKE2B_BLOB")),
67        expected_value: 0xee1f_55f1,
68        expected_gas: 62_396,
69    },
70    Program {
71        name: "ecrecover",
72        blob: include_bytes!(env!("ECRECOVER_BLOB")),
73        expected_value: 0x1,
74        expected_gas: 6_811_627,
75    },
76    Program {
77        name: "goldilocks_mul",
78        blob: include_bytes!(env!("GOLDILOCKS_MUL_BLOB")),
79        expected_value: 0x2cf7_3e57,
80        expected_gas: 2_400_166,
81    },
82    Program {
83        name: "poseidon2_perm",
84        blob: include_bytes!(env!("POSEIDON2_PERM_BLOB")),
85        expected_value: 0x3ce3_3156,
86        expected_gas: 14_561_457,
87    },
88    Program {
89        name: "mini_verifier",
90        blob: include_bytes!(env!("MINI_VERIFIER_BLOB")),
91        expected_value: 0xf98f_c4ab,
92        expected_gas: 5_879_175,
93    },
94    Program {
95        name: "poly_eval",
96        blob: include_bytes!(env!("POLY_EVAL_BLOB")),
97        expected_value: 0x01da_34e2,
98        expected_gas: 9_005_991,
99    },
100    Program {
101        name: "fri_fold_tree",
102        blob: include_bytes!(env!("FRI_FOLD_TREE_BLOB")),
103        expected_value: 0x37e6_76f4,
104        expected_gas: 6_194_439,
105    },
106];
107
108/// Run endpoint 0 of `blob` on the interpreter, returning
109/// `(return_value, gas_used)`.
110///
111/// Panics unless the program halts cleanly, so a benchmark can never
112/// silently measure a trapping program.
113pub fn run_interpreter(name: &str, blob: &ProgramBlob) -> (u64, u64) {
114    let result = nub_arch_local::run_blob(blob, 0, [0; 4], BENCH_GAS)
115        .unwrap_or_else(|e| panic!("[{name}] prepare: {e}"));
116    assert_eq!(
117        result.exit_reason, EXIT_HOST_CALL,
118        "[{name}] did not halt cleanly: exit_reason={} exit_arg={}",
119        result.exit_reason, result.exit_arg,
120    );
121    assert_eq!(result.exit_arg, 0, "[{name}] unexpected host call");
122    (result.return_value, BENCH_GAS - result.gas_remaining)
123}
124
125/// Path to the flat personality's guest ELF, built by this crate's
126/// `build.rs`.
127///
128/// The blob is a bare-metal x86-64 kernel that boots inside the KVM
129/// sandbox and drives the JIT. Handing a path (rather than bytes) is
130/// what `nub::Nub::create_hyperlight` takes.
131#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
132pub const FLAT_GUEST_BLOB_PATH: &str = env!("NUB_FLAT_GUEST_BLOB");
133
134/// A process-wide `Nub<Flat>` over the KVM sandbox.
135///
136/// There can only ever be one Hyperlight sandbox per process — the
137/// guest-VA window is a single process-wide reservation that is never
138/// released, even after drop — so every caller shares this one.
139#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
140pub fn flat_sandbox() -> &'static nub::Nub<nub_flat::Flat> {
141    use std::sync::OnceLock;
142    static SANDBOX: OnceLock<nub::Nub<nub_flat::Flat>> = OnceLock::new();
143    SANDBOX.get_or_init(|| {
144        nub::Nub::create_hyperlight(FLAT_GUEST_BLOB_PATH, nub::NubOptions::default())
145            .expect("create the flat sandbox")
146    })
147}