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 #[inline]
135 $vis unsafe fn $name($($arg: $typ),*) -> $ret {
136 // Forcing the hash through an enum discriminant guarantees it is
137 // computed in a `const` context (matching the reference crate).
138 #[repr(usize)]
139 enum Syscall {
140 Code = $crate::syscalls::sys_hash($sym),
141 }
142 // SAFETY: Under SIMD-0178 the sBPF v3 loader resolves syscalls by a
143 // static dispatch key equal to murmur32(name); `sys_hash` computes
144 // exactly that key at const-eval. Transmuting the key to a function
145 // pointer whose signature mirrors the relocation declaration this
146 // arm replaces, then calling it, is the loader's defined static-call
147 // ABI. The invariant that makes this sound is that the hash equals
148 // Agave's dispatch key, pinned by the host tests to known
149 // constants, and that the pointer type matches the syscall's real
150 // C signature (identical to the relocation decl). The caller upholds
151 // the syscall's own pointer/length contract, unchanged from the
152 // relocation path.
153 let syscall: extern "C" fn($($arg: $typ),*) -> $ret =
154 unsafe { core::mem::transmute(Syscall::Code) };
155 // `syscall` is a *safe* `extern "C" fn` pointer, so the call itself
156 // needs no `unsafe` (matches the reference `solana-define-syscall`).
157 syscall($($arg),*)
158 }
159 };
160
161 // ── Public surface: explicit runtime symbol via leading attribute ─
162 // The Rust name differs from the runtime symbol; give the symbol as a
163 // leading `#[link_name = "..."]`. This arm must precede the generic arm
164 // so the symbol is not swallowed by `$(#[$attr])*`. Written as a normal
165 // `#[attr] fn` item so `rustfmt` leaves it intact (no inner separators).
166 (#[link_name = $sym:literal] $(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?) -> $ret:ty) => {
167 #[cfg(not(any(feature = "static-syscalls", target_feature = "static-syscalls")))]
168 $crate::define_syscall!(@reloc $(#[$attr])* $vis fn $name($($arg: $typ),*) -> $ret; link_name = $sym);
169 #[cfg(any(feature = "static-syscalls", target_feature = "static-syscalls"))]
170 $crate::define_syscall!(@static $(#[$attr])* $vis fn $name($($arg: $typ),*) -> $ret; hash = $sym);
171 };
172 (#[link_name = $sym:literal] $(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?)) => {
173 $crate::define_syscall!(#[link_name = $sym] $(#[$attr])* $vis fn $name($($arg: $typ),*) -> ());
174 };
175
176 // ── Public surface: Rust name == symbol name ─────────────────────
177 ($(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?) -> $ret:ty) => {
178 #[cfg(not(any(feature = "static-syscalls", target_feature = "static-syscalls")))]
179 $crate::define_syscall!(@reloc $(#[$attr])* $vis fn $name($($arg: $typ),*) -> $ret);
180 #[cfg(any(feature = "static-syscalls", target_feature = "static-syscalls"))]
181 $crate::define_syscall!(@static $(#[$attr])* $vis fn $name($($arg: $typ),*) -> $ret; hash = stringify!($name));
182 };
183 ($(#[$attr:meta])* $vis:vis fn $name:ident($($arg:ident: $typ:ty),* $(,)?)) => {
184 $crate::define_syscall!($(#[$attr])* $vis fn $name($($arg: $typ),*) -> ());
185 };
186}
187
188// The declarations below stay gated on `target_os = "solana"`, exactly as the
189// single historical `extern "C"` block was: off-chain (host) builds get no
190// syscall symbols and rely on each wrapper module's `not(target_os = "solana")`
191// fallback. The only change is that each decl now flows through the macro so the
192// same source is v3-ready under `--features static-syscalls`.
193
194/// Log a UTF-8 message.
195#[cfg(target_os = "solana")]
196define_syscall!(pub fn sol_log_(message: *const u8, len: u64));
197
198/// Log a 64-bit value.
199#[cfg(target_os = "solana")]
200define_syscall!(pub fn sol_log_64_(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64));
201
202/// Log the current compute unit consumption.
203#[cfg(target_os = "solana")]
204define_syscall!(pub fn sol_log_compute_units_());
205
206/// Remaining compute units for the current invocation (SIMD-0049).
207///
208/// Unlike `sol_log_compute_units_` (which only *logs*), this returns the
209/// value to the program, see `budget::CuBudget`. SIMD-0049 is Withdrawn and
210/// its gate has never been activated on a public cluster, so the loader
211/// rejects any ELF that references the symbol (`Unresolved symbol`). It is
212/// bound only under the `remaining-compute-units-syscall` feature.
213#[cfg(all(target_os = "solana", feature = "remaining-compute-units-syscall"))]
214define_syscall!(pub fn sol_remaining_compute_units() -> u64);
215
216/// Log structured data segments (for events).
217#[cfg(target_os = "solana")]
218define_syscall!(pub fn sol_log_data(data: *const u8, data_len: u64));
219
220/// Invoke a cross-program instruction (C ABI).
221#[cfg(target_os = "solana")]
222define_syscall!(pub fn sol_invoke_signed_c(
223 instruction_addr: *const u8,
224 account_infos_addr: *const u8,
225 account_infos_len: u64,
226 signers_seeds_addr: *const u8,
227 signers_seeds_len: u64
228) -> u64);
229
230/// Create a program-derived address.
231#[cfg(target_os = "solana")]
232define_syscall!(pub fn sol_create_program_address(
233 seeds_addr: *const u8,
234 seeds_len: u64,
235 program_id_addr: *const u8,
236 address_addr: *mut u8
237) -> u64);
238
239/// Find a program-derived address with bump seed.
240#[cfg(target_os = "solana")]
241define_syscall!(pub fn sol_try_find_program_address(
242 seeds_addr: *const u8,
243 seeds_len: u64,
244 program_id_addr: *const u8,
245 address_addr: *mut u8,
246 bump_seed_addr: *mut u8
247) -> u64);
248
249/// SHA-256 hash.
250#[cfg(target_os = "solana")]
251define_syscall!(pub fn sol_sha256(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64);
252
253/// Validate whether a point lies on the selected curve.
254#[cfg(target_os = "solana")]
255define_syscall!(pub fn sol_curve_validate_point(
256 curve_id: u64,
257 point_addr: *const u8,
258 result_point_addr: *mut u8
259) -> u64);
260
261/// Run a group operation on runtime-supported curve points.
262#[cfg(target_os = "solana")]
263define_syscall!(pub fn sol_curve_group_op(
264 curve_id: u64,
265 group_op: u64,
266 left_input_addr: *const u8,
267 right_input_addr: *const u8,
268 result_point_addr: *mut u8
269) -> u64);
270
271/// Run variable-length multiscalar multiplication on runtime-supported curves.
272#[cfg(target_os = "solana")]
273define_syscall!(pub fn sol_curve_multiscalar_mul(
274 curve_id: u64,
275 scalars_addr: *const u8,
276 points_addr: *const u8,
277 points_len: u64,
278 result_point_addr: *mut u8
279) -> u64);
280
281/// Keccak-256 hash.
282#[cfg(target_os = "solana")]
283define_syscall!(pub fn sol_keccak256(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64);
284
285/// BLAKE3 hash.
286#[cfg(target_os = "solana")]
287define_syscall!(pub fn sol_blake3(vals: *const u8, val_len: u64, hash_result: *mut u8) -> u64);
288
289/// Recover a secp256k1 public key from a 32-byte hash and compact signature.
290#[cfg(target_os = "solana")]
291define_syscall!(pub fn sol_secp256k1_recover(
292 hash: *const u8,
293 recovery_id: u64,
294 signature: *const u8,
295 result: *mut u8
296) -> u64);
297
298/// Poseidon hash.
299#[cfg(target_os = "solana")]
300define_syscall!(pub fn sol_poseidon(
301 parameters: u64,
302 endianness: u64,
303 vals: *const u8,
304 val_len: u64,
305 hash_result: *mut u8
306) -> u64);
307
308/// BN254 / alt_bn128 group operation.
309#[cfg(target_os = "solana")]
310define_syscall!(pub fn sol_alt_bn128_group_op(
311 group_op: u64,
312 input: *const u8,
313 input_size: u64,
314 result: *mut u8
315) -> u64);
316
317/// BN254 / alt_bn128 compression operation.
318#[cfg(target_os = "solana")]
319define_syscall!(pub fn sol_alt_bn128_compression(
320 op: u64,
321 input: *const u8,
322 input_size: u64,
323 result: *mut u8
324) -> u64);
325
326/// Big integer modular exponentiation.
327#[cfg(target_os = "solana")]
328define_syscall!(pub fn sol_big_mod_exp(params: *const u8, result: *mut u8) -> u64);
329
330/// Set return data for the current instruction.
331#[cfg(target_os = "solana")]
332define_syscall!(pub fn sol_set_return_data(data: *const u8, length: u64));
333
334/// Get return data from the previous CPI.
335#[cfg(target_os = "solana")]
336define_syscall!(pub fn sol_get_return_data(data: *mut u8, length: u64, program_id: *mut u8) -> u64);
337
338/// Get the current clock sysvar.
339#[cfg(target_os = "solana")]
340define_syscall!(pub fn sol_get_clock_sysvar(addr: *mut u8) -> u64);
341
342/// Get the current rent sysvar.
343#[cfg(target_os = "solana")]
344define_syscall!(pub fn sol_get_rent_sysvar(addr: *mut u8) -> u64);
345
346/// Get epoch schedule sysvar.
347#[cfg(target_os = "solana")]
348define_syscall!(pub fn sol_get_epoch_schedule_sysvar(addr: *mut u8) -> u64);
349
350/// Abort program execution.
351#[cfg(target_os = "solana")]
352define_syscall!(pub fn sol_panic_(file: *const u8, len: u64, line: u64, column: u64) -> !);
353
354/// Terminate the current invocation without exhausting its compute budget.
355#[cfg(target_os = "solana")]
356define_syscall!(pub fn abort() -> !);
357
358// ── Memory operations (SVM-optimized) ─────────────────────────
359
360/// Copy `n` bytes from `src` to `dst` (non-overlapping).
361#[cfg(target_os = "solana")]
362define_syscall!(pub fn sol_memcpy_(dst: *mut u8, src: *const u8, n: u64));
363
364/// Copy `n` bytes from `src` to `dst` (overlapping safe).
365#[cfg(target_os = "solana")]
366define_syscall!(pub fn sol_memmove_(dst: *mut u8, src: *const u8, n: u64));
367
368/// Compare `n` bytes. Sets `*result` to <0, 0, or >0.
369#[cfg(target_os = "solana")]
370define_syscall!(pub fn sol_memcmp_(s1: *const u8, s2: *const u8, n: u64, result: *mut i32));
371
372/// Fill `n` bytes with `c`.
373#[cfg(target_os = "solana")]
374define_syscall!(pub fn sol_memset_(s: *mut u8, c: u8, n: u64));
375
376// ── Instruction introspection ────────────────────────────────
377
378/// Get the current instruction stack height.
379#[cfg(target_os = "solana")]
380define_syscall!(pub fn sol_get_stack_height() -> u64);
381
382/// Get a previously processed sibling instruction.
383#[cfg(target_os = "solana")]
384define_syscall!(pub fn sol_get_processed_sibling_instruction(
385 index: u64,
386 meta: *mut u8,
387 program_id: *mut u8,
388 data: *mut u8,
389 accounts: *mut u8
390) -> u64);
391
392/// Get the last restart slot sysvar.
393#[cfg(target_os = "solana")]
394define_syscall!(pub fn sol_get_last_restart_slot(addr: *mut u8) -> u64);
395
396/// Generalized sysvar read: copy `length` bytes starting at `offset`
397/// from the sysvar identified by `sysvar_id_addr` into `result`.
398///
399/// This is the modern replacement for the per-sysvar syscalls and the
400/// only way to read large sysvars (SlotHashes, StakeHistory) without
401/// passing them as accounts.
402#[cfg(target_os = "solana")]
403define_syscall!(pub fn sol_get_sysvar(
404 sysvar_id_addr: *const u8,
405 result: *mut u8,
406 offset: u64,
407 length: u64
408) -> u64);
409
410/// Get the activated stake (current epoch) of the vote account at
411/// `vote_address`. A null `vote_address` returns the cluster-wide
412/// total active stake (SIMD-0133).
413#[cfg(target_os = "solana")]
414define_syscall!(pub fn sol_get_epoch_stake(vote_address: *const u8) -> u64);
415
416#[cfg(test)]
417mod tests {
418 // This module is intentionally *not* gated on `target_os = "solana"`, so
419 // the host test build exercises whichever `define_syscall!` arm the active
420 // feature selects:
421 // * `cargo test -p hopper-native` -> relocation arm
422 // * `cargo test -p hopper-native --features static-syscalls` -> static arm
423 // The generated declaration is never *called* on the host (the relocation
424 // symbol does not exist off-chain and the static hash is a fabricated
425 // pointer); we only prove it compiles into a callable declaration.
426 #[allow(dead_code)]
427 mod generated {
428 // Exercises the "symbol == name" surface.
429 crate::define_syscall!(
430 /// Test-only dummy syscall (never linked/called on host).
431 pub fn __hopper_probe_syscall(a: *const u8, n: u64) -> u64
432 );
433 // Exercises the explicit-`link_name` surface + unit return.
434 crate::define_syscall!(
435 #[link_name = "sol_memset_"]
436 pub fn __hopper_probe_alias(a: *mut u8, c: u8, n: u64)
437 );
438 }
439
440 #[test]
441 fn sys_hash_matches_known_agave_constants() {
442 // `sol_memcmp_` is the canonical cross-check: the Quasar
443 // `solana-compiler-builtins` reference pins it to 0x5FDC_DE31.
444 assert_eq!(super::sys_hash("sol_memcmp_"), 0x5FDC_DE31);
445 // Additional well-known Agave sBPF static-syscall dispatch keys.
446 assert_eq!(super::sys_hash("abort"), 0xB6FC_1A11);
447 assert_eq!(super::sys_hash("sol_memcpy_"), 0x717C_C4A3);
448 assert_eq!(super::sys_hash("sol_memset_"), 0x3770_FB22);
449 assert_eq!(super::sys_hash("sol_memmove_"), 0x4343_71F8);
450 assert_eq!(super::sys_hash("sol_invoke_signed_c"), 0xA22B_9C85);
451 }
452
453 // Under the static feature the macro must produce a real, addressable
454 // `unsafe fn`. Taking (but never calling) its address proves the static
455 // arm expanded to a callable declaration.
456 #[cfg(any(feature = "static-syscalls", target_feature = "static-syscalls"))]
457 #[test]
458 fn static_arm_expands_to_callable_fn() {
459 let f: unsafe fn(*const u8, u64) -> u64 = generated::__hopper_probe_syscall;
460 assert!(f as usize != 0);
461 }
462}