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}