This is a simple proof of concept for creating a custom syscall in the Agave validator. My goal was to create a simple syscall from a program that returns a string or number.
msg!("sol_get_magic_number {:?}", sol_get_magic_number()); // returns 2002My first step when building on complex tech is to achieve simple working code and build complex functionality after it.
Clone these Anza repositories:
First, I tried to find the registered syscalls in the solana-sdk repo. I searched for a simple one and found fn sol_remaining_compute_units() -> u64 (here). I then searched for the function name sol_remaining_compute_units() in both the solana-sdk and agave repos to understand where syscalls need to be defined and registered.
Now I know where a syscall needs to be defined and registered. I began implementing a simple syscall named sol_get_magic_number() that returns the number 2002. There are better ways of doing this - mine is not systematic. I'm sharing how I made changes to the code. I checked out to a new branch named custom-syscall in locally cloned agave and solana-sdk repos.
// CUSTOM SYSCALL
define_syscall!(fn sol_get_magic_number() -> u64);
// CUSTOM SYSCALLYou can find the sol_remaining_compute_units name near my code changes.
Register the syscall:
// CUSTOM SYSCALL: Accessing the magic number
register_feature_gated_function!(
result,
remaining_compute_units_syscall_enabled,
"sol_get_magic_number",
SyscallGetMagicNumnberSysvar::vm
)?;Define the syscall logic:
// custom sysvar: just returns a number
declare_builtin_function!(
/// Get a magic-number sysvar
SyscallGetMagicNumnberSysvar,
fn rust(
_invoke_context: &mut InvokeContext,
_arg1: u64,
_arg2: u64,
_arg3: u64,
_arg4: u64,
_arg5: u64,
_memory_mapping: &mut MemoryMapping,
) -> Result<u64, Error> {
Ok(2002)
}
);Since sol_remaining_compute_units can be imported to programs from use solana_program::compute_units::sol_remaining_compute_units, I had to find where the code makes it importable. Like before, I searched for the sol_remaining_compute_units keyword, which led me to solana-sdk/program (the solana_program crate).
Add to definitions.rs:
pub use solana_define_syscall::definitions::{
sol_alt_bn128_compression, sol_alt_bn128_group_op, sol_big_mod_exp, sol_blake3,
sol_curve_group_op, sol_curve_multiscalar_mul, sol_curve_pairing_map, sol_curve_validate_point,
sol_get_clock_sysvar, sol_get_epoch_rewards_sysvar, sol_get_epoch_schedule_sysvar,
sol_get_epoch_stake, sol_get_fees_sysvar, sol_get_last_restart_slot, sol_get_magic_number, // <-- HERE!!!
sol_get_rent_sysvar, sol_get_sysvar, sol_keccak256, sol_remaining_compute_units,
};Create a new file named magic_number.rs in solana-sdk/program/src:
/// Return the magic number
#[inline]
pub fn sol_get_magic_number() -> u64 {
#[cfg(target_os = "solana")]
unsafe {
crate::syscalls::sol_get_magic_number()
}
#[cfg(not(target_os = "solana"))]
{
crate::program_stubs::sol_get_magic_number()
}
}Add to solana-sdk/program/lib.rs:
pub mod magic_number;Add to solana-sdk/sysvar/program_stubs.rs:
fn sol_get_magic_number(&self) -> u64 {
sol_log("MAGIC NUMBER DEFAULT TO 0");
0
}pub fn sol_get_magic_number() -> u64 {
SYSCALL_STUBS.read().unwrap().sol_get_magic_number()
}All code changes are complete. Now I'm adding these dependencies to a sample Solana program. I used a program in the repo to call this syscall. Here you have the sol_remaining_compute_units and the custom sol_get_magic_number:
use solana_program::{
account_info::AccountInfo, compute_units::sol_remaining_compute_units, declare_id, entrypoint,
entrypoint::ProgramResult, magic_number::sol_get_magic_number, msg,
program_error::ProgramError, pubkey::Pubkey,
};
#[cfg(test)]
mod tests;
declare_id!("r8p3kwsDxPyTu1KyacFxJcP5b98GRn9wocBUsTToWTd");
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
_accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
if program_id.ne(&crate::ID) {
return Err(ProgramError::IncorrectProgramId);
}
msg!(
"sol_remaining_compute_units {:?}",
sol_remaining_compute_units()
);
msg!("sol_get_magic_number {:?}", sol_get_magic_number()); // CUSTOM ONE
Ok(())
}Cargo.toml looks like this. Initially, I tried to add dependencies from my forked GitHub repo, but I got more errors. So I added the dependencies by local path like this:
[package]
name = "custom-syscall"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.98"
solana-program = {path="/Users/arjunc/Documents/solana/svm/solana-sdk/program", version="2.3.0"}
solana-sysvar-id = {path="/Users/arjunc/Documents/solana/svm/solana-sdk/sysvar-id", version="2.2.1"}
tokio = "1.46.1"
[dev-dependencies]
mollusk-svm = "0.4.1"
solana-sdk = "2.3.1"
solana-client = "2.3.5"
[lib]
crate-type = ["cdylib", "lib"]cargo build-sbf was giving versioning issues like this:
error[E0277]: the trait bound `StakeHistory: SysvarId` is not satisfied
--> src/stake_history.rs:61:17
|
61 | impl Sysvar for StakeHistory {
| ^^^^^^^^^^^^ the trait `SysvarId` is not implemented for `StakeHistory`
|
This was fixed by changing stake and system dependencies in Cargo.toml of local solana-sdk:
solana-stake-interface = { path="/Users/arjunc/Documents/solana/svm/stake/interface", version = "1.2.0" }
solana-system-interface = { path="/Users/arjunc/Documents/solana/svm/system/interface", version="1.0"}Also changed one line in stake/interface/Cargo.toml to:
solana-sysvar-id = { path="/Users/arjunc/Documents/solana/svm/solana-sdk/sysvar-id" , version="2.2.1"}I haven't pushed this because it's a single line change.
Final TOML changes are to agave:
solana-define-syscall = { path = "/Users/arjunc/Documents/solana/svm/solana-sdk/define-syscall"}This is another error you might get while building the program
error[E0451]: field `start` of struct `BumpAllocator` is private
--> src/lib.rs:12:1
|
12 | entrypoint!(process_instruction);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ private field
|
Fix in solana-sdk/program-entrypoint/src/lib.rs
pub struct BumpAllocator {
pub start: usize,
pub len: usize,
}These are the code changes. Now let's run the local validator, build, deploy, and send a transaction to the program.
To build the local validator, run this from the agave directory. Mine is a MacBook Air M1, 8GB 2020 model. On some laptops this won't work:
cargo run -p agave-validator --bin solana-test-validatorIn the program's directory, run:
cargo build-sbf-
Set Solana config to localhost:
solana config set -ul -
Run this deploy command from the agave repo:
cargo run -p solana-cli -- program deploy ~/Documents/solana/svm/custom-syscall/target/deploy/custom_syscall.so --program-id ~/Documents/solana/svm/custom-syscall/target/deploy/custom_syscall-keypair.json
If you run
solana program deployfrom your program's repo, it won't work because the installed CLI doesn't know about the newly added syscall. It will return this error:Error: ELF error: ELF error: Unresolved symbol (sol_get_magic_number) at instruction #187 (ELF file offset 0x5d8)
Now run the test code to send a transaction to the program from the program's repo:
$ cargo test
Finished `test` profile [unoptimized + debuginfo] target(s) in 3.04s
Running unittests src/lib.rs (target/debug/deps/custom_syscall-6e5586ec56b9774c)
running 2 tests
test test_id ... ok
local validator: r8p3kwsDxPyTu1KyacFxJcP5b98GRn9wocBUsTToWTd
Transaction signature: 2ArWbUJAGE9xU4ED3S2m367cfyCTsSVcshTc9ENV2GDWdS85C1PX7Njobdc5puT53ggYduusiCuigCZFYHTx59co
test tests::custom_syscall_localvalidator ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.94s
Doc-tests custom_syscall
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sTo see the logged message, add the local RPC URL to https://explorer.solana.com/'s custom URL. I tried it in Brave and it didn't work; for me it worked in Chrome. Search the transaction hash, and the magic was displayed in the logs.
The forked repos with the above mentioned changes. Changes are in custom-syscall branch
