Tenzro Testnet is live. Get testnet TNZO

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

SyscallDescriptionStatus
sol_logLog message stringImplemented
sol_log_64Log 5 u64 valuesImplemented
sol_log_pubkeyLog public keyImplemented
sol_sha256Compute SHA-256 hashImplemented
sol_keccak256Compute Keccak-256 hashImplemented
sol_memcpyCopy memory regionImplemented
sol_memsetSet memory to valueImplemented
sol_memmoveMove memory (overlap-safe)Implemented
sol_memcmpCompare memory regionsImplemented
sol_create_program_addressDerive program addressStub (SHA-256)
sol_try_find_program_addressFind valid PDA with bumpStub (SHA-256)
sol_invoke_signedCross-program invocationNot 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

ConstantValueDescription
DEFAULT_COMPUTE_UNITS1,400,000Default budget per transaction
MAX_COMPUTE_UNITS1,400,000Maximum budget per transaction
CU_PER_INSTRUCTION1Cost per BPF instruction
CU_SYSCALL_BASE100Base cost for any syscall
CU_SHA256_BASE85SHA-256 base cost
CU_SHA256_PER_BYTE1SHA-256 per byte
CU_LOG100Cost per log operation
MAX_CALL_DEPTH4Maximum CPI call depth
MAX_ACCOUNT_DATA_SIZE10 MBMaximum 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)