Skip to main content

hopper_native/
syscalls.rs

1//! Raw Solana syscall declarations.
2//!
3//! These are the functions provided by the Solana BPF/SBF runtime. Only
4//! available when compiling for `target_os = "solana"`.
5//!
6//! # Dual-mode dispatch (SIMD-0178 readiness)
7//!
8//! Every declaration goes through `define_syscall!`, which emits one of two
9//! equivalent forms depending on the active build:
10//!
11//! * **Default (relocation).** With neither the `static-syscalls` cargo feature
12//!   nor the `static-syscalls` target-feature enabled, the macro emits exactly
13//!   the historical `extern "C"` declaration. This is byte-for-byte the same
14//!   relocation-based syscall the framework has always used, so the default
15//!   build is unchanged.
16//!
17//! * **Static (sBPF v3 / SIMD-0178).** When `static-syscalls` is enabled, the
18//!   macro instead emits an `unsafe fn` that transmutes the murmur32 hash of the
19//!   syscall name to the matching `extern "C"` function pointer and calls it.
20//!   This is the static-syscall ABI the sBPF v3 loader uses in place of syscall
21//!   relocations (SIMD-0178), matching the reference `solana-define-syscall`
22//!   implementation.
23//!
24//! The feature is **opt-in and default-off**: it exists so Hopper is ready to
25//! build for the v3 loader, without changing anything for today's v0..v2
26//! targets. The hash is computed at const-eval time from the syscall name; the
27//! constants are pinned by host tests against known Agave values (e.g.
28//! `sol_memcmp_` → `0x5FDC_DE31`).
29
30// Rustdoc cannot attach outer doc comments to a declarative-macro invocation,
31// even though `define_syscall!` forwards attributes to the emitted function.
32// Keep the call-site documentation readable in source and suppress only that
33// target-specific compiler artifact.
34#![cfg_attr(target_os = "solana", allow(unused_doc_comments))]
35
36/// murmur3 (32-bit) hash of a syscall name, seed `0`.
37///
38/// This is the exact construction the Agave sBPF loader and the reference
39/// `solana-define-syscall` crate use to derive the static-syscall dispatch key
40/// under SIMD-0178. Kept `const` so the hash folds at compile time inside the
41/// generated syscall stubs.
42#[doc(hidden)]
43pub const fn sys_hash(name: &str) -> usize {
44    murmur3_32(name.as_bytes(), 0) as usize
45}
46
47/// `const`-evaluable murmur3-32 over `buf` with the given `seed`.
48///
49/// Mirrors the reference implementation in `solana-define-syscall` verbatim so
50/// that Hopper's static-syscall dispatch keys are identical to Agave's.
51const fn murmur3_32(buf: &[u8], seed: u32) -> u32 {
52    const fn pre_mix(buf: [u8; 4]) -> u32 {
53        u32::from_le_bytes(buf)
54            .wrapping_mul(0xcc9e2d51)
55            .rotate_left(15)
56            .wrapping_mul(0x1b873593)
57    }
58
59    let mut hash = seed;
60
61    let mut i = 0;
62    while i < buf.len() / 4 {
63        let buf = [buf[i * 4], buf[i * 4 + 1], buf[i * 4 + 2], buf[i * 4 + 3]];
64        hash ^= pre_mix(buf);
65        hash = hash.rotate_left(13);
66        hash = hash.wrapping_mul(5).wrapping_add(0xe6546b64);
67
68        i += 1;
69    }
70
71    match buf.len() % 4 {
72        0 => {}
73        1 => {
74            hash ^= pre_mix([buf[i * 4], 0, 0, 0]);
75        }
76        2 => {
77            hash ^= pre_mix([buf[i * 4], buf[i * 4 + 1], 0, 0]);
78        }
79        3 => {
80            hash ^= pre_mix([buf[i * 4], buf[i * 4 + 1], buf[i * 4 + 2], 0]);
81        }
82        _ => { /* unreachable */ }
83    }
84
85    hash ^= buf.len() as u32;
86    hash ^= hash.wrapping_shr(16);
87    hash = hash.wrapping_mul(0x85ebca6b);
88    hash ^= hash.wrapping_shr(13);
89    hash = hash.wrapping_mul(0xc2b2ae35);
90    hash ^= hash.wrapping_shr(16);
91
92    hash
93}
94
95/// Declare a Solana syscall in a build-mode-agnostic way.
96///
97/// See the module docs for the two emitted forms. Two surface syntaxes are
98/// supported:
99///
100/// ```ignore
101/// // 1. Rust name == the runtime symbol name (native decls).
102/// define_syscall!(fn sol_log_(message: *const u8, len: u64));
103///
104/// // 2. Rust name differs from the symbol name; give the symbol explicitly
105/// //    so the static-mode hash is taken over the real syscall name.
106/// define_syscall!(fn syscall_sol_memcpy(dst: *mut u8, src: *const u8, n: u64);
107///     link_name = "sol_memcpy_");
108/// ```
109///
110/// Invocations are expected to be gated on `#[cfg(target_os = "solana")]` by the
111/// caller, exactly as the historical `extern "C"` blocks were. The macro itself
112/// does not add that gate, which lets host tests exercise both emitted forms.
113#[macro_export]
114macro_rules! define_syscall {
115    // ── Relocation-mode emitter (default build) ──────────────────────
116    // Byte-identical to the historical `extern "C"` declaration.
117    (@reloc $(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?) -> $ret:ty) => {
118        extern "C" {
119            $(#[$attr])*
120            $vis fn $name($($arg: $typ),*) -> $ret;
121        }
122    };
123    (@reloc $(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?) -> $ret:ty; link_name = $sym:literal) => {
124        extern "C" {
125            #[link_name = $sym]
126            $(#[$attr])*
127            $vis fn $name($($arg: $typ),*) -> $ret;
128        }
129    };
130
131    // ── Static-mode emitter (SIMD-0178 / sBPF v3) ────────────────────
132    (@static $(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?) -> $ret:ty; hash = $sym:expr) => {
133        $(#[$attr])*
134        // SAFETY: `unsafe` because this is a raw syscall: the caller
135        // upholds the contract of the named syscall for every pointer
136        // and length argument, exactly as for the `extern "C"`
137        // declaration this arm replaces.
138        #[inline]
139        $vis unsafe fn $name($($arg: $typ),*) -> $ret {
140            // Forcing the hash through an enum discriminant guarantees it is
141            // computed in a `const` context (matching the reference crate).
142            #[repr(usize)]
143            enum Syscall {
144                Code = $crate::syscalls::sys_hash($sym),
145            }
146            // SAFETY: Under SIMD-0178 the sBPF v3 loader resolves syscalls by a
147            // static dispatch key equal to murmur32(name); `sys_hash` computes
148            // exactly that key at const-eval. Transmuting the key to a function
149            // pointer whose signature mirrors the relocation declaration this
150            // arm replaces, then calling it, is the loader's defined static-call
151            // ABI. The invariant that makes this sound is that the hash equals
152            // Agave's dispatch key, pinned by the host tests to known
153            // constants, and that the pointer type matches the syscall's real
154            // C signature (identical to the relocation decl). The caller upholds
155            // the syscall's own pointer/length contract, unchanged from the
156            // relocation path.
157            let syscall: extern "C" fn($($arg: $typ),*) -> $ret =
158                unsafe { core::mem::transmute(Syscall::Code) };
159            // `syscall` is a *safe* `extern "C" fn` pointer, so the call itself
160            // needs no `unsafe` (matches the reference `solana-define-syscall`).
161            syscall($($arg),*)
162        }
163    };
164
165    // ── Public surface: explicit runtime symbol via leading attribute ─
166    // The Rust name differs from the runtime symbol; give the symbol as a
167    // leading `#[link_name = "..."]`. This arm must precede the generic arm
168    // so the symbol is not swallowed by `$(#[$attr])*`. Written as a normal
169    // `#[attr] fn` item so `rustfmt` leaves it intact (no inner separators).
170    (#[link_name = $sym:literal] $(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?) -> $ret:ty) => {
171        #[cfg(not(any(feature = "static-syscalls", target_feature = "static-syscalls")))]
172        $crate::define_syscall!(@reloc $(#[$attr])* $vis fn $name($($arg: $typ),*) -> $ret; link_name = $sym);
173        #[cfg(any(feature = "static-syscalls", target_feature = "static-syscalls"))]
174        $crate::define_syscall!(@static $(#[$attr])* $vis fn $name($($arg: $typ),*) -> $ret; hash = $sym);
175    };
176    (#[link_name = $sym:literal] $(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?)) => {
177        $crate::define_syscall!(#[link_name = $sym] $(#[$attr])* $vis fn $name($($arg: $typ),*) -> ());
178    };
179
180    // ── Public surface: Rust name == symbol name ─────────────────────
181    ($(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?) -> $ret:ty) => {
182        #[cfg(not(any(feature = "static-syscalls", target_feature = "static-syscalls")))]
183        $crate::define_syscall!(@reloc $(#[$attr])* $vis fn $name($($arg: $typ),*) -> $ret);
184        #[cfg(any(feature = "static-syscalls", target_feature = "static-syscalls"))]
185        $crate::define_syscall!(@static $(#[$attr])* $vis fn $name($($arg: $typ),*) -> $ret; hash = stringify!($name));
186    };
187    ($(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?)) => {
188        $crate::define_syscall!($(#[$attr])* $vis fn $name($($arg: $typ),*) -> ());
189    };
190}
191
192// The declarations below stay gated on `target_os = "solana"`, exactly as the
193// single historical `extern "C"` block was: off-chain (host) builds get no
194// syscall symbols and rely on each wrapper module's `not(target_os = "solana")`
195// fallback. The only change is that each decl now flows through the macro so the
196// same source is v3-ready under `--features static-syscalls`.
197
198/// Log a UTF-8 message.
199#[cfg(target_os = "solana")]
200define_syscall!(pub fn sol_log_(message: *const u8, len: u64));
201
202/// Log a 64-bit value.
203#[cfg(target_os = "solana")]
204define_syscall!(pub fn sol_log_64_(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64));
205
206/// Log the current compute unit consumption.
207#[cfg(target_os = "solana")]
208define_syscall!(pub fn sol_log_compute_units_());
209
210/// Remaining compute units for the current invocation (SIMD-0049).
211///
212/// Unlike `sol_log_compute_units_` (which only *logs*), this returns the
213/// value to the program, see `budget::CuBudget`. SIMD-0049 is Withdrawn and
214/// its gate has never been activated on a public cluster, so the loader
215/// rejects any ELF that references the symbol (`Unresolved symbol`). It is
216/// bound only under the `remaining-compute-units-syscall` feature.
217#[cfg(all(target_os = "solana", feature = "remaining-compute-units-syscall"))]
218define_syscall!(pub fn sol_remaining_compute_units() -> u64);
219
220/// Log structured data segments (for events).
221#[cfg(target_os = "solana")]
222define_syscall!(pub fn sol_log_data(data: *const u8, data_len: u64));
223
224/// Invoke a cross-program instruction (C ABI).
225#[cfg(target_os = "solana")]
226define_syscall!(pub fn sol_invoke_signed_c(
227    instruction_addr: *const u8,
228    account_infos_addr: *const u8,
229    account_infos_len: u64,
230    signers_seeds_addr: *const u8,
231    signers_seeds_len: u64
232) -> u64);
233
234/// Create a program-derived address.
235#[cfg(target_os = "solana")]
236define_syscall!(pub fn sol_create_program_address(
237    seeds_addr: *const u8,
238    seeds_len: u64,
239    program_id_addr: *const u8,
240    address_addr: *mut u8
241) -> u64);
242
243/// Find a program-derived address with bump seed.
244#[cfg(target_os = "solana")]
245define_syscall!(pub fn sol_try_find_program_address(
246    seeds_addr: *const u8,
247    seeds_len: u64,
248    program_id_addr: *const u8,
249    address_addr: *mut u8,
250    bump_seed_addr: *mut u8
251) -> u64);
252
253/// SHA-256 hash.
254#[cfg(target_os = "solana")]
255define_syscall!(pub fn sol_sha256(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64);
256
257/// Validate whether a point lies on the selected curve.
258#[cfg(target_os = "solana")]
259define_syscall!(pub fn sol_curve_validate_point(
260    curve_id: u64,
261    point_addr: *const u8,
262    result_point_addr: *mut u8
263) -> u64);
264
265/// Run a group operation on runtime-supported curve points.
266#[cfg(target_os = "solana")]
267define_syscall!(pub fn sol_curve_group_op(
268    curve_id: u64,
269    group_op: u64,
270    left_input_addr: *const u8,
271    right_input_addr: *const u8,
272    result_point_addr: *mut u8
273) -> u64);
274
275/// Run variable-length multiscalar multiplication on runtime-supported curves.
276#[cfg(target_os = "solana")]
277define_syscall!(pub fn sol_curve_multiscalar_mul(
278    curve_id: u64,
279    scalars_addr: *const u8,
280    points_addr: *const u8,
281    points_len: u64,
282    result_point_addr: *mut u8
283) -> u64);
284
285/// Keccak-256 hash.
286#[cfg(target_os = "solana")]
287define_syscall!(pub fn sol_keccak256(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64);
288
289/// BLAKE3 hash.
290#[cfg(target_os = "solana")]
291define_syscall!(pub fn sol_blake3(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64);
292
293/// SHA-512 over a slice list (feature gate
294/// `s512oDwgx8hjMnaQjXfqqrZroVj4HvC6TkN3iSSWXCh`, `enable_sha512_syscall`).
295/// Active on devnet and testnet and absent on mainnet-beta on 2026-09-27; a
296/// program that references the symbol fails to load where the gate is
297/// inactive, so it is bound only under the `sha512-syscall` feature.
298#[cfg(all(target_os = "solana", feature = "sha512-syscall"))]
299define_syscall!(pub fn sol_sha512(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64);
300
301/// Recover a secp256k1 public key from a 32-byte hash and compact signature.
302#[cfg(target_os = "solana")]
303define_syscall!(pub fn sol_secp256k1_recover(
304    hash: *const u8,
305    recovery_id: u64,
306    signature: *const u8,
307    result: *mut u8
308) -> u64);
309
310/// Poseidon hash.
311#[cfg(target_os = "solana")]
312define_syscall!(pub fn sol_poseidon(
313    parameters: u64,
314    endianness: u64,
315    vals: *const u8,
316    val_len: u64,
317    hash_result: *mut u8
318) -> u64);
319
320/// BN254 / alt_bn128 group operation.
321#[cfg(target_os = "solana")]
322define_syscall!(pub fn sol_alt_bn128_group_op(
323    group_op: u64,
324    input: *const u8,
325    input_size: u64,
326    result: *mut u8
327) -> u64);
328
329/// BN254 / alt_bn128 compression operation.
330#[cfg(target_os = "solana")]
331define_syscall!(pub fn sol_alt_bn128_compression(
332    op: u64,
333    input: *const u8,
334    input_size: u64,
335    result: *mut u8
336) -> u64);
337
338/// Big integer modular exponentiation.
339#[cfg(target_os = "solana")]
340define_syscall!(pub fn sol_big_mod_exp(params: *const u8, result: *mut u8) -> u64);
341
342/// Set return data for the current instruction.
343#[cfg(target_os = "solana")]
344define_syscall!(pub fn sol_set_return_data(data: *const u8, length: u64));
345
346/// Get return data from the previous CPI.
347#[cfg(target_os = "solana")]
348define_syscall!(pub fn sol_get_return_data(data: *mut u8, length: u64, program_id: *mut u8) -> u64);
349
350/// Get the current clock sysvar.
351#[cfg(target_os = "solana")]
352define_syscall!(pub fn sol_get_clock_sysvar(addr: *mut u8) -> u64);
353
354/// Get the current rent sysvar.
355#[cfg(target_os = "solana")]
356define_syscall!(pub fn sol_get_rent_sysvar(addr: *mut u8) -> u64);
357
358/// Get epoch schedule sysvar.
359#[cfg(target_os = "solana")]
360define_syscall!(pub fn sol_get_epoch_schedule_sysvar(addr: *mut u8) -> u64);
361
362/// Abort program execution.
363#[cfg(target_os = "solana")]
364define_syscall!(pub fn sol_panic_(file: *const u8, len: u64, line: u64, column: u64) -> !);
365
366/// Terminate the current invocation without exhausting its compute budget.
367#[cfg(target_os = "solana")]
368define_syscall!(pub fn abort() -> !);
369
370// ── Memory operations (SVM-optimized) ─────────────────────────
371
372/// Copy `n` bytes from `src` to `dst` (non-overlapping).
373#[cfg(target_os = "solana")]
374define_syscall!(pub fn sol_memcpy_(dst: *mut u8, src: *const u8, n: u64));
375
376/// Copy `n` bytes from `src` to `dst` (overlapping safe).
377#[cfg(target_os = "solana")]
378define_syscall!(pub fn sol_memmove_(dst: *mut u8, src: *const u8, n: u64));
379
380/// Compare `n` bytes. Sets `*result` to <0, 0, or >0.
381#[cfg(target_os = "solana")]
382define_syscall!(pub fn sol_memcmp_(s1: *const u8, s2: *const u8, n: u64, result: *mut i32));
383
384/// Fill `n` bytes with `c`.
385#[cfg(target_os = "solana")]
386define_syscall!(pub fn sol_memset_(s: *mut u8, c: u8, n: u64));
387
388// ── Instruction introspection ────────────────────────────────
389
390/// Get the current instruction stack height.
391#[cfg(target_os = "solana")]
392define_syscall!(pub fn sol_get_stack_height() -> u64);
393
394/// Get a previously processed sibling instruction.
395#[cfg(target_os = "solana")]
396define_syscall!(pub fn sol_get_processed_sibling_instruction(
397    index: u64,
398    meta: *mut u8,
399    program_id: *mut u8,
400    data: *mut u8,
401    accounts: *mut u8
402) -> u64);
403
404/// Get the last restart slot sysvar.
405#[cfg(target_os = "solana")]
406define_syscall!(pub fn sol_get_last_restart_slot(addr: *mut u8) -> u64);
407
408/// Generalized sysvar read: copy `length` bytes starting at `offset`
409/// from the sysvar identified by `sysvar_id_addr` into `result`.
410///
411/// This is the modern replacement for the per-sysvar syscalls and the
412/// only way to read large sysvars (SlotHashes, StakeHistory) without
413/// passing them as accounts.
414#[cfg(target_os = "solana")]
415define_syscall!(pub fn sol_get_sysvar(
416    sysvar_id_addr: *const u8,
417    result: *mut u8,
418    offset: u64,
419    length: u64
420) -> u64);
421
422/// Get the activated stake (current epoch) of the vote account at
423/// `vote_address`. A null `vote_address` returns the cluster-wide
424/// total active stake (SIMD-0133).
425#[cfg(target_os = "solana")]
426define_syscall!(pub fn sol_get_epoch_stake(vote_address: *const u8) -> u64);
427
428#[cfg(test)]
429mod tests {
430    // This module is intentionally *not* gated on `target_os = "solana"`, so
431    // the host test build exercises whichever `define_syscall!` arm the active
432    // feature selects:
433    //   * `cargo test -p hopper-native`                       -> relocation arm
434    //   * `cargo test -p hopper-native --features static-syscalls` -> static arm
435    // The generated declaration is never *called* on the host (the relocation
436    // symbol does not exist off-chain and the static hash is a fabricated
437    // pointer); we only prove it compiles into a callable declaration.
438    #[allow(dead_code)]
439    mod generated {
440        // Exercises the "symbol == name" surface.
441        crate::define_syscall!(
442            /// Test-only dummy syscall (never linked/called on host).
443            pub fn __hopper_probe_syscall(a: *const u8, n: u64) -> u64
444        );
445        // Exercises the explicit-`link_name` surface + unit return.
446        crate::define_syscall!(
447            #[link_name = "sol_memset_"]
448            pub fn __hopper_probe_alias(a: *mut u8, c: u8, n: u64)
449        );
450    }
451
452    #[test]
453    fn sys_hash_matches_known_agave_constants() {
454        // `sol_memcmp_` is the canonical cross-check: the Quasar
455        // `solana-compiler-builtins` reference pins it to 0x5FDC_DE31.
456        assert_eq!(super::sys_hash("sol_memcmp_"), 0x5FDC_DE31);
457        // Additional well-known Agave sBPF static-syscall dispatch keys.
458        assert_eq!(super::sys_hash("abort"), 0xB6FC_1A11);
459        assert_eq!(super::sys_hash("sol_memcpy_"), 0x717C_C4A3);
460        assert_eq!(super::sys_hash("sol_memset_"), 0x3770_FB22);
461        assert_eq!(super::sys_hash("sol_memmove_"), 0x4343_71F8);
462        assert_eq!(super::sys_hash("sol_invoke_signed_c"), 0xA22B_9C85);
463    }
464
465    // Under the static feature the macro must produce a real, addressable
466    // `unsafe fn`. Taking (but never calling) its address proves the static
467    // arm expanded to a callable declaration.
468    #[cfg(any(feature = "static-syscalls", target_feature = "static-syscalls"))]
469    #[test]
470    fn static_arm_expands_to_callable_fn() {
471        let f: unsafe fn(*const u8, u64) -> u64 = generated::__hopper_probe_syscall;
472        assert!(f as usize != 0);
473    }
474}