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        #[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}