Skip to content

4rjunc/custom-syscall

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Custom Syscall in Agave Validator

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 2002

My first step when building on complex tech is to achieve simple working code and build complex functionality after it.

Setup

Clone these Anza repositories:

Research

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.

Implementation Steps

1. Define syscall in solana-sdk/define-syscall

// CUSTOM SYSCALL
define_syscall!(fn sol_get_magic_number() -> u64);
// CUSTOM SYSCALL

Link to code

2. Register syscall in agave repo agave/programs/bpf_loader

You 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
)?;

Link to code

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)
    }
);

Link to code

3. Make syscall importable in solana-sdk repo

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,
};

Link to code

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()
    }
}

Link to code

Add to solana-sdk/program/lib.rs:

pub mod magic_number;

Link to code

Add to solana-sdk/sysvar/program_stubs.rs:

fn sol_get_magic_number(&self) -> u64 {
    sol_log("MAGIC NUMBER DEFAULT TO 0");
    0
}

Link to code

pub fn sol_get_magic_number() -> u64 {
    SYSCALL_STUBS.read().unwrap().sol_get_magic_number()
}

Link to code

Sample Program Usage

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(())
}

Dependency Configuration

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"]

Fixing Build Issues

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"}

Link to code

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"}

Link to code

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,
}

Link to code

Running the Implementation

These are the code changes. Now let's run the local validator, build, deploy, and send a transaction to the program.

Build Local Validator

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-validator

Build the Program

In the program's directory, run:

cargo build-sbf

Deploy the Program

  1. Set Solana config to localhost:

    solana config set -ul
  2. 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 deploy from 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)

Test the Program

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.00s

To 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.

Logged

The forked repos with the above mentioned changes. Changes are in custom-syscall branch

Thank you!

twitter github

About

how to implement custom syscall on solana validator ?

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages