Skip to main content

Command Palette

Search for a command to run...

SVM Series 03: Compute Budget & Syscalls

Updated
6 min read
4
<[Open Source, Rust, Decentralized Infrastructure]>::earth()

The SVM requires compute budget limits and syscalls that programs can call to perform certain tasks.

Compute Budget

What is the Compute Budget & Compute Budget Program?

The compute budget defines the maximum compute units (CU) a transaction can consume to prevent resource abuse on the Solana network. The default CUs are 200,000 with a maximum limit of 1.4 million CUs. During program execution, the SVM breaks the transaction logic into opcodes (BPF instructions) where the VM consumes 1 CU per opcode. Examples of operations that are converted to opcodes are program syscalls, signature verification, locking instructions during transaction sanitization, etc. The compute budget program ComputeBudget111111111111111111111111111111 handles requesting more CUs for program execution if the default CUs are not enough.

To use the Compute Budget in the SVM API

cargo add solana-compute-budget
fn main() {
    // Initialize `ComputeBudget` struct with default paramemters.
    // These parameters include:
    //    - compute unit limit (defaults to 1.4 million CUs)
    //    - Maximum nesting of instructions that can happen during a transaction (defaults to 5)
    //    - Maximum cross-program invocation and instructions per transaction (defaults to 64)
    //    - Maximum number of slices hashed per syscall (defaults to 20,000)
    //    - Maximum SBF to BPF call nesting that can happen within a program (defaults to 64 SBF to BPF to BPF calls)
    //    - Size of a stack frame in bytes matching the size specified in the LLVM SBF backend (defaults to 4096)
    //    - Maximum cross-program invocation instruction size (default to IPv6 Min MTU size of 1280)
    //    - Length of the heap memory region used for program heap 
    //      (defaults to solana_program_entrypoint::HEAP_LENGTH of 32768 = 1024 * 32) 
    //    - Loads the default execution costs defined by `solana_program_runtime::execution_budget::SVMTransactionExecutionCost` struct
    //      which is defines cost paramemters for operations like loggin, CPI, signature verification and more. 
    let compute_budget_config = ComputeBudget::default();    
}

The SVMTransactionExecutionCost source code parameters are defined at Solana Program Runtime Modeule #L90.

Syscalls

What are syscalls?

In SVM, a syscall (short for system call) is a low-level function provided by the Solana VM that programs can invoke to interact with the VM environment. Syscalls are predefined entrypoints into the runtime that ensure only specific actions are allowed during program execution. These operations that are not part of the program itself but are offered by the VM as trusted privileged operations. Examples of a syscall is verifying Ed25519 signatures or logging information.

Configuration parameters used in syscalls parameters

Defining syscalls in SVM requires some configuration provided by the solana_sbpf::vm::Config. This configures VM parameters like maximum call depth, stack frame size, copying read-only data, tracing instructions, which SBF versions are allowed and more.

The solana-sbpf Rust crate is used to define syscalls and their configuration parameters. An example of this:

# add `solana-program` to your dependencies
# it contains `solana-sbpf` crate used to configure the VM
cargo add solana-program-runtime 

cargo add solana-bpf-loader-program # Contains the syscalls source code to configure the VM

In the src/main.rs file part of the Compute Budget code above.

use solana_program_runtime::{
    // `InvokeContext` is used to configure the entire exeuction pipeline
    invoke_context::InvokeContext,
    solana_sbpf::{
        program::BuiltinProgram,
        // Rename from `Config` to `SbpfVmConfig` to avoid conflicts with other deps with structs called `Config`
        vm::Config as SbpfVmConfig,
    },
};

    // ... previous code from above

    // Defining syscalls requires some configuration using `solana_sbpf::vm::Config`.
    // This configures maximum call depth, stack frame size, copying read only data, tracing instructions,
    // allowed SBF versions, sanitizing user provided values and many more
    let vm_config = SbpfVmConfig {
        // configure maximum call depth using that of our compute budget `compute_budget_config`
        max_call_depth: compute_budget_config.max_call_depth,
        // configure stack frame size using that of our compute budget `compute_budget_config`
        stack_frame_size: compute_budget_config.stack_frame_size,
        // Enable instruction tracing
        enable_instruction_tracing: true,
        // enable symbol and section labels for BPF (disabled by default)
        enable_symbol_and_section_labels: true,
        // Reject ELF files containing issues that the verifier did not catch before (up to v0.2.21). Disabled by default
        reject_broken_elfs: true,
        // Avoid copying read only sections when possible. Enabled by default
        optimize_rodata: false,
        // Use all other default parameters
        ..Default::default()
    };

We can now specify the syscalls the compiled program can call during execution using solana_sbf::program::BuiltinProgram Rust crate. The SVM configuration defined above as vm_config is used to configure the VM. The BuiltinProgram takes a generic parameter, in our case InvokeContext which defines the main pipeline from runtime to program execution.


    // The SVM configuration `vm_config` defined above is used  to configure the VM.
    // The `BuiltinProgram` takes a generic parameter, in our case the generic parameter will be
    // `solana_program_runtime::invoke_context::InvokeContext`
    let mut loader: BuiltinProgram<InvokeContext<'_>> = BuiltinProgram::new_loader(vm_config);

To register a syscall we use the register_function() method on BuiltinProgram<InvokeContext<'_>>. This function expects the name of the syscall and the syscall itself defined in the solana_bpf_loader_program::syscalls module. For example the msg! macro used in Solana programs to log messages is dependent on the sol_log() function which calls the sol_log_ syscall in the VM.

// How the `msg!()` macro is defined in the source code
#[cfg(feature = "std")]
#[macro_export]
macro_rules! msg {
    ($msg:expr) => {
        $crate::sol_log($msg)
    };

    // The macro calls `sol_log()` function within the same crate and passes a message
    // formatted message using Rust `format!()` macro 
    ($($arg:tt)*) => ($crate::sol_log(&format!($($arg)*))); 
}

// How the `sol_log()` function calls the sycall to show the formatted message
/// Print a string to the log.
#[inline]
pub fn sol_log(message: &str) {
    #[cfg(target_os = "solana")] // Only available on the SVM
    unsafe {
        // syscall is defined as `sol_log_` in the VM
        syscalls::sol_log_(message.as_ptr(), message.len() as u64);
    }

   // ...
}

We can define our own sol_log_ syscall in the VM similar to the above msg!() implementation using the loader variable we defined earlier.

// Import the `SyscallLog` from the `solana_bpf_loader_program::syscalls` module.
use solana_bpf_loader_program::syscalls::SyscallLog;

    loader
        // call this method and pass in the syscall function name in our case `sol_log_` 
        // and `SyscallLog::vm` which provides `vm()` method which is the VM interface that accepts parameters for execution.
        .register_function("sol_log_", SyscallLog::vm)
        .expect("Registration of `sol_log_` syscall failed!"); //Handle the error

These syscalls have a vm() method that provides the VM interface and a rust() method that provides a Rust interface.

We have learnt how to configure the VM and add syscalls. Explore the syscalls of the solana-bpf-loader-program Rust crate to learn other syscalls like SyscallLogPubkey, SyscallGetClockSysvar and many more.

The source code for this article can be found at - https://github.com/448-OG/SVM-Series/tree/master/syscalls

< Previous Article: SVM Series 02: SVM Feature Activation & Management

Next Article: SVM Series 04: TransactionBatchProcessor >

Resources

  1. Cost model source code
    https://github.com/anza-xyz/agave/blob/d5a84daebd2a7225684aa3f722b330e9d5381e76/cost-model/src/transaction_cost.rs#L121

  2. Compute budget source code
    https://github.com/anza-xyz/agave/blob/d5a84daebd2a7225684aa3f722b330e9d5381e76/compute-budget/src/compute_budget.rs#L22

  3. Bytecode / ISA
    https://github.com/anza-xyz/sbpf/blob/main/doc/bytecode.md

  4. How does the Solana BPF_VM calculate how many Compute units a program consumes?
    https://solana.stackexchange.com/questions/15119/how-does-the-solana-bpf-vm-calculate-how-many-compute-units-a-program-consumes
    https://github.com/anza-xyz/agave/blob/d5a84daebd2a7225684aa3f722b330e9d5381e76/program-runtime/src/invoke_context.rs#L529
    https://github.com/solana-labs/rbpf/blob/4dc039f4ee7409838c7f230558aebf6869c32db9/src/program.rs#L331
    https://github.com/anza-xyz/agave/blob/d5a84daebd2a7225684aa3f722b330e9d5381e76/programs/bpf_loader/src/lib.rs#L1405
    https://github.com/solana-labs/rbpf/blob/4dc039f4ee7409838c7f230558aebf6869c32db9/src/vm.rs#L422

  5. BPF instructions all consume 1 CU in the interpreter (which must match the JIT results)
    https://github.com/solana-labs/rbpf/blob/f3758ecee89198433422f751beee7f0f52dbcd55/src/interpreter.rs#L162

  6. Solana sycalls module
    https://docs.rs/solana-bpf-loader-program/latest/solana_bpf_loader_program/syscalls/index.html