What is the Solana Virtual Machine (SVM)?
/Fundamentals

What is the Solana Virtual Machine (SVM)?

67 min read

Many thanks to Lostin, Alessandro, Brian, Brady, and Daniel Cumming for reviewing earlier versions of this work. 

Actionable Insights

  • The SVM encompasses the entire transaction execution stack, unlike the EVM, which refers unambiguously to a bytecode executor.
  • Requiring transactions to declare which accounts they’ll access before execution unlocks parallel execution across CPU cores and localized fee markets.
  • Rust source compiles through rustc to LLVM IR, then LLVM’s eBPF backend, specifically Solana’s fork sBPF, lowers it to sBPF bytecode. This means that any language with an LLVM frontend (e.g., C, C++, Zig) can be used to write Solana programs.
  • sBPF is Solana’s fork of Linux’s eBPF, which, for all intents and purposes, is effectively the same, with some historical additions that have been introduced. However, these additions are set to be rolled back. The only meaningful difference is that upstream eBPF functions have a maximum of 5 arguments, while Solana’s fork can have more.
  • Compiled sBPF bytecode is stored in ELF files containing sections for instructions and constants, as well as relocation tables. The linker resolves syscall references to deterministic 32-bit Murmur3 hashes and rewrites internal functions as relative jumps for portability. 
  • The BPF Loader Upgradeable uses a two-account model for in-place upgrades and deployments, while the upcoming Loader V4 streamlines this to a single account with optional compression. All bytecode is statically verified before being marked as executable.
  • Transactions contain an array of account addresses with read and write permissions, instructions, and signatures. This structured format allows for conflict detection and scheduling non-conflicting transactions in parallel.
  • The TPU’s Banking Stage is responsible for scheduling non-conflicting transactions in parallel. The Bank loads account state from AccountsDB. BPF Loaders provision isolated sBPF VMs with bounded memory regions and compute budgets. Successful state changes commit atomically, while failures revert completely.
  • The SVM ISA is the only formal specification—besides Alpenglow’s whitepaper, and the series of articles initially written by Toly, there is no single “SVM spec.” The runtime emerges from the interaction of the Bank, scheduler, BPF Loaders, and the sBPF VM itself.

Introduction

The Solana Virtual Machine (SVM) is one of the most misunderstood systems in blockchains today. Unlike the Ethereum Virtual Machine (EVM), which refers unambiguously to an opcode executor, the term SVM encompasses an entire transaction execution pipeline, from the Banking Stage scheduler to the sBPF bytecode interpreter itself. This ambiguity reflects Solana’s architectural difference: there is no traditional specification that defines “the SVM” in isolation. The only specification that comes close pertains to the Solana Virtual Machine Instruction Set Architecture (SVM ISA), which describes how sBPF bytecode must execute, but says nothing about the broader runtime.

This article aims to serve as a comprehensive reference for what the SVM is, how it works, and why it’s fundamentally different from the perspective of Anza’s Agave validator implementation. Exploration into how Firedancer’s client operates, as well as their custom virtual machine implementation that adheres to the SVM ISA, is outside the scope of this work.

We are interested in examining the actual codebase, rather than some abstract specification. We trace the complete execution pipeline—how Rust source code compiles through LLVM to sBPF bytecode, how programs are deployed and verified, how the runtime provisions isolated execution environments for parallel execution, and how transactions interact with deployed bytecode.

The first few sections contextualize the SVM’s ambiguity and give a high-level overview of how it works. The remainder of the article is intended for a more technical audience seeking a rigorous understanding of Solana’s execution layer.

A Contentious Definition

The term “Solana Virtual Machine” (SVM) has sparked contentious debate within the community, especially since the emergence of network extensions and other layer blockchains being built on top of Solana. Contention stems from the term’s scope: Is the SVM strictly the low-level sBPF interpreter, or does it encompass the full transaction execution stack? 

The narrow view treats the SVM as analogous to a traditional virtual machine (VM), such as the EVM’s opcode executor. More specifically, the eBPF-derived virtual machine (rBPF, now sBPF) that interprets and JIT-compiles bytecode. This perspective emphasizes the SVM as a sandboxed, register-based executor that handles instructions, such as ALU operations or Solana-specific system calls. Essentially, the SVM is inspired by Linux eBPF’s safety model, but is customized for blockchain infrastructure. This aligns with phrases like the SVM ISA (Instruction Set Architecture) in validator code, where the SVM is the VM layer alone.

The broad view defines the SVM as the entire transaction execution layer of a Solana validator. This includes more than just running bytecode; it also encompasses upstream components, such as the Banking Stage’s scheduler, compute unit budgeting, and state updates via the accounts database, colloquially known as AccountsDB. It’s the “runtime” that turns raw transactions into validated state changes.

The ambiguity arises because official Solana communications use the term “runtime” interchangeably with “SVM,” without a singular pinned definition. Anza has added much-needed clarity to the debate, explicitly validating it while promoting a pragmatic, action-oriented perspective grounded in engineering. Their framing of the SVM as the Bank-driven runtime that provisions the eBPF VM offers a much broader, pipeline-inclusive view that can be used to form a proper definition of the SVM.

This is formalized in Anza’s official SVM specification, which defines the SVM as “the components responsible for transaction execution,” packaged as a stand-alone library for validators, fraud proofs, sidecars, and more.

For our purposes, we can define the Solana Virtual Machine as:

The decoupled runtime interface and transaction processing pipeline within Solana validators, driven by the Bank component, that orchestrates parallel instruction and on-chain program execution, provisioning a customized eBPF-based virtual machine for secure bytecode interpretation, JIT compilation, and resource metering.

The SVM at a Glance

The Solana Virtual Machine (SVM) serves as the execution environment for processing transactions that interact with on-chain programs across the network. It’s the runtime layer where code meets state—the execution environment that transforms cryptographically signed transactions into validated state changes. 

To truly understand the SVM, we must first understand what a virtual machine means within the context of blockchains.

Virtual Machines

A virtual machine (VM) is software that virtualizes or emulates a computer system, providing an isolated execution environment that behaves like physical hardware. The concept emerged from IBM’s 1960s work on mainframe systems, which enabled multiple users to run different operating systems on the same physical machine. There are two main categories of VMs: system VMs and process VMs. The former provides a substitute for a real machine, whereas the latter is designed to execute programs in a platform-independent environment. For our purposes, we are interested in system virtual machines, and we’ll refer to them as “virtual machines” or simply “VMs” going forward.

Virtual machines solve several fundamental problems. For starters, they offer a level of abstraction for hardware. That is, programs written for a VM can run on any physical hardware that supports the VM, without needing the program to be rewritten. Java’s philosophy of “write once, run anywhere” exemplifies this, as Java bytecode runs identically on Windows, macOS, Linux, and other systems with a Java Virtual Machine (JVM) installed.

VMs also provide isolation and security guarantees. Each VM instance operates in a sandbox, meaning it cannot access the host system’s resources or other VMs unless explicitly permitted. So, if a program crashes or contains malicious code, the damage is contained to that VM instance. This isolation principle is why cloud providers such as Google Cloud and AWS use VMs to separate customer workloads.

VMs also offer predictable outputs. This means that VMs provide a controlled environment where the same input always produces the same output, regardless of the underlying hardware. This predictability is crucial for debugging, testing, and achieving consensus among distributed systems.

VMs can also be extremely performant. Modern VMs use Just-In-Time (JIT) compilation to minimize performance overhead. JIT compilation translates VM bytecode into native machine code at runtime to achieve near-native performance, while maintaining portability and the aforementioned security guarantees.

Virtual Machines in Blockchains

Blockchains adapted the concept of the VM to solve a unique challenge: how do thousands of independent computers around the world execute untrusted code and arrive at identical results? VMs serve as the deterministic runtime environment that executes smart contracts (i.e., programs on Solana) and manages the network’s state (i.e., the current status of all accounts, balances, and other data across the network).

When a transaction is submitted to a blockchain, the VM is responsible for:

  • Loading the necessary account data from storage.
  • Executing the program bytecode specified in the transaction.
  • Metering resource consumption to prevent infinite loops or denial-of-service (DoS) attacks.
  • Validating that all state changes follow the network’s predefined consensus rules.
  • Committing the updated state back to permanent storage (i.e., the ledger).

The specific rules for how state transitions occur are defined by the VM’s instruction set architecture and runtime constraints.

How the SVM Works

The SVM is a pipeline of subsystems that work together to execute transactions safely and efficiently. The Bank orchestrates execution for a specific slot, managing account state, enforcing consensus rules, and coordinating between the Banking Stage and persistent storage (i.e., AccountsDB). Each Bank represents the state of all accounts at a specific slot and progresses through three lifecycles: active (i.e., open to new transactions), frozen (i.e., not open to new transactions as the slot is complete), and rooted (i.e., part of the canonical chain).

The Banking Stage is where transaction execution happens within a validator’s Transaction Processing Unit (TPU). It receives verified transactions from the SigVerify stage, buffers them, and schedules them for parallel execution using conflict detection on account locks. Worker threads in the Banking Stage process batches of non-conflicting transactions, calling into the Bank’s execution methods to load accounts, provision sBPF VM instances for each instruction, execute program bytecode, and collect results. The Banking Stage continues to process batches of non-conflicting transactions until the Bank is frozen at the slot boundary. Note that batches are distinct from entries, which are the recorded units of transactions written to the ledger for replication and consensus.

The BPF Loaders manage the program lifecycle: deployment, JIT compilation, upgrades, and execution. When an instruction targets a given program, an sBPF VM is provisioned with its own memory regions and compute budget, and execution is handed off to the program’s bytecode.

The sBPF VM is the sandboxed execution environment where program bytecode actually runs. It is derived from Linux’s eBPF and uses a register-based architecture with 11 general-purpose registers. The VM enforces memory isolation through five distinct memory regions, each with explicit bounds and permissions. The VM also meters compute unit consumption to prevent runaway execution and dispatches system calls for privileged operations such as cryptography, logging, or Cross-Program Invocations (CPIs).

The AccountsDB is the persistent state layer where all account data lives. Account state is loaded in before execution, leveraging caches to avoid repeated disk reads for frequently accessed accounts. After successful execution, updates are committed back to AccountsDB. If execution fails, all state changes are reverted atomically.

Together, these components form the SVM, a decoupled, reusable execution engine.

What Makes the SVM Special: Upfront Account Declarations

The defining architectural decision of the SVM is that all transactions must explicitly declare which accounts they will read from and write to before execution begins. This simple requirement, baked into the transaction format itself, unlocks two transformative capabilities that set Solana apart: parallel execution and localized fee markets.

Parallel Execution (Sealevel)

Unlike the Ethereum Virtual Machine (EVM), which processes transactions sequentially—one at a time, waiting for each one to complete before moving onto the next—the SVM enables horizontal scaling by executing multiple transactions simultaneously across multiple CPU cores. This parallelization is possible because all Solana transactions explicitly declare which accounts they will read from and write to before execution begins. 

Declaring which accounts a transaction will read and write to allows for the runtime to analyze account dependencies to detect conflicts and schedule non-conflicting transactions:

  • Transactions that touch completely different accounts can run in parallel with zero coordination overhead.
  • Transactions that only read from the same accounts can also run in parallel since reads do not conflict.
  • Transactions that attempt to write to the same accounts run sequentially to prevent race conditions and ensure state consistency. 

Local Fee Markets

Because the runtime knows exactly which accounts each transaction will access before execution, fees can be localized to specific accounts rather than competing globally across the entire network. This is a concept known as local fee markets

On Ethereum, and other EVM chains, every transaction competes in a single global fee market—sending ETH to a friend, minting an NFT, or trading on Uniswap all bid against each other for the same blockspace. A surge in one domain drives up fees for everyone, regardless of whether they are trying to do something completely different. 

On Solana, only transactions that access the same accounts compete with each other. A user transferring SOL between two accounts shouldn’t have to worry about a popular NFT mint happening simultaneously. A transaction’s priority fee is determined solely by account contention. This localization is why Solana transactions can remain inexpensive, even during periods of high activity.

For instance, on October 10th, the crypto market experienced its largest-ever liquidation event. Despite the record-breaking surge in activity, Solana transactions remained relatively inexpensive, with the median transaction fee hitting $0.007, average fees briefly hitting $0.10, and the top 1% of transactions peaking at just above $1.00. In that same timeframe, both Ethereum and Arbitrum had their median fees spike above $100, while Base’s fees peaked at over $3.

A Paradigm Shift

Aspect

EVM

SVM

Architecture

Stack-based VM

Register-based VM (eBPF-derived)

Execution

Sequential

Parallel (conflict detection)

Fee Market

Global

Localized (per-account contention)

Account Declarations

Not required upfront

Required upfront

ISA

~140 opcodes, stack operations

~100 opcodes, RISC-like registers

JIT Compilation

Optional (client-dependent)

Standard (native perf)

State Model

Contract storage fees

Flat account database

Languages

Solidity/Vyper → EVM bytecode

Rust/C/C++ → LLVM → sBPF

The SVM represents a fundamentally different approach to blockchain execution. Bitcoin introduced programmable money. Ethereum introduced general-purpose smart contracts and arbitrary on-chain execution. However, both are constrained by sequential execution and global fee markets—architectural decisions that place fundamental limitations on both throughput and cost.

The SVM diverges from traditional constraints, offering a network that can handle high throughput without sacrificing programmability or forcing users into prohibitively high fee auctions. The decision to require account declarations upfront is simple yet powerful, as it enables parallel execution across CPU cores and localizes fees to account-level markets.

Of course, these are not the only optimizations that Solana offers compared to other blockchains. Solana’s mantra of Increase Bandwidth, Reduce Latency, and hyperobsession with realizing the dream of Internet Capital Markets have resulted in various performance optimizations, design choices, and implementations to cultivate a high-throughput network.

The remainder of this article explores exactly how this works—how Rust source code gets compiled to bytecode, how that bytecode is deployed and verified, and how the runtime provisions isolated execution environments to safely run thousands of programs in parallel while maintaining strict determinism and security guarantees. 

From Rust Source to sBPF Bytecode: The Compilation Pipeline

Rust

Rust is the lingua franca of Solana program development. Frameworks like Anchor provide a robust, opinionated approach for developers to build secure programs efficiently. solana_program is intended to be the base library for all on-chain programs. Recently, Pinocchio, a highly optimized, zero-dependency library, has become the preferred option for developers looking to build native Solana programs. 

Regardless of the framework or library used, all programs have an entrypoint that the runtime will call when the program is invoked. solana_program’s macro entrypoint emits the standard boilerplate necessary to begin program execution. That is, deserializing inputs, setting up a global allocator, and a panic handler. Pinocchio exports entrypoint macros that function similarly but decouple the entrypoint from the heap allocator and panic handler setup, giving developers more optionality. 

Basic Program Anatomy 

A program is a type of account capable of running code. More specifically, a program is an executable account that stores a blob of sBPF bytecode in an account owned by the BPF Loader with a unique public key. Programs are stateless by design: all persistent data lives in separate accounts, which programs can read from or write to when invoked.

The SVM expects all programs to have a specific skeleton—an entrypoint that accepts three inputs:

  • Program ID: The address of the program itself, used for self-referential checks (e.g., ownership).
  • Accounts: An array of account metadata (i.e., pubkeys, lamport balances, data buffers, owners, and flags). These are the “state” a program needs to read and write to.
  • Instruction Data: A byte slice of arbitrary data from the transaction.

A program is expected to process these inputs via its entrypoint, mutate the relevant writable accounts, emit logs or events, and then return a success status indicating whether it was able to do all of this successfully. This boils down to a process_instruction function.

A simple program written in Rust using the solana_program crate looks like:

simple_rust_program.rs
use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    pubkey::Pubkey,
};

entrypoint!(process_instruction);

pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, Solana!");

    Ok(())
}

Under the hood, this is all defined by the SVM’s Application Binary Interface (ABI), which we’ll explore later.

The Rust Compiler and LLVM IR

Rust, like many other programming languages, is a second-order abstraction built on top of assembly. It is designed for humans to write safe, concurrent, and readable code without micromanaging every minute hardware interaction. However, computers don’t speak Rust, or any high-level language for that matter. 

Computers understand machine code—binary instructions tailored to a specific architecture or virtual machine. All programs are eventually converted into binary code, and this translation is ultimately done by computers. Compilation is a multi-step translation process that strips away high-level abstractions, optimizes for efficiency, and outputs executable bytecode.

rustc is Rust’s official compiler. Most developers typically do not interact with rustc directly; instead, they invoke it via Cargo, Rust’s package manager. Nevertheless, rustc takes Rust source code through three primary stages before emitting executable bytecode. Each stage strips away abstraction, enforces safety, and prepares the code for the next transformation:

  • Parsing and Expansion
  • MIR (Mid-level Intermediate Representation)
  • LLVM IR (Low Level Virtual Machine Intermediate Representation)

Parsing and Expansion

The compiler reads a given Rust file, denoted by the .rs extension, as plain text. It searches for specific tokens (e.g., use, fn, None, impl, &[u8]) in a process known as lexing. Lexical tokenization is the process of converting text into meaningful lexical tokens that belong to a given category (e.g., identifiers, operators, separators, literals, keywords). 

rustc takes these lexical tokens and converts them into a data structure known as an Abstract Syntax Tree (AST). This tree-like structure represents the nested, hierarchical structure of the Rust source code. That is, functions contain blocks, blocks contain expressions, expressions contain operators, and so forth. Although still at a high level, the AST provides a faithful representation of the source code and its underlying logic.

Once the AST is built, the compiler performs several key transformations:

  • Macro Expansion: Macros like entrypoint! and println! are expanded into raw Rust code such that all macros are converted into plain AST nodes.
  • Lowering: High-level shorthand syntax designed to make code more readable is rewritten into more primitive forms in a process referred to as lowering. For example, a for loop is transformed into a loop with manual iteration. The result of desugaring is High-Level Intermediate Representation (HIR).
  • Borrow Checking and Safety Analysis: Rust takes the HIR and performs type checking, trait resolution, and type inference. The result of this process is Typed High-Level Intermediate Representation (THIR).

At this stage, the compiler handles unsafe code. Unsafe code allows developers to perform operations that bypass Rust’s safety guarantees (e.g., dereferencing raw pointers, calling foreign functions, implementing unsafe traits). It serves as a deliberate escape hatch for low-level control, relaxing specific rules while still requiring the code to compile under Rust’s semantics. This is key for Solana programs, where unsafe code might be used sparingly for gains in performance-critical operations, such as zero-copy account deserialization (e.g., in Pinocchio’s wrapper struct for an Account).    

Unsafe code is first identified post-lexing within AST building, as the unsafe tokens are recognized and marked as special nodes. It is then handled post-AST expansion during type and borrow checking. The compiler ensures that unsafe operations are confined to unsafe contexts, flagging errors otherwise (e.g., “cannot dereference raw pointer outside unsafe”). The compiler does not check, for example, that unsafe code is not corrupting or mismanaging memory.

By the end of this stage, the expanded AST, now THIR, is a validated, desugared representation of the Rust source code.

MIR

The THIR is then lowered to the Mid-Level Intermediate Representation (MIR), a Rust-centric form that represents the source code as a simplified control-flow graph (CFG). All Rust-specific sugar and complex constructs (e.g., pattern matching, traits, closures) are expressed in terms of basic blocks, which include assignments and branches. These basic blocks are connected by jumps, or more specifically, Gotos, and branches, making it straightforward to reason about a program’s flow. 

MIR is not strictly necessary. The compiler could simply lower THIR directly into LLVM IR. However, MIR provides a Rust-aware layer that allows the compiler to enforce Rust-specific rules and perform optimizations before LLVM’s generic optimizations are applied. This makes MIR perfect for checks and transformations that are too high-level for LLVM but too low-level for THIR, including:

  • Borrow Checking: Initial semantic checks occur during type analysis on THIR. However, MIR’s simplified CFG enables full, precise borrow checking to enforce all ownership, borrowing, and lifetime rules.
  • Move and Drop Checking: The compiler ensures that all values are moved and dropped in accordance with Rust’s memory safety guarantees, preventing use-after-free errors.
  • Initialization Analysis: The compiler ensures all variables are initialized before use.
  • Inlining and Early Optimizations: The compiler can inline small functions, simplify arithmetic expressions, and eliminate unreachable code.

For example, earlier we invoked msg!(“Hello, Solana!”) in our sample Rust program. This is a macro defined in the solana-program crate that, for single expressions like a static string, expands during the macro expansion phase to directly call sol_log($msg), where $msg is the expression. The sol_log syscall takes a pointer to the string data and its length, logging it to the SVM’s output without the overhead of formatting. This might be simplified in MIR to:

Code
bb0: {
  _0 = const "Hello, Solana!"; // Constant string allocation
  _1 = len(_0); // Compute length
  sol_log(move _0, move _1); // Syscall invocation with explicit moves for ownership
  return = Ok(());
}

Here,

  • _0 = const “Hello, Solana!”;—MIR introduces temporaries (i.e., _0) for intermediate values. The string is treated as a constant slice, allocated in read-only data.
  • _1 = len(_0);—MIR exposes a simple length operation on the slice for potential constant folding.
  • sol_log(move _0, move _1);—The syscall invocation, which translates to an sBPF instruction sequence that loads registers and calls the syscall ID. We’ll break down exactly what this means in later sections, but the important part to note is that these moves connect to Rust’s ownership semantics and enforce them at compilation.
  • Return = Ok(());—Ends the block with a terminator, signaling a success to the SVM.

MIR’s focus on Rust semantics makes it the ideal place to spot inefficiencies and debug CU-expensive patterns. For instance, if your log contains dynamic strings, MIR may identify additional allocations or loops that could be optimized. Developers can use the command cargo rustc -- -Z dump-mir=all to dump MIR.

MIR ensures that the code is semantically sound, optimized, and stripped of all Rust-specific rules before finally lowering it into LLVM IR.

Note that this is generally referred to as the Code Generation phase. This is not necessarily LLVM. However, LLVM is common and what most people think of when it comes to Rust code generation. The Rust compiler also ships with GCC and Cranelift backends that emit GIMPLE and CLIF, respectively. We focus on LLVM IR within the context of Solana, but not that this is not always the case with Rust in general.

LLVM IR

LLVM, originally meaning "Low Level Virtual Machine," refers to a modular compiler framework. Instead of having a single compiler, LLVM is a toolkit of reusable components for building compilers, optimizers, and code generators. Many languages (e.g., Rust, C, C++, Julia, Swift, Brainfuck, Zig) use LLVM to leverage its ability to target diverse architectures from x86 CPUs to virtual ISAs. 

rustc translates MIR into LLVM IR (Low Level Virtual Machine Intermediate Representation)—the bridge between Rust’s semantics and the eventual bytecode that gets deployed on Solana. It is much closer to machine code, with explicit memory allocations (i.e., alloca), stores, loads, and function calls. It has no notion of ownership, lifetimes, or traits, as those Rust abstractions have already been expanded to no longer exist—the guarantees of the earlier rounds are preserved going forward.

Various optimizations are applied at this stage, including:

  • Constant Folding (i.e., evaluating constants at compile time).
  • Inlining (i.e., replacing a call with the function’s body).
  • Dead Code Elimination (i.e., removing instructions that don’t affect results).
  • Loop Unrolling and Vectorization (i.e., rewriting loops to execute faster).

Thus, LLVM provides us with:

  • LLVM IR: A portable, assembly-like intermediate format.
  • Optimization Passes: To create LLVM IR, LLVM uses Static Single Assignment (SSA), which ensures that each variable is assigned exactly once, enabling optimizations such as inlining and eliminating dead code.
  • Code Generators: Targets that lower LLVM IR into actual machine code (i.e., x86_64, ARM, WebAssembly, eBPF).

Rust programs would typically be compiled for hardware targets such as x86_64 or ARM. However, Solana programs do not run directly on hardware. Instead, they run inside the Solana Virtual Machine. The LLVM backend, therefore, lowers LLVM IR into BPF bytecode, which, on Solana, becomes sBPF bytecode (i.e., a fork of eBPF that removes non-deterministic features and introduces Solana-specific syscalls).

Note that while Rust is the lingua franca of Solana program development, any language that can target the LLVM’s BPF backend (e.g., C, Nim, Swift, Zig) can be used.

eBPF

LLVM IR is lowered to eBPF, the register-based ISA that forms the foundation of Solana’s runtime. eBPF (Extended Berkeley Packet Filter) originated from the Berkeley Packet Filter (BPF), developed in 1992 by Steven McCanne and Van Jacobson while at the Lawrence Berkeley Laboratory for Berkeley Software Distribution (BSD) Unix systems. Essentially, BPF is a network tap and packet filter that enables network packets to be captured and filtered at the operating system level without copying data, leveraging qualifiers

eBPF has since evolved (i.e., been extended) into a general-purpose, sandboxed VM within the Linux kernel. Its unlock is analogous to what JavaScript unlocked for web development—it is a safe script engine for kernels. eBPF enables developers to run small, verified programs directly within the Linux kernel with a constrained instruction set for tasks such as performance monitoring, observability, security, and networking.

This is important because developers can get:

  • Sandboxed Execution: eBPF programs run in a restricted virtual machine inside the kernel, meaning they can’t crash or corrupt kernel memory.
  • Safety Guarantees: eBPF bytecode is statically verified before loading to ensure that no invalid memory access, out-of-bounds jumps, or other privileged operations occur, providing safety without runtime overhead.
  • Efficiency: eBPF is register-based (instead of stack-based like the EVM) and can be JIT-compiled into machine code for near-native speeds due to its lightweight design (i.e., no full OS overhead).
  • Flexibility: eBPF exposes system calls, also known as syscalls, which are essentially hooks into kernel features. Syscalls can be extended with new capabilities without requiring the instruction set to be redesigned.

Solana required a deterministic, safe, and high-performance VM to run untrusted programs across its entire validator set. eBPF offers a proven safety model, a portable and efficient ISA designed to run thousands of lightweight programs, and JIT support for better performance. Thus, rather than invent a brand new VM, Solana forked eBPF and created sBPF.

sBPF 

Initially, Solana Labs forked rBPF by Quentin Monnet to create Solana’s version of rBPF, ensuring that every validator would have a bytecode format that produces the exact same results for executing a given program and input. 

It was thought that a fork of eBPF was needed because Solana’s consensus required deterministic execution and bounded resource use. While eBPF itself is deterministic, Solana needed additional guarantees and blockchain-specific functionality:

  • Fixed instruction timings and costs.
  • User-space execution.
  • A deterministic runtime.

Notably, it is designed to run in userspace rather than the kernel, thereby avoiding the need for kernel privileges or modifications. This enables deployment across various OS environments without requiring root access or custom kernel modules. Userspace is a practical choice for Solana—portability, testing, and easier deployment. Despite running in userspace, JIT still achieves near-native performance. Moreover, user-space execution also allows for testing and fuzzing to occur without kernel access.

rBPF is no longer in use. Instead, when Anza was created, they forked rBPF to create sBPF (Solana Berkeley Packet Filter). The rBPF GitHub repository, owned by Solana Labs, was archived on January 10, 2025.

The SVM ISA

The SVM ISA (Solana Virtual Machine Instruction Set Architecture) is the core specification that defines how Solana-compatible VMs (e.g., Agave’s sBPF or Firedancer’s reimplementation) must execute programs. It is not the VM itself, but rather the standard or contract, that ensures consistency and protocol compliance across various SVM implementations. It is the SVM ISA that imposes these safety and determinism constraints on eBPF, removing kernel-centric features while adding blockchain-specific functionality.

The ISA governs registers, instruction encoding, opcodes, classes, verification rules, panic conditions, and the Application Binary Interface (ABI). Any changes to the SVM ISA must be implemented via SIMDs to support the controlled evolution of this instruction set, ensuring deterministic execution across validators.

Registers

Registers are tiny storage slots inside the VM that hold numbers or addresses while instructions run, similar to variables or labeled boxes on a workbench. The SVM ISA defines a 64-bit register architecture with 11 general-purpose registers (R0-R10) and a hidden program counter. Registers are 64 bits wide for ints and addresses, allowing for the efficient handling of large values or pointers. R0 holds function return values, R1-R5 pass the first five function arguments like parameters, R6-R9 are callee-saved and persist across function calls, and R10 serves as a read-only frame pointer that marks the current stack frame. The hidden program counter tracks execution, indicating which instruction to execute next.

Instructions

An instruction is a single operation that the VM knows how to do, such as “add these two numbers” or “jump to this line of code.” Instructions have a RISC-like design with approximately 100 opcodes, versus the thousands in CISC architectures like x86, which is what makes verification fast and JIT compilation efficient.

Instructions are encoded as 64-bit values in Little Endian format with the structure:

  • opcode: 8 bits
  • dst_reg: 4 bits
  • src_reg: 4 bits
  • offset: 16 bits (signed)
  • immediate: 32 bits (signed)

opcode tells what to do, dst_reg tells where the result goes, src_reg tells where the input comes from, offset tells what memory offset to look at, and immediate is an extra constant that can be included in the instruction.

lddw, or load double word, is the only wide instruction that occupies two 64-bit slots to support full 64-bit immediate values. 

Instructions are categorized into classes, including memory operations, arithmetic or logic operations, conditional and unconditional branches, function calls and returns, and Endianness conversion.

Memory Regions

The ISA identifies five memory regions, each with explicit bounds (i.e., [addr, addr+len]) on where a program can read or write:

  • Program code: the compiled instructions themselves (read + execute).
  • Stack: a temporary workspace for functions (read + write, typically 4KB per frame).
  • Heap: dynamic memory that a program can request (read + write).
  • Input data: read-only bytes passed in with a transaction.
  • Read-only data: constants and immutable values.

Programs have a predefined virtual memory map such that program code starts at address 0x000000000 or 0x100000000, depending on the compilation version, stack frames start at 0x200000000, the heap starts at 0x300000000, and input data starts at 0x400000000.

The Verifier

The verifier performs static analysis before execution—it examines every possible code path without running the program—to ensure safety guarantees at load time, rather than at runtime. This includes checking:

  • No unknown or unsupported instructions.
  • All jump targets land on valid instruction boundaries, and backward jumps are handled
  • No unreachable code paths.
  • The function call depth limits are enforced.
  • Division or modulo by zero is statically rejected.
  • Programs have a maximum size limit and must fit within it.

While helpful, the verifier does not prevent a developer from introducing unexpected behavior. That is, developers are still able to introduce use-after-free and buffer overflow errors, for example, within a Solana program. 

Panic Conditions

Panic conditions are the list of runtime error cases defined by the SVM ISA. These include:

  • Invalid or unsupported instructions.
  • Division or modulo by zero.
  • Out of bounds memory access.
  • Invalid memory access for different memory regions (i.e., permission violation).
  • Stack overflow.
  • Call depth exceeded.
  • Exceeded the maximum number of allowed instructions.
  • The program returned an error code.

ABI

The Application Binary Interface (ABI) is the format contract between a Solana program and the SVM. While the earlier Basic Program Anatomy section demonstrated how this works in Rust (i.e., process_instruction with three inputs), the ABI specifies how those inputs and outputs are represented in memory, ensuring that every validator can execute programs deterministically.

At a high level, the ABI defines three things: entrypoint convention, calling conventions and registers, and the memory layout.

Every Solana program must expose an entrypoint function. The loader serializes program inputs into the VM’s memory space in a canonical order: the program ID, the accounts array, and the instruction data. The VM then passes pointers to these regions in the program’s entrypoint.

The first five registers (i.e., R1-R5) are reserved for entrypoint arguments, while the return register (i.e., R0) holds the program’s exit code. An exit code of zero is considered a success, whereas a nonzero value is a failure mapped to a specific InstructionError. This ensures that all programs return status codes in a consistent manner. Moreover, parameters beyond the first five are passed on the stack. R6-R9 follow a callee-saved convention, meaning functions must preserve these values if they use them.

Accounts and data are serialized into the VM’s linear memory as byte slices. Programs must deserialize them into higher-level Rust types (e.g., AccountInfo, Pubkey). The ABI enforces strict bounds so no program can access memory outside of its allocated regions.

Together, these rules make the ABI the “glue” that binds the high-level developer experience to the low-level ISA. It ensures that a simple Rust function signature compiles into the correct register usage, memory layout, and return codes so that every validator interprets a given program in the exact same way every time.

Syscalls

The ISA is intentionally minimal, having no built-in accounts or state. It also does not provide any higher-level functionality, such as logging, hashing, or cross-program calls, directly. Instead, system calls—special functions built into the VM to allow programs to interact with the outside world—are exposed. These calls are colloquially referred to as syscalls.

Syscalls can be thought of as APIs provided by the VM. Instead of every program having to reimplement specific cryptographic primitives or account logic, syscalls expose safe, standardized operations that are guaranteed to behave deterministically across all validators.

Popular syscall categories include:

  • Logging and Debugging (e.g., the sol_log syscall writes a UTF-8 string to program logs, and is used under the hood by msg!).
  • Cross-Program Invocation (CPI) (e.g., sol_invoke_signed allows a program to call another on-chain program, passing in accounts and instruction data, which is crucial for Solana’s composability).
  • Cryptography (e.g., the sol_sha256, sol_keccak256, and sol_ed25519_verify syscalls all add deterministic, fast cryptographic primitives without requiring developers to write their own implementations).
  • Memory and Account Utilities (i.e., syscalls that expose helpers for account data borrowing, reallocating memory, or working with program-owned heap allocations).
  • Compute Budget and Metering (i.e., every syscall consumes CUs, enforced by the runtime’s metering system).

Syscalls are invoked using a special CALL_IMM instruction with a unique hash identifier. When a program calls a syscall, the sBPF VM traps execution, looks up the hash in its syscall registry, and dispatches to the native implementation running in privileged runtime code. Syscalls execute outside the sandbox with access to runtime state, which is entirely different from how regular function calls within a program execute.

Syscalls follow the same ABI as regular functions, with the first five arguments being passed into registers R1 through R5 with the return value in R0. Each syscall has a fixed compute unit cost, ensuring deterministic resource consumption. For example, all calls to the secp256k1_recover syscall consume 25,000 CUs.

Syscalls form a controlled security boundary, as each one validates its inputs and checks the relevant permissions before performing any privileged operations. For example, a Cross-Program Invocation (CPI) syscalls verify the caller has the appropriate permissions regarding the accounts being passed.

Note that new syscalls can be added through feature gates without needing to modify the ISA itself. This enables Solana to expand its VM capabilities, including support for new cryptographic primitives, while maintaining backward compatibility with existing programs.  

Program Binary

At the end of compilation, all of the stages—from Rust source to LLVM IR, to eBPF, to sBPF and its adherence to the SVM ISA—produce a single output: a program binary. This binary is what actually gets deployed to Solana. 

ELF

Solana programs are compiled into Executable and Linkable Format (ELF) files, a standard binary format used across Unix-like systems. The ELF format acts as a container that packages everything the VM needs to execute a given program while maintaining platform independence.

An ELF file typically contains the following sections:

  • Bytecode Section: Contains the compiled sBPF instructions in the .text section.
  • Read-Only Data Section: Holds constants, static strings, and immutable values in a .rodata section.
  • BSS and Data Sections: Contains global or static mutable variables in the .bss and .data sections, respectively. Note that Solana does not allow mutable data. That is, the ELF can have .rodata, but not the BSS and data sections. 
  • Symbol and Relocation Tables: Define how function calls, syscalls, and memory references are resolved during loading in the .symtab and .strtab sections for symbols and .rel.dyn and .rela.dyn sections for relocation entries.

Each ELF file also includes a header describing the architecture, instruction width (i.e., 64-bit), endianness (i.e., Little Endian), and entrypoint address.

Linking and Relocation

The process of turning compiler output into a single, executable ELF file involves a final component: the linker. The linker is responsible for combining multiple compiled code units into one cohesive binary. It also resolves all symbolic references (i.e., placeholders) that were left unresolved by the compiler.
For example,

  • When a program calls a function such as sol_log, the compiler doesn’t know where that function lives in memory, so a placeholder is used.
  • The linker replaces this symbolic reference with the syscall’s unique hashed identifier (i.e., a deterministic 32-bit Murmur3 hash).
  • Similarly, calls between internal functions are rewritten as relative jumps to instruction offsets within the .text section.

This process of rewriting symbolic references into concrete addresses or hashed syscall IDs is called relocation. However, it’s worth noting that relocations are largely an artifact of how the initial tooling was built rather than a fundamental requirement. In fact, there are plans to remove relocations entirely in future versions of the toolchain to simplify the deployment process.

This relocation step is necessary to ensure that the same ELF binary runs identically across all validators since no absolute memory addresses or system-specific symbols are embedded. 

Moreover, the bytecode with these relocations already in place is cached in memory, so all future executions rely on the updated bytecode without needing to reprocess the relocations.

Once the linker produces a fully relocated ELF file, the program is ready for deployment. The final result is a binary that is:

  • Portable: It runs identically on any validator or SVM implementation.
  • Deterministic: It contains no non-deterministic syscalls or OS dependencies.
  • Self-contained: It carries all bytecode and metadata needed for execution.

How Bytecode is Uploaded to Solana

Once a Solana program is compiled and linked into a valid ELF file, the next step is to upload it to the blockchain so that it can be executed by validators. This process, known as program deployment, involves multiple components that work together: the BPF Loader, account models, bytecode verification, and state management.

The BPF Loader Program

The BPF Loader is a native program that validates, relocates, and marks ELF files as executable. Essentially, it manages the lifecycle of deployed programs—it processes instructions to initialize accounts, writes bytecode, deploys programs, and handles upgrades.

Solana has evolved through several iterations of loaders, each improving upon the previous version:

  • BPF Loader: The original loader for static, unupgradable programs, which is no longer supported
  • BPF Loader V2: A simplified loader with no management instructions.
  • BPF Loader Upgradeable: The current loader, which introduced program upgradability.
  • BPF Loader V4: The latest iteration with better deployment features, simplifying the current two-account model to a single-account model.

Deployment Architecture: Account Models

Current Account Model

The current loader uses a two-account architecture to separate program logic from program data. This results in two accounts for a given program: the Program account and the ProgramData account.

The Program account is a small account, approximately 36 bytes in size, that holds metadata and is marked as executable. It stores a reference to the ProgramData via UpgradeableLoaderState::Program { programdata_address }.

The ProgramData account is a larger account that stores the actual ELF bytecode along with deployment metadata (e.g., slot, upgrade authority address) via UpgradeableLoaderState::ProgramData.

The separation of the two accounts enables in-place upgrades. That is, the Program account remains at the same address while the ProgramData account’s bytecode can be replaced.

Future Account Model

Loader V4 seeks to streamline the deployment process with a single account model. The idea here is that the program account will directly store the metadata and bytecode, eliminating the need for a separate ProgramData account. It also gives developers the option of storing a zstd-compressed image to save on rent costs.

How Solana Programs are Deployed

The deployment process involves uploading a compiled ELF binary and having the BPF Loader verify, cache, and mark it as executable. The process differs slightly between the upgradeable loader and V4 due to the deployment architecture outlined in the previous section.

BPF Loader Upgradeable

The current deployment process with the upgradeable loader involves initializing a buffer account for staging the ELF bytecode. The deployer sends an InitializeBuffer instruction to the BPF Loader Upgradeable, which creates a new account owned by the loader, and sets the account state to UpgradeableLoaderState::Buffer { authority_address }, recording the address authorized to write to the buffer. 

The compiled ELF binary is uploaded to the buffer in chunks using the Write { offset, bytes } instruction. Each write instruction verifies the signer matches the buffer’s authority, checks the buffer is still mutable (i.e., not yet deployed), and writes bytes at the specified offset past the metadata header. Note that large programs require multiple Write instructions to upload the entire ELF file due to transaction size limits.

Once the buffer contains the complete ELF, the deployer sends a DeployWithMaxDataLen { max_data_len } instruction. This is the most complex step within the entire deployment process, as it orchestrates the actual deployment from account validation to state finalization.

The loader first validates all accounts in the deployment process and verifies:

  • The program account is uninitialized and rent-exempt.
  • The buffer contains valid data, and the authority who initialized the buffer has signed the transaction.
  • The max_data_len is large enough to accommodate the buffer’s data.
  • The total size doesn’t exceed the MAX_PERMITTED_DATA_LENGTH (i.e., 10MiB, or 10,485,760 bytes). 

The loader then creates the ProgramData account, deriving the address as a PDA using the program ID and loader ID. It then drains the buffer’s lamports back to the payer, as the buffer account is no longer needed after deployment. 

Additionally, it creates the ProgramData account via CPI to the System Program, allocating sufficient space for the metadata and the max_data_len bytes. The loader then uses the PDA’s bump seed to sign the CPI. 

The deploy_program! Macro ensures that the bytecode is safe to execute. It first parses the ELF file structure to validate ELF magic bytes (i.e., 0x7f ‘E’ ‘L’ ‘F’) and headers (i.e., 64-bit, Little Endian), extracts the program sections, processes relocation tables, and validates the section boundaries and alignments. Loading will fail immediately if the ELF is malformed or uses unsupported features.

The RequisiteVerifier (i.e., sBPF’s verifier) then performs static analysis on every possible execution path without running the program, ensuring it’s provably safe before executing any instructions. The verifier also enforces the aforementioned SVM ISA constraints. If verification fails, the deployment is rejected with InstructionError::InvalidAccountData, and the program is never marked as executable.

Once verification passes, the bytecode is compiled and cached for execution. The load_program_from_bytes function creates a ProgramCacheEntry containing:

  • JIT-compiled executable: The sBPF bytecode is Just-In-Time (JIT) compiled to native machine code for the validator’s CPU architecture. This provides near-native execution speed while maintaining safety.
  • Slot metadata: The deployment time and visibility time of the program are recorded as the deployment_slot and effective_slot, respectively. The delay prevents programs from being used in the same slot they’re deployed.
  • Runtime environment: References to the syscall registry to outline which syscalls are available, and the execution configuration that will be used when the program executes. 

The cache entry is stored in the program_cache_for_tx_batch, making the program available for execution in subsequent transactions.
After the program is verified and cached successfully, the loader updates the account states to finalize deployment. The ProgramData account’s state is updated to record when the program was deployed and who is authorized to upgrade it. The ELF bytecode is also copied from the buffer into the account. The Program account’s state is also updated to link it to the ProgramData account, and is marked as executable. Lastly, the buffer’s data length is set to the metadata size, effectively zeroing the bytecode and reclaiming space.

The program is now fully deployed and can be invoked by transactions.

BPF Loader V4

The BPF Loader V4 streamlines deployment by eliminating the need for a separate ProgramData account, allowing the program account to store bytecode directly. It also introduces support for zstd-compressed ELF storage, which significantly reduces rent costs while being decompressed on demand during loading.

The deployer calls SetProgramLength { new_size } to allocate space for the program’s metadata and bytecode. For new programs, this initializes the account with the LoaderV4State::Retracted status, records the authority, and marks the account as executable, although it cannot be invoked yet.

The deployer then writes the ELF binary via Write { offset, bytes } instructions directly to the program account. These writes are only permitted when the program is in the Retracted state. The Copy instruction can also be used to copy bytecode from another program, regardless of the loader version, which is helpful for migrations.

The Deploy instruction is then used to transition the program from a Retracted to a Deployed state. Essentially, this instruction extracts the bytecode from the program account at the offset and runs the exact same verification pipeline as the BPF Loader Upgradeable (i.e., ELF parsing, static verification, JIT compilation, caching). If verification succeeds, the program’s state is updated to LoaderV4Status::Deployed, and the deployment slot is recorded.

The program is now fully deployed and can be invoked by transactions.

Loader V4 also enforces a cooldown period between state transitions (i.e., deploy and retracted) to prevent redeployment attacks. Programs cannot be deployed or retracted within one slot of their last deployment. This helps prevent malicious actors from rapidly updating programs to exploit race conditions or confuse users, ensuring per-slot atomicity rather than multi-slot delays. Note that this cooldown applies to both the Deploy and Retract instructions.

Programs can also be made immutable via the Finalize instruction. This transitions the program from a Deployed status to a Finalized status, which means the program can no longer be retracted or upgraded. The authority field is repurposed to point to a “next version” program address, enabling explicit upgrade paths while maintaining the program’s immutability.

How Execution Works in the SVM

The SVM is the transaction processing engine within validators, responsible for executing program invocations and updating state accordingly. 

When a transaction arrives at a validator, it flows through a multi-stage pipeline: validation, account loading, program execution in an isolated sBPF VM, invariant verification, and state commitment. If all instructions succeed, account changes are written to AccountsDB. If any instruction fails, the entire transaction rolls back atomically.

The SVM operates as a decoupled execution engine. That is, it doesn’t manage consensus, networking, or ledger history. Instead, it focuses solely on executing programs safely, deterministically, and efficiently. 

The Bank orchestrates the SVM's execution, providing runtime context (e.g., blockhash, rent, feature set), and commits the results to persistent storage. The SVM manages program execution, from loading bytecode to enforcing compute budgets. This separation of concerns makes the SVM reusable beyond validators.

Transactions 

Transactions are the lifeblood of Solana, or any blockchain for that matter—they invoke programs to enact state changes. 

A transaction is a bundle of instructions that outline what actions should be performed, on what accounts, and whether they have the necessary permissions to do so. 

An instruction is a directive for a single program invocation. It is the smallest unit of execution logic, acting as the most basic operational unit on Solana.

Programs interpret the data passed in from an instruction to operate on the accounts specified. An instruction includes a program ID (i.e., the program being invoked), a list of accounts to read from and write to, and the input being passed to the program.

Transactions begin when a user defines a goal, such as transferring 10 SOL to another account. This intent translates into an instruction that tells the System Program to transfer 10 SOL from account A to account B. Account A would be passed into the transaction as a writable signer, and account B would be passed in as a writable account. The instruction would then be packaged into a transaction that also specifies the fee payer, signers, and a recent blockhash.

The transaction would then, typically, be sent to an RPC provider, such as Helius. The RPC node receiving the transaction would then verify that all required signatures are present and valid, that the transaction has not already been processed, that the recent blockhash provided is still valid, and that the transaction does not exceed the maximum size (i.e., 1232 bytes).

The RPC then forwards the transaction to the current leader’s Transaction Processing Unit (TPU). 

The Transaction Processing Unit (TPU)

The Transaction Processing Unit (TPU) is the transaction ingestion and processing pipeline within Solana validators. It has several stages that receive, verify, schedule, and execute transactions before they are committed to Solana’s ledger.

For our purposes, we will examine the Fetch Stage, SigVerify Stage, and Banking Stage in considerable detail before proceeding to the Bank and the provisioning of the sBPF VM. 

For a more detailed examination of the TPU, see Stake-Weighted Quality of Service: Everything You Need to Know.

The Fetch Stage

The Fetch Stage is the first stage in the TPU pipeline. It receives all incoming transactions via QUIC connections, which leverage UDP sockets as the underlying transport layer, and batches them for downstream processing.

Three UDP sockets are created:

  • tpu: Regular transactions such as token transfers, NFT mints, and program interactions.
  • tpu_vote: Vote transactions from validators—this will change with Alpenglow when vote transactions are removed.
  • tpu_forwards: Forwarded unprocessed transactions from the previous leader who couldn’t process them in time.

These sockets are registered in the validator’s gossip service and stored in the ContactInfo struct, which allows other validators and RPC nodes to discover where to send transactions.

The Fetch Stage spawns one thread per socket, which all continuously:

  • Poll the UDP socket for incoming packets.
  • Creates a batch of 64 packets.
  • Sends the batch through its unbounded channel.

Unbounded channels are currently used to pass batches to downstream stages, meaning that channels have unlimited capacity. The use of unbounded channels also means that the Fetch Stage can operate independently of downstream processing speed. 

While this prevents immediate packet drops during traffic spikes, it can lead to memory issues: if downstream stages can’t keep up with the Fetch Stage, channels grow at an unbounded rate, potentially causing slowdowns or out-of-memory (OOM) crashes. 

Work is underway to implement bounded channels with proper backpressure, allowing the system to signal congestion and prevent unbounded memory growth.

The Fetch Stage also creates another thread dedicated to handling forwarded packets. These packets are marked with a FORWARDED flag and are either held or discarded based on the leader schedule:

  • Honored: If the current validator will become the leader soon, forwarded packets are processed via the regular TPU channel and sent to the next stage.
  • Discarded: If the current validator is not going to become the leader soon, forwarded packets are dropped to prevent wasted processing. 

The Fetch Stage uses a PacketBatchRecycler to pre-allocate 1,000 packet batches, each containing 1,024 packets. This is done to reduce memory allocation overhead, as packet batch memory can be reused instead of allocating fresh batches every time. Historically, the recycler was needed for CUDA memory pinning, which is no longer functional and can largely be considered tech debt. 

The SigVerify Stage

The SigVerify Stage is the second stage in the TPU pipeline, responsible, as the name suggests, for verifying signatures. This is done early in the pipeline because signature verification for Ed25519 is computationally expensive, although not as expensive as executing transactions. Verifying transactions before execution allows validators to reject fraudulent transactions, prevent denial-of-service attacks, and ensure only well-formed transactions reach the Banking stage.

The SigVerify stage acts as a single thread that continuously receives packets from the Fetch Stage channels and processes them through a verification pipeline. Despite being a single thread, there is a great deal of internal parallelism.

By default, signatures are verified on the CPU using a parallel iterator. This is done to distribute verification across all available CPU cores, allowing each core to verify a subset of signatures independently. 

Signature verification can also be offloaded to the GPU if performance libraries are detected via perf_libs::api(). The GPU can only be used if there are at least 64 packets, and 90% are expected to be valid. The rationale is that the GPU has ~15-20ms overhead for setup and transfer, while the CPU can verify 64 signatures in ~10-20ms. Admittedly, this feature has proven impractical in production due to the latency overhead, making it much slower than CPU verification for realistic workloads. Plans are in place to remove this unused code path.

The verification process is relatively straightforward:

  • Receive packet batches from the Fetch Stage’s unbounded channels.
  • Randomly discard transactions via load shedding if packet volume exceeds 165,000.
  • Remove duplicate transactions.
  • Discard excess packets such that no single IP can monopolize verification bandwidth.
  • Pre-shrink and reorganize batches to improve cache locality and reduce memory waste.
  • Verify signatures.

All valid packets proceed to the Banking Stage. 

The Banking Stage

The Banking Stage is where transaction execution happens. Here, transactions are buffered, scheduled, and executed by parallel worker threads.

A central scheduler pattern is used, separating how vote and non-vote transactions are handled:

  • A single worker thread handles vote and gossip vote transactions.
  • Four worker threads process all non-vote transactions.
  • A single thread coordinates work distribution among the worker threads.

Incoming packets are deserialized and buffered up to 100,000 transactions. The scheduler continuously receives new transactions while managing this buffer, dropping expired or invalid transactions during clean queue operations that check up to 10,000 transactions at a time.

The scheduler determines transaction execution order using conflict detection (i.e., transactions that want to secure read and write locks for the same accounts). There are two scheduler implementations available by default:

  • PrioGraphScheduler: A scheduler that builds a priority graph to detect account conflicts.
  • GreedyScheduler: The default scheduler, with a more straightforward FIFO approach to transaction ordering.

The scheduler then selects non-conflicting transactions and dispatches them to worker threads. Each worker receives a batch and starts processing.

Bank Orchestration

The Bank represents the state of all accounts at a specific slot. It is the central data structure that manages account data, enforces runtime rules, and acts as the orchestrator between the Banking Stage workers and the sBPF VM for transaction execution. 

Lifecycle

Each Bank progresses through three states:

  • Active: A newly created Bank that is open to transactions. The Banking Stage workers apply transactions until either the Bank reaches its target tick count or all entries in the slot have been processed.
  • Frozen: Once the tick count has been reached or all entries have been processed, the Bank is frozen. No more transactions can be applied. At this point, transaction fees are accumulated to the block leader, sysvar accounts are updated, and the final bank hash is computed.
  • Rooted: After the frozen Bank receives sufficient votes from validators, it becomes rooted. The state is now finalized and becomes part of the chain’s ledger.

Each Bank, except for the genesis Bank, points back to a parent Bank, forming a tree structure that represents different forks of the ledger. 

Execution Flow

The Banking Stage workers operate on the current working bank (i.e., the active, unfrozen Bank being built for the current slot). When a worker receives a transaction batch from the scheduler, the Bank orchestrates the execution:

  • Account Locking: Workers call the prepare_sanitized_batch_with_results() function to lock all accounts referenced in the transactions to prevent concurrent modifications.
  • Account Loading: The Bank fetches account data from the AccountsDB for all of the locked accounts.
  • Fee Deduction: The Bank deducts the transaction fees from the fee payer’s account before execution.
  • Validation: The Bank validates the blockhash is recent, checks the nonce account state, and verifies account ownership.
  • VM Handoff: The Bank calls load_and_execute_transactions(), which passes the loaded accounts off to the sBPF VM.
  • Execution: The sBPF VM executes each instruction’s program bytecode.
  • Results: The sBPF VM returns all of the execution output (i.e., LoadAndExecuteTransactionOutput).
  • Commitment: The Bank writes the updated account states back to the AccountsDB via bank.commit_transactions().

Execution now enters the SVM proper (i.e., the sBPF VM). After the Bank calls load_and_execute_transactions(), transactions go through a multi-stage pipeline where each instruction is processed using a fresh, isolated sBPF VM instance that is provisioned to execute the program’s bytecode.

A transaction’s instructions are executed sequentially. The pipeline for each instruction is:

  • Locate the Program: Look up the program account from the instruction’s program ID.
  • Load from Cache: Check if the program in question is already JIT-compiled in the program cache. 
  • Provision the VM: Create an isolated sBPF VM instance with specific memory regions and compute budget.
  • Execute Bytecode: Run the program’s entrypoint with the instruction’s inputs.
  • Verify Invariants: Check that execution does not violate any runtime rules.
  • Collect Results: Package execution outcomes.

Program Loading

Before a program can execute, it must be loaded from its on-chain account, verified, and compiled into native machine code. This happens through the program cache and the JIT compilation pipeline.

The program cache is a performance optimization that avoids reloading and recompiling programs on every invocation. It’s maintained at the transaction batch level and stores ProgramCacheEntry objects, which contain:

  • JIT-compiled Executable: Native machine code for the validator’s CPU architecture.
  • Deployment Metadata: The slot when the program was deployed and became effective.
  • Runtime Environment: References to the syscall registry and execution configuration.

When a transaction references a program ID, the following lookup sequence is followed:

  • Check Transaction Batch Cache: Look for a cached, JIT-compiled version.
  • Check Global Program Cache: If not in the batch cache, check the validator’s global cache.
  • Load From Account: If all the caches miss, load the program account from AccountsDB.
  • Parse ELF: Extract the bytecode from the program account data.
  • Verify Bytecode: Run the static verifier to ensure safety.
  • JIT Compile: Translate the sBPF bytecode into native machine code.
  • Cache Entry: Store the compiled program for future invocations.

This means that the first invocation of a newly deployed program incurs the full cost of loading, verifying, and compiling, whereas subsequent invocations execute the cached native code directly. 

When the cache misses, the program must be loaded from its on-chain account. In the case of BPF Loader Upgradeable owned programs, the program account contains a reference to the ProgramData account, which is what gets loaded from AccountsDB. For Loader V4’s single-account model, the bytecode is loaded directly from the program account, which could require decompression if it is zstd-compressed.

The extracted ELF bytes are parsed to locate the executable bytecode and the aforementioned sections (i.e., .text, .rodata, .data / .bss, .symtab / .strtab). After the ELF is parsed successfully, the RequisiteVerifier (i.e., the sBPF’s static analyzer) verifies every possible execution path without actually running the program, as described in the earlier sections.

JIT Compilation

Once verification passes, the bytecode is Just-In-Time (JIT) compiled to native machine code. The JIT compiler translates each sBPF instruction into the equivalent native CPU instructions for the validator’s architecture. 

JIT compilation enables the sBPF VM to be performant enough to handle Solana’s high throughput. Without JIT, the VM would need to interpret sBPF bytecode instruction by instruction, incurring significant overhead. 

Each bytecode instruction requires fetching, decoding, and dispatching to handler code, which adds interpretation overhead. Then, the interpreted code cannot leverage CPU-level optimizations such as pipelining, branch prediction, or out-of-order execution. Thus, every sBPF instruction becomes a function call in the interpreter.

JIT compilation completely eliminates these costs by producing native machine code that runs directly on the CPU. This results in near-native performance, optimized bounds checks, inlined compute metering, hardware-level optimizations, and a clean register allocation mapping.

The JIT compiler performs a single-pass translation from sBPF bytecode to native machine code. For each sBPF instruction:

  • The instruction is decoded to extract the opcode, registers, offset, and immediate value.
  • Set up the stack frame, and save callee-saved registers.
  • Map the sBPF operation to the equivalent CPU instruction so each operation is compiled to native instructions. For example, in x86-64 mapping, rax would map to R0, and rbp would map to R10.
  • Emit bounds checks and software address translation for memory access—translating guest addresses to host addresses is one of the VM’s slowest operations due to its overhead.
  • Maintain stack isolation (i.e., the guest stack lives in a heap-allocated buffer rather than the host stack).
  • Insert CU deduction and budget checks to emit compute metering.
  • Restore registers and clean up the stack.

Instruction Translation

The JIT compiler translates different instruction types (i.e., arithmetic, memory access, store operations, conditional branches, syscall dispatches) in specific ways. 

Arithmetic operations are mapped directly to single native CPU instructions with zero overhead since sBPF’s registers map nicely to the validator’s corresponding hardware registers. 

Memory access operations require bounds checking to prevent out-of-bounds reads and writes. The JIT compiler generates validation code that checks the lower and upper bounds of each memory access against the valid region boundaries. 

This essentially involves 3 to 6 native instructions that calculate the effective address, verify it falls within the expected bounds, and then perform the actual load. This is handled relatively efficiently via branch prediction since bounds violations are rare. 

Store operations include bounds checking and write permission validation. The compiled code verifies that the target address is within bounds and that the memory region has write permissions enabled before writing to memory.

Conditional branches compile to native conditional jump instructions. The JIT compiler resolves all jump targets during compilation such that sBPF’s relative instruction offsets are converted into absolute addresses in native code. 

A syscall dispatch requires saving the VM’s state—all 11 registers—and calling the native syscall handler. After, the VM state is restored with the return value. This state management overhead is why syscalls have fixed CU costs, which are higher than regular instructions.

Compute Unit Metering

The JIT compiler inlines compute unit tracking directly into the generated code. Every sBPF instruction includes a budget check to ensure the budget isn’t exhausted as instructions decrement from the remaining CU count. 

This inlining avoids function call overhead, and can be done efficiently via branch prediction, out-of-order execution (i.e., the CU check and the operation can execute in parallel), and instruction-level parallelism.

For syscalls with a variable cost (e.g., the sol_sha256 syscall that scales with data length), the cost calculation happens inside the native syscall implementation before returning to the VM.

Caching the Compiled Code

After JIT compilation has completed, the native executable is stored in a ProgramCacheEntry, which contains:

  • The JIT-compiled code
  • The deployment slot and effective slot metadata
  • Syscall registry references
  • Runtime environment configuration

The cache entry is placed in the transaction batch cache and the global program cache. The former is available to all instructions in the current batch, whereas the latter is available across all future transactions. 

The cache entry can be invalidated when the program is upgraded (i.e., new bytecode is deployed), the program account is closed, feature gates change syscall availability, or whenever a validator decides to flush its cache.

Effective Slot Delay

Note that a program deployed in slot n cannot be invoked until slot n + 1. This delay ensures that all validators observe the deployment, that program caches synchronize across the network, and that per-slot atomicity is enforced. 

Provisioning the sBPF VM

Once the program is loaded and JIT compiled, or retrieved from cache, a new sBPF VM is provisioned for each instruction execution. This provisioning happens in the BPF Loader and involves setting up five distinct memory regions, initializing the compute budget, and registering syscalls.

Memory Regions

As mentioned in our SVM ISA section, the VM creates five distinct memory regions. Together, these regions form the isolated sandbox in which Solana programs execute.
The program memory region typically starts at the address 0x100000000. It contains the JIT-compiled native code to be executed—this is the actual machine code, depending on the validator’s CPU architecture. If JIT is disabled for debugging, then this region contains the interpreted sBPF bytecode instead. The permissions for this section are read and execute only.

Read-only data is also included in 0x100000000. It contains constants and static strings extracted from the ELF .rodata section during program loading. The purpose of this memory region is to provide efficient access to compile-time constants without requiring heap allocation. 

The stack starts at 0x200000000 and contains local variables, function call frames, and return addresses. This is where temporary computation happens during execution. It grows downward from the top of the region with register R10 (i.e., the frame pointer) marking the current frame boundary. Read and write permissions are allowed for this region, and its size is fixed at 4KB per call frame. Exceeding this limit will throw a StackAccessViolation error, indicating a stack overflow. The stack’s smaller size encourages developers to use the heap, or, better yet, store data in accounts, rather than rely on stack-based storage.

The heap starts at 0x300000000 and contains dynamically allocated memory for runtime data structures that don’t fit on the stack or in an account. Its size ranges from a default of 32KB to a maximum of 256KB. Previously, programs could expand the heap via the sol_alloc_free syscall. However, the sol_alloc_free syscall has been deprecated and is disabled for new program deployments. Programs must specify their required heap size at deployment time rather than expanding dynamically. 

Note: heap growth consumes compute units based on the formula (heap_size / 32KB) * 8,000 CUs, with a default heap cost of 8 CUs.

The input data memory region starts at the address 0x400000000. It contains the serialized entrypoint parameters that the program receives when invoked. This is a read-only memory region where the actual size varies by transaction. The three serialized components are the 32-byte pubkey of the program being invoked, an array of accounts, and the instruction data.

Memory Access Enforcement

Every memory load and store instruction is bounds-checked by the VM. Before each memory access, the VM verifies that the address falls within the region’s valid range and the access type (i.e., read or write) is permitted for that region. 

Execution aborts immediately with an AccessViolation error if either check fails. The entire transaction rolls back, and no state changes are committed. 

This enforcement happens at near-zero runtime cost because the JIT compiler compiles these checks into efficient native code that the CPU can execute directly. Modern branch prediction handles the checks efficiently, given that violations are rare. 

Compute Budget Initialization

Each VM instance is initialized with a compute unit budget that limits the total amount of work a given program can perform. This bounded execution model ensures that programs cannot run indefinitely and that all validators execute transactions within a predictable window.

The current budget parameters are as follows:

The default CUs per transaction is min(1_400_00, (200_000*non_reserve_instructions + 3_000*reserve_instructions)). This is essentially the minimum between the maximum compute unit limit and the default costs per instruction type based on the instructions provided.

The compute budget tracks the remaining units throughout execution. As each sBPF instruction executes, it decrements its CU cost from the remaining budget. If the budget reaches zero before the program completes, execution halts immediately with InstructionError::ComputationalBudgetExceeded. The transaction fails, no state changes are committed, but the fee payer is still charged the transaction fee to compensate the validator for processing their transaction. 

Syscall Registry

A mapping between each syscall’s unique 32-bit Murmur hash identifier and its native Rust implementation is also made during the VM provisioning process, such that all available syscalls are registered. 

The following occurs when a program executes a CALL_IMM instruction with a syscall hash:

  • The VM pauses the sBPF instruction stream.
  • The syscall dispatcher finds the implementation in the registry using the hash.
  • The syscall verifies the caller has the necessary permissions (e.g., for making a CPU, account ownership).
  • The syscall’s Rust implementation executes with full runtime access outside the sandbox.
  • The syscall’s fixed cost is subtracted from the remaining compute budget.
  • The result is placed in register R0, and sBPF execution resumes. 

Program Execution

Once the sBPF VM is provisioned, execution begins at the program’s entrypoint function. For JIT-compiled programs, the VM jumps directly to native machine code and lets the validator’s CPU execute it natively. As outlined in the previous section, the JIT-compiled code includes all the necessary instrumentation—memory-bound checks, compute metering, and control flow validation, all inlined for maximum performance.

Register R1 contains a pointer to the input data region where the three serialized parameters (i.e., the pubkey of the program being invoked, an array of accounts, and the instruction data) reside. 

Note: account data is accessed via pointers rather than being copied. Using pointers allows programs to read and modify account data in place, which is critical for performance.

Each function call allocates a new 4KB stack frame, with register R10 updated to point to the new frame. Compute is metered according to the aforementioned compute budget.

Cross-Program Invocations (CPI)

During execution, programs can invoke other programs through Cross-Program Invocations (CPIs)—the backbone of the SVM’s composability. 

A CPI is initiated via the sol_invoke_signed syscall, which costs 1,000 CUs plus additional costs based on the serialized account data being passed. Account data and instruction data serialization both cost 250 bytes per CU.

When a program makes a CPI, a new execution context with its own instruction stack frame is created. At the time of writing, the maximum instruction stack depth is 5, or 9 with SIMD-0268 enabled, meaning a program can invoke another program, which can invoke another, up to the depth limit. Each nested invocation maintains its own set of writable accounts and signer privileges. A CPI can have a maximum of 16 signers and pass in 128 AccountInfo structs

The caller serializes the target program ID, accounts, and instruction data, then invokes the syscall. The current program’s execution is paused, and a new sBPF VM instance is provisioned for the callee program, following the same aforementioned provisioning process. Execution then begins for the callee. The callee runs with its own compute budget drawn from the caller’s remaining budget, meaning CPI calls share the transaction’s total compute budget.

Programs can sign on behalf of accounts that they own via Program Derived Addresses (PDAs). When invoking with sol_invoke_signed, the caller provides seeds that prove ownership of the PDA. PDA derivation is verified before granting signing authority to the callee. 

When the callee completes, control returns to the caller. Account modifications made by the callee are visible to the caller, allowing state to flow through the call chain. If any program in the CPI chain fails, the entire transaction aborts, and all state changes revert.

Note: sol_invoke is a helper that calls sol_invoke_signed with no seeds.

Post-Execution Verification

After a program’s execution completes, whether that be directly or as part of a CPI chain, several post-execution checks occur to ensure state consistency and security invariants. 

For example, the runtime verifies that all accounts marked as writable were actually owned by the program or properly signed—programs cannot modify accounts they don’t own unless those accounts were explicitly marked as writable and the owner granted permission, preventing unauthorized state modifications.

The runtime also checks that the total sum of lamports across all accounts in the transaction is the same, unless lamports were explicitly transferred via System Program instructions. This conservation check prevents programs from creating or destroying lamports such that SOL’s total supply remains constant.

The runtime validates that all accounts with executable flags (i.e., programs) were not modified. A program cannot have its data changed during normal execution, and can only be upgraded through the BPF Loader’s upgrade authority mechanism.

Execution Results

Once post-execution verification completes, the execution result is returned to the Bank’s transaction processor. The result contains the success or error status (i.e., a zero or non-zero result, respectively), the number of compute units consumed, and any modifications made to account state. 

The Bank commits all account modifications atomically for successful executions. The updated account data, lamport balances, and metadata are written to the AccountsDB and become visible in subsequent transactions. The consumed compute units are logged for transaction fee calculations and network metrics.

No state changes are committed for failed transactions—there are no partial reverts on Solana as the transaction is rolled back entirely. However, the transaction fee is still deducted from the fee payer’s account to compensate the validator for the computational work performed. The error code and consumed compute units are recorded in the transaction metadata for debugging and analytical purposes.

The execution result flows back through the Banking Stage scheduler, which updates its internal metrics and moves on to the next transaction. Both successful and failed transactions are recorded into the Proof of History stream and contribute to the current block being built. Failed transactions are included to help prevent replay attacks and maintain a complete transaction history.

When the slot completes and receives its maximum tick count, the Bank transitions to a frozen state. Freezing is a one-way operation that prevents new transactions from being committed and computes the Bank’s hash. Note that frozen does not mean finalized—the slot could still be on a fork that gets discarded. 

The Bank becomes rooted when the validator calls BankForks::set_root() to designate it as part of the canonical chain. Rooting triggers a squash operation that flattens the rooted Bank’s account state into AccountsDB, merging all parent state and making it permanent from the validator’s perspective. Non-rooted forks are pruned and discarded. Even rooted Banks are not yet finalized from the cluster’s perspective due to Solana’s differing commitment levels.

Looking Forward

The Solana Virtual Machine represents a fundamentally different approach to blockchain execution—a scalable blockchain available to the masses, thanks to parallel processing, local fee markets, and a performant, deterministic eBPF-derived runtime. 

Understanding the SVM requires examining the entire execution pipeline, from Rust source code compilation down to LLVM, sBPF, and finally the provisioning of isolated VM instances. 

There is no single “specification” that defines the SVM. Instead, it emerges from the interaction of the Bank, the scheduler, the BPF Loaders, the sBPF VM, and the SVM ISA.

The future looks promising as the SVM continues to evolve. 

The Solana toolchain is undergoing a complete overhaul to eliminate the custom LLVM infrastructure that has plagued the developer onboarding for years. 

The current approach requires developers to install custom toolchains via platform-specific scripts. The solution is to adopt the same toolchain used by Aya, the Rust eBPF library. Developers will be able to run two simple commands to compile directly to eBPF bytecode:

Code
rustup toolchain install nightly
cargo build --target=bpfel-unknown-none

No scripts. No custom LLVM forks. Just standard Rust tooling compiling directly to eBPF bytecode using the upstream bpfel-unknown-none target, leveraging the countless years of Linux kernel development and LLVM infrastructure improvements.

The SVM ISA is also set to be updated with SIMD-0377, which proposes aligning Solana’s eBPF implementation (i.e., sBPF) with modern eBPF standards. This includes introducing JMP32 instruction variants, signed division and modulo operations, indirect jumps, and dynamic stack frames. These changes will help reduce program compute costs, improve compatibility with upstream LLVM infrastructure, and enable more efficient code generation. 

The SVM is fundamentally a metered system via compute budgets. SIMD-0370 is poised to change how this works by removing the block-level compute cap and, possibly, the transaction cap. Removing these compute caps would allow block producers to maximize throughput based on their hardware capabilities rather than artificial limits. Combined with Alpenglow’s timeout mechanism, this change would let market forces, rather than protocol-level constraints, determine optimal block sizes. Of course, this is very forward, as Anza would like to increase the CU limit to 100m+ first before removing such caps.

Underlying all of these changes is a builder’s ethos to push the boundaries of what blockchains can achieve without sacrificing safety, determinism, or decentralization. 

The SVM is not merely a bytecode interpreter—it is a complete execution pipeline that revolutionizes the capabilities of blockchains. It is the culmination of architectural decisions that prioritize throughput and low latency. As Solana matures, the SVM will continue to evolve to support highly performant, capital-efficient applications.

The dream of Internet Capital Markets requires infrastructure capable of handling the throughput, latency, and cost requirements of global financial systems. The Solana Virtual Machine is a critical step toward realizing that vision.

Additional Resources

Related Articles

Subscribe to Helius

Stay up-to-date with the latest in Solana development and receive updates when we post