This project is a proof-of-concept (PoC) demonstrating how to implement a custom syscall in the Solana blockchain runtime. In this project I've implemented a simple syscall to print a Unit8 array of the pubkey.
It is intended for advanced Solana developers and contributors who want to understand or extend Solana’s system call (syscall) interface with custom logic.
Custom syscalls allow you to add new low-level functionality to the Solana runtime, which can then be invoked by on-chain programs. This PoC walks through the process of defining, registering, and using a custom syscall, providing a practical reference for similar extensions.
- agave/: Contains the core Solana runtime and related modules, with modifications to support the custom syscall.
- native-test-syscall/: Example Solana program and test suite that demonstrates how to invoke the custom syscall from on-chain code.
- solana-sdk/: SDK and client-side utilities, potentially with extensions to support the new syscall.
- README.md: This file.
-
Define your syscall logic in
agave/programs/bpf_loader/src/syscalls. I have added mine in logging.rs since my syscall is related to logging a pubkey.// Custom Syscall declare_builtin_function!( /// Log a [`Pubkey`] as a Unit8 array SyscallLogPubkeyAsUnit8, fn rust( invoke_context: &mut InvokeContext, pubkey_addr: u64, _arg2: u64, _arg3: u64, _arg4: u64, _arg5: u64, memory_mapping: &mut MemoryMapping, ) -> Result<u64, Error> { let cost = invoke_context.get_execution_cost().log_pubkey_units; consume_compute_meter(invoke_context, cost)?; let pubkey = translate_type::<Pubkey>( memory_mapping, pubkey_addr, invoke_context.get_check_aligned(), )?; let bytes = pubkey.to_bytes(); let byte_str = bytes.iter().map(|b| b.to_string()).collect::<Vec<_>>().join(", "); let log_msg = format!("[{}]", byte_str); stable_log::program_log(&invoke_context.get_log_collector(), &log_msg); Ok(0) } );
-
Register your syscall in
agave/programs/bpf_loader/src/mod.rs// Custom Syscall result.register_function("sol_log_pubkey_as_unit8", SyscallLogPubkeyAsUnit8::vm)?;
-
Define your syscall in the
solana-sdk/define-syscall/src/definitions.rs// Custom Syscall define_syscall!(fn sol_log_pubkey_as_unit8(pubkey_addr: *const u8));
- Create a new file named sol_pub_unit8.rs in
solana-sdk/program/src:
use solana_pubkey::Pubkey; pub fn sol_log_pubkey_as_unit8(pubkey: &Pubkey) { #[cfg(target_os = "solana")] unsafe { crate::syscalls::sol_log_pubkey_as_unit8(pubkey.as_ref() as *const _ as *const u8) }; #[cfg(not(target_os = "solana"))] crate::program_stubs::sol_log_pubkey_as_unit8(pubkey); }
- Import this file to
solana-sdk/program/src/lib.rs
pub mod sol_pub_unit8;
- Create a new file named sol_pub_unit8.rs in
-
Now we need to add this to
solana-sdk/sysvar/program_stubs.rs- Add under SyscallStubs:
fn sol_log_pubkey_as_unit8(&self, pubkey: &Pubkey) { println!("pubkey: {}", pubkey); }
- Define the function somewhere below with all other syscalls:
pub fn sol_log_pubkey_as_unit8(pubkey: &Pubkey) { SYSCALL_STUBS .read() .unwrap() .sol_log_pubkey_as_unit8(pubkey); }
-
Now in your program import our local Solana program in the following way:
solana-program = { path = "../../solana-sdk/program", version = "2.3.0" } -
Import the syscall in the following way and use it:
- Import:
use solana_program::{ sol_pub_unit8::sol_log_pubkey_as_unit8, };
- Usage:
sol_log_pubkey_as_unit8(payer.key);
- To avoid any build & versioning issues make the dependencies causing issues to point to local. For eg, in the solana-sdk I have pointed solana-stake-interface and solana-system-interface to the ones I have cloned locally:
solana-stake-interface = { path = "../stake/interface", version = "1.2.0", features = ["bincode"] } solana-system-interface = { path = "../system/interface", version = "1.0.0" }
- In the stake and system repo I have make some dependencies point to local to avoid versioning issues:
- system
solana-instruction = {path = "../solana-sdk/instruction"} solana-pubkey = { path="../solana-sdk/pubkey", default-features = false } solana-sysvar = {path = "../solana-sdk/sysvar", version = "2.2.1", features = ["bincode"] }
- stake
solana-sysvar-id = { path = "../../solana-sdk/sysvar-id", version = "2.2.1"}
- While running the solana-cli to deploy the program, make sure you are on the agave directory because your local solana-cli is not aware of the syscall, else you'll get the following error:
Error: ELF error: ELF error: Unresolved symbol (sol_log_pubkey_as_unit8) at instruction #187 (ELF file offset 0x5d8)
I have written a simple program in native rust to transfer sol via cpi, and there we have used this syscall.
- Rust (latest stable)
- Node.js & npm (for TypeScript tests)
- Solana tool suite (for building and running local clusters)
-
Clone the repository
git clone <this-repo-url> cd agave_custom_syscall_example
-
Build the Solana runtime with the custom syscall
cd agave cargo build -
Build the example program
cd native-test-syscall/program cargo build-sbf -
Run the validator
cd agave cargo run -p agave-validator --bin solana-test-validator -
Deploy your program from the agave solana-cli
cargo run -p solana-cli -- program deploy ../native-test-syscall/program/target/deploy/native_test_syscall.so -ul
-
Run the tests
cd native-test-syscall pnpm i pnpm run test
- Custom Syscall Definition: The syscall is defined and registered in the Solana runtime (see
agave/). - Program Invocation: The example program in
native-test-syscall/calls the custom syscall. - Testing: TypeScript tests demonstrate the syscall in action and validate its behavior.
- Extending Solana with new runtime features
- Experimenting with low-level blockchain mechanics
- Educational reference for Solana core contributors
This project is for educational and experimental purposes only. Custom syscalls require modifying the Solana runtime and are not supported on mainnet or by default in public clusters.
