SVM Executor
The Tenzro SVM executor provides Solana Virtual Machine compatibility using the solana_rbpf library for BPF (Berkeley Packet Filter) program execution. It implements Solana's account model, syscall interface, compute budget metering, and program deployment mechanics.
Architecture
The SvmExecutor wraps solana_rbpf to execute eBPF bytecode compiled from Rust programs. Unlike the EVM's stack-based architecture, SVM uses a register-based virtual machine with explicit account passing and copy-on-write semantics.
┌────────────────────────────────┐
│ SvmExecutor │
│ (Solana Compatibility) │
└───────────┬────────────────────┘
│
│ wraps
▼
┌────────────────────────────────┐
│ solana_rbpf │
│ (eBPF VM Implementation) │
├────────────────────────────────┤
│ • Register-based execution │
│ • BPF bytecode interpreter │
│ • Compute unit metering │
│ • Syscall interface │
└───────────┬────────────────────┘
│
│ syscalls
▼
┌────────────────────────────────┐
│ Syscall Registry │
│ (sol_log, sol_sha256, etc.) │
└───────────┬────────────────────┘
│
│ state access
▼
┌────────────────────────────────┐
│ Account Storage │
│ (Solana Account Model) │
└────────────────────────────────┘
Program Execution
SVM programs are executed by loading eBPF bytecode into the VM, serializing account data according to Solana's format, and invoking the program entry point. The VM executes instructions while metering compute units consumed.
Basic Program Invocation
use tenzro_vm::svm::SvmExecutor;
use tenzro_types::{Transaction, Account};
let executor = SvmExecutor::new(state_adapter);
// Load program from storage
let program_address = Address::from_base58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")?;
let program_bytecode = state_adapter.get_code(&program_address)?;
// Prepare accounts for instruction
let accounts = vec![
Account {
address: token_mint_address,
lamports: 1_000_000,
data: mint_account_data,
owner: token_program_id,
is_signer: false,
is_writable: true,
},
Account {
address: user_token_account,
lamports: 2_000_000,
data: token_account_data,
owner: token_program_id,
is_signer: false,
is_writable: true,
},
Account {
address: authority_address,
lamports: 100_000_000,
data: vec![],
owner: system_program_id,
is_signer: true,
is_writable: false,
},
];
// Encode instruction data
let instruction_data = borsh::to_vec(&TransferInstruction {
amount: 1_000_000_000, // 1 token (9 decimals)
})?;
// Execute program
let result = executor.execute_program(
&program_address,
&accounts,
&instruction_data,
1_400_000, // Compute budget
).await?;
println!("Compute units used: {}", result.compute_units_used);
println!("Logs: {:?}", result.logs);
println!("Modified accounts: {}", result.modified_accounts.len());
Account Serialization
Solana programs receive account data as a serialized byte array with a specific format. The executor serializes accounts according to Solana's specification: duplicate account count, account metadata, and account data.
use tenzro_vm::svm::account_serialization::serialize_accounts;
// Solana account serialization format:
// - 8 bytes: number of accounts (u64 LE)
// - For each account:
// - 1 byte: duplicate flag
// - 1 byte: is_signer flag
// - 1 byte: is_writable flag
// - 32 bytes: account public key
// - 8 bytes: lamports (u64 LE)
// - 8 bytes: data length (u64 LE)
// - N bytes: account data
// - 32 bytes: owner public key
// - 1 byte: executable flag
// - 8 bytes: rent_epoch (u64 LE)
fn serialize_accounts(accounts: &[Account]) -> Vec<u8> {
let mut serialized = Vec::new();
// Write account count
serialized.extend_from_slice(&(accounts.len() as u64).to_le_bytes());
for account in accounts {
// Duplicate flag (0 = not duplicate)
serialized.push(0);
// Flags
serialized.push(if account.is_signer { 1 } else { 0 });
serialized.push(if account.is_writable { 1 } else { 0 });
// Account address (32 bytes)
serialized.extend_from_slice(account.address.as_bytes());
// Lamports
serialized.extend_from_slice(&account.lamports.to_le_bytes());
// Data length and data
serialized.extend_from_slice(&(account.data.len() as u64).to_le_bytes());
serialized.extend_from_slice(&account.data);
// Owner
serialized.extend_from_slice(account.owner.as_bytes());
// Executable flag
serialized.push(if account.executable { 1 } else { 0 });
// Rent epoch
serialized.extend_from_slice(&account.rent_epoch.to_le_bytes());
}
serialized
}
// Deserialize modified accounts after execution
fn deserialize_accounts(serialized: &[u8]) -> Result<Vec<Account>, Error> {
// Parse modified account data from VM output
}
Syscall Interface
SVM programs interact with the runtime through syscalls. The executor implements stubs for common Solana syscalls including logging, hashing, memory operations, and account validation.
Logging Syscalls
// sol_log - Log a message
// Signature: fn sol_log(message: *const u8, len: u64)
fn syscall_sol_log(
message_ptr: u64,
message_len: u64,
memory: &Memory,
logger: &mut Logger,
) -> u64 {
let message_bytes = memory.read(message_ptr, message_len as usize);
let message = String::from_utf8_lossy(&message_bytes);
logger.log(&format!("Program log: {}", message));
0 // Success
}
// sol_log_64 - Log 5 64-bit values
// Signature: fn sol_log_64(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64)
fn syscall_sol_log_64(
arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64,
logger: &mut Logger,
) -> u64 {
logger.log(&format!(
"Program log: {:#x}, {:#x}, {:#x}, {:#x}, {:#x}",
arg1, arg2, arg3, arg4, arg5
));
0
}
// sol_log_pubkey - Log a public key
// Signature: fn sol_log_pubkey(pubkey: *const u8)
fn syscall_sol_log_pubkey(
pubkey_ptr: u64,
memory: &Memory,
logger: &mut Logger,
) -> u64 {
let pubkey_bytes = memory.read(pubkey_ptr, 32);
let pubkey = base58::encode(&pubkey_bytes);
logger.log(&format!("Program log: {}", pubkey));
0
}
Cryptographic Syscalls
use sha2::{Sha256, Digest};
use sha3::Keccak256;
// sol_sha256 - Compute SHA-256 hash
// Signature: fn sol_sha256(input: *const u8, len: u64, output: *mut u8) -> u64
fn syscall_sol_sha256(
input_ptr: u64,
input_len: u64,
output_ptr: u64,
memory: &mut Memory,
) -> u64 {
let input = memory.read(input_ptr, input_len as usize);
let hash = Sha256::digest(&input);
memory.write(output_ptr, &hash);
0 // Success
}
// sol_keccak256 - Compute Keccak-256 hash
fn syscall_sol_keccak256(
input_ptr: u64,
input_len: u64,
output_ptr: u64,
memory: &mut Memory,
) -> u64 {
let input = memory.read(input_ptr, input_len as usize);
let hash = Keccak256::digest(&input);
memory.write(output_ptr, &hash);
0
}
// sol_blake3 - Compute BLAKE3 hash
fn syscall_sol_blake3(
input_ptr: u64,
input_len: u64,
output_ptr: u64,
memory: &mut Memory,
) -> u64 {
let input = memory.read(input_ptr, input_len as usize);
let hash = blake3::hash(&input);
memory.write(output_ptr, hash.as_bytes());
0
}
Memory Syscalls
// sol_memcpy - Copy memory
// Signature: fn sol_memcpy(dst: *mut u8, src: *const u8, len: u64)
fn syscall_sol_memcpy(
dst_ptr: u64,
src_ptr: u64,
len: u64,
memory: &mut Memory,
) -> u64 {
let data = memory.read(src_ptr, len as usize);
memory.write(dst_ptr, &data);
0
}
// sol_memset - Set memory to value
// Signature: fn sol_memset(dst: *mut u8, value: u8, len: u64)
fn syscall_sol_memset(
dst_ptr: u64,
value: u8,
len: u64,
memory: &mut Memory,
) -> u64 {
let data = vec![value; len as usize];
memory.write(dst_ptr, &data);
0
}
// sol_memmove - Move memory (overlapping regions allowed)
fn syscall_sol_memmove(
dst_ptr: u64,
src_ptr: u64,
len: u64,
memory: &mut Memory,
) -> u64 {
// Handle overlapping regions correctly
let data = memory.read(src_ptr, len as usize);
memory.write(dst_ptr, &data);
0
}
// sol_memcmp - Compare memory
fn syscall_sol_memcmp(
s1_ptr: u64,
s2_ptr: u64,
len: u64,
result_ptr: u64,
memory: &mut Memory,
) -> u64 {
let s1 = memory.read(s1_ptr, len as usize);
let s2 = memory.read(s2_ptr, len as usize);
let result = s1.cmp(&s2) as i32;
memory.write(result_ptr, &result.to_le_bytes());
0
}
Account Validation Syscalls
// sol_create_program_address - Derive program address (PDA)
// Signature: fn sol_create_program_address(
// seeds: *const u8,
// seeds_len: u64,
// program_id: *const u8,
// address_out: *mut u8,
// ) -> u64
fn syscall_sol_create_program_address(
seeds_ptr: u64,
seeds_len: u64,
program_id_ptr: u64,
address_out_ptr: u64,
memory: &mut Memory,
) -> u64 {
let seeds = memory.read(seeds_ptr, seeds_len as usize);
let program_id = memory.read(program_id_ptr, 32);
// Derive PDA using SHA-256
let mut hasher = Sha256::new();
hasher.update(&seeds);
hasher.update(&program_id);
hasher.update(b"ProgramDerivedAddress");
let hash = hasher.finalize();
memory.write(address_out_ptr, &hash[..32]);
0 // Success
}
// sol_try_find_program_address - Find valid PDA with bump seed
fn syscall_sol_try_find_program_address(
seeds_ptr: u64,
seeds_len: u64,
program_id_ptr: u64,
address_out_ptr: u64,
bump_out_ptr: u64,
memory: &mut Memory,
) -> u64 {
let base_seeds = memory.read(seeds_ptr, seeds_len as usize);
let program_id = memory.read(program_id_ptr, 32);
// Try bump seeds from 255 down to 0
for bump in (0..=255).rev() {
let mut seeds = base_seeds.clone();
seeds.push(bump);
let mut hasher = Sha256::new();
hasher.update(&seeds);
hasher.update(&program_id);
hasher.update(b"ProgramDerivedAddress");
let hash = hasher.finalize();
// Check if address is valid (off the Ed25519 curve)
if is_valid_pda(&hash) {
memory.write(address_out_ptr, &hash[..32]);
memory.write(bump_out_ptr, &[bump]);
return 0; // Success
}
}
1 // Error: no valid PDA found
}
Compute Unit Metering
SVM uses compute units (CU) instead of gas for resource metering. Each BPF instruction consumes a specific number of compute units, with syscalls consuming additional units based on complexity. The default compute budget is 1,400,000 CU per transaction.
use tenzro_vm::svm::compute_budget::{ComputeBudget, ComputeMeter};
// Default compute budget
const DEFAULT_COMPUTE_UNITS: u64 = 1_400_000;
const MAX_COMPUTE_UNITS: u64 = 1_400_000;
// Instruction costs (in compute units)
const CU_PER_INSTRUCTION: u64 = 1;
const CU_SYSCALL_BASE: u64 = 100;
const CU_SHA256_BASE: u64 = 85;
const CU_SHA256_PER_BYTE: u64 = 1;
const CU_KECCAK256_BASE: u64 = 85;
const CU_KECCAK256_PER_BYTE: u64 = 1;
const CU_MEMCPY_PER_BYTE: u64 = 1;
const CU_LOG: u64 = 100;
let mut meter = ComputeMeter::new(DEFAULT_COMPUTE_UNITS);
// Consume CUs during execution
meter.consume(CU_PER_INSTRUCTION)?; // Each instruction
// Syscall costs
meter.consume(CU_SYSCALL_BASE + CU_SHA256_BASE + data_len * CU_SHA256_PER_BYTE)?;
// Check remaining budget
if meter.remaining() < required_cu {
return Err(VmError::ComputeBudgetExceeded);
}
println!("Compute units used: {}", meter.used());
println!("Compute units remaining: {}", meter.remaining());
Program Deployment
SVM programs are deployed by writing eBPF bytecode to a program account and marking it as executable. Programs are loaded into the VM on first invocation and cached for subsequent executions.
use tenzro_vm::svm::program_loader::deploy_program;
// Compile Rust program to eBPF
// cargo build-bpf --manifest-path=./my_program/Cargo.toml
let program_bytecode = std::fs::read("my_program.so")?;
let program_address = Address::new_unique();
// Create program account
let program_account = Account {
address: program_address,
lamports: 1_000_000, // Rent-exempt minimum
data: program_bytecode,
owner: bpf_loader_upgradeable_id,
executable: true,
rent_epoch: 0,
};
// Store program in state
state_adapter.create_account(&program_account)?;
// Invoke program
let result = executor.execute_program(
&program_address,
&accounts,
&instruction_data,
DEFAULT_COMPUTE_UNITS,
).await?;
println!("Program deployed at: {}", program_address);
Cross-Program Invocation
SVM supports cross-program invocation (CPI) where one program calls another. The executor maintains a call stack and enforces privilege escalation rules for signer and writable account access.
// sol_invoke_signed - Invoke another program with signer seeds
// Signature: fn sol_invoke_signed(
// instruction: *const Instruction,
// accounts: *const AccountMeta,
// signers_seeds: *const &[&[u8]],
// ) -> u64
struct CrossProgramInvocation {
program_id: Address,
accounts: Vec<AccountMeta>,
data: Vec<u8>,
signer_seeds: Vec<Vec<Vec<u8>>>,
}
fn syscall_sol_invoke_signed(
instruction_ptr: u64,
accounts_ptr: u64,
signers_seeds_ptr: u64,
memory: &Memory,
executor: &SvmExecutor,
call_depth: u32,
) -> u64 {
const MAX_CALL_DEPTH: u32 = 4;
if call_depth >= MAX_CALL_DEPTH {
return 1; // Error: call depth exceeded
}
// Deserialize CPI instruction
let instruction = deserialize_instruction(instruction_ptr, memory)?;
let accounts = deserialize_account_metas(accounts_ptr, memory)?;
let signer_seeds = deserialize_signer_seeds(signers_seeds_ptr, memory)?;
// Verify PDAs for signer seeds
for seeds in signer_seeds {
let pda = derive_pda(&seeds, &instruction.program_id);
// Verify PDA is in accounts and marked as signer
}
// Execute called program
let result = executor.execute_program(
&instruction.program_id,
&accounts,
&instruction.data,
remaining_compute_units,
).await?;
0 // Success
}
Syscall Registry
| Syscall | Description | Status |
|---|
sol_log | Log message string | Implemented |
sol_log_64 | Log 5 u64 values | Implemented |
sol_log_pubkey | Log public key | Implemented |
sol_sha256 | Compute SHA-256 hash | Implemented |
sol_keccak256 | Compute Keccak-256 hash | Implemented |
sol_memcpy | Copy memory region | Implemented |
sol_memset | Set memory to value | Implemented |
sol_memmove | Move memory (overlap-safe) | Implemented |
sol_memcmp | Compare memory regions | Implemented |
sol_create_program_address | Derive program address | Stub (SHA-256) |
sol_try_find_program_address | Find valid PDA with bump | Stub (SHA-256) |
sol_invoke_signed | Cross-program invocation | Not implemented |
Configuration
use tenzro_vm::svm::{SvmExecutor, SvmConfig};
let config = SvmConfig {
compute_budget: 1_400_000,
enable_syscalls: true,
max_account_data_size: 10_485_760, // 10 MB
max_instruction_data_size: 1_280, // 1.25 KB
max_call_depth: 4,
enable_cpi: true,
log_compute_units: true,
};
let executor = SvmExecutor::with_config(config, state_adapter);
// Override compute budget per transaction
executor.set_compute_budget(200_000); // Lower budget for simple operations
Compute Budget Constants
| Constant | Value | Description |
|---|
| DEFAULT_COMPUTE_UNITS | 1,400,000 | Default budget per transaction |
| MAX_COMPUTE_UNITS | 1,400,000 | Maximum budget per transaction |
| CU_PER_INSTRUCTION | 1 | Cost per BPF instruction |
| CU_SYSCALL_BASE | 100 | Base cost for any syscall |
| CU_SHA256_BASE | 85 | SHA-256 base cost |
| CU_SHA256_PER_BYTE | 1 | SHA-256 per byte |
| CU_LOG | 100 | Cost per log operation |
| MAX_CALL_DEPTH | 4 | Maximum CPI call depth |
| MAX_ACCOUNT_DATA_SIZE | 10 MB | Maximum account data size |
Testing
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_simple_program_execution() {
let state = InMemoryStateAdapter::new();
let executor = SvmExecutor::new(state.clone());
// Deploy simple program
let program_bytecode = compile_test_program()?;
let program_id = deploy_program(&state, program_bytecode)?;
// Execute program
let result = executor.execute_program(
&program_id,
&accounts,
&instruction_data,
DEFAULT_COMPUTE_UNITS,
).await?;
assert!(result.success);
assert!(result.compute_units_used < DEFAULT_COMPUTE_UNITS);
}
#[tokio::test]
async fn test_syscall_logging() {
// Test sol_log syscall captures output
}
#[tokio::test]
async fn test_compute_budget_exceeded() {
// Test execution fails when budget exhausted
}
}
Production Readiness
Production-Ready:
- Real BPF program execution via solana_rbpf
- Solana-format account serialization
- Compute unit metering and budget enforcement
- Syscall stubs for logging, hashing, and memory operations
- Program deployment and caching
Outstanding Issues:
- PDA derivation uses SHA-256 instead of Ed25519 curve-compliant derivation (issue #85)
- Cross-program invocation not implemented
- State adapter does not persist to storage (issue #25)
- No transaction signature verification (issue #26)