diff --git a/docs/implementation_details.md b/docs/implementation_details.md new file mode 100644 index 0000000..a15545b --- /dev/null +++ b/docs/implementation_details.md @@ -0,0 +1,186 @@ +# LessVM Implementation Details + +This document provides details about the implementation of LessVM, focusing on recent changes and optimizations. + +## Table of Contents + +- [SIMD Vector Addition](#simd-vector-addition) +- [Data Structure Store](#data-structure-store) +- [Memory Management](#memory-management) +- [Opcode Implementations](#opcode-implementations) + - [Solana Account Operations](#solana-account-operations) + - [Graph Operations](#graph-operations) + - [OHLCV Operations](#ohlcv-operations) + - [Hypergraph Operations](#hypergraph-operations) + +## SIMD Vector Addition + +The `vector_add` function uses SIMD (Single Instruction, Multiple Data) instructions to efficiently add multiple values at once. The implementation loads two vectors from the stack and adds them together. + +```mermaid +sequenceDiagram + participant VM + VM->>VM: _mm256_loadu_si256(stack[top] to values2) + VM->>VM: stack.pop() x 4 + VM->>VM: _mm256_loadu_si256(stack[top] to values1) + VM->>VM: result = _mm256_add_epi64(values1, values2) + VM->>VM: stack.push(result) x 4 +``` + +The function works as follows: +1. Load the second vector (values2) from the top of the stack +2. Pop the first 4 values from the stack +3. Load the first vector (values1) from the new top of the stack +4. Add the two vectors together using SIMD instructions +5. Push the result back onto the stack + +This approach ensures that we're adding two different vectors together, rather than adding a vector to itself. + +## Data Structure Store + +The `DataStructureStore` manages various data structures used by the VM, including BTreeMaps, Tries, Graphs, OHLCV, and Hypergraphs. + +```mermaid +classDiagram + class DataStructureStore { + btrees: Vec> + tries: Vec> + graphs: Vec> + ohlcvs: Vec> + hypergraphs: Vec> + +new() DataStructureStore + +ensure_capacity(ds_type: DataStructureType, id: usize) void + } + note for DataStructureStore "Stores different types of data structures." +``` + +The `ensure_capacity` method ensures that the vectors have enough capacity to store a data structure at a specific index. If the index is beyond the current length of the vector, the vector is resized to accommodate the new index. + +```mermaid +sequenceDiagram + participant VM + participant DataStructureStore + VM->>DataStructureStore: ensure_capacity(DataStructureType::BTreeMap, 5) + alt Vector length < 6 + DataStructureStore->>DataStructureStore: resize_with(6, || None) + else Vector length >= 6 + DataStructureStore->>DataStructureStore: Do nothing + end +``` + +## Memory Management + +The memory management system has been optimized to use a more efficient growth strategy. Instead of doubling the capacity when more space is needed, the new implementation grows the memory by 50% or to the required size, whichever is larger. + +```mermaid +sequenceDiagram + participant Memory + participant Vec + Memory->>Memory: ensure_capacity(required_size) + alt required_size > data.len() + Memory->>Memory: new_capacity = (data.len() * 3 / 2).max(required_size) + Memory->>Vec: resize(new_capacity, 0) + else required_size <= data.len() + Memory->>Memory: Do nothing + end +``` + +This approach reduces memory waste while still providing amortized constant-time operations. + +## Opcode Implementations + +### Solana Account Operations + +The following Solana account operations have been implemented: + +- `GetBalance`: Gets the balance (lamports) of an account +- `GetOwner`: Gets the owner of an account +- `IsWritable`: Checks if an account is writable +- `IsSigner`: Checks if an account is a signer + +```mermaid +sequenceDiagram + participant VM + participant Account + VM->>VM: Pop account_idx from stack + VM->>VM: Check if account_idx is valid + alt Valid account_idx + VM->>Account: Get account information + Account->>VM: Return account information + VM->>VM: Push result to stack + else Invalid account_idx + VM->>VM: Return InvalidAccount error + end +``` + +### Graph Operations + +The following graph operations have been implemented: + +- `GraphAddEdge`: Adds an edge to a graph +- `GraphGetNode`: Gets the value of a node +- `GraphSetNode`: Sets the value of a node +- `GraphGetNeighbors`: Gets the neighbors of a node +- `GraphBfs`: Performs a breadth-first search starting from a node +- `GraphClear`: Clears a graph + +```mermaid +sequenceDiagram + participant VM + participant DataStructureStore + participant GraphDS + VM->>VM: Pop weight, to, from, and id from stack + VM->>DataStructureStore: Access graph with id + alt Graph exists + DataStructureStore->>GraphDS: add_edge(from, to, weight) + GraphDS-->>DataStructureStore: Result + else Graph does not exist + DataStructureStore-->>VM: Error: InvalidDataStructureOperation + end +``` + +### OHLCV Operations + +The following OHLCV (Open-High-Low-Close-Volume) operations have been implemented: + +- `OhlcvAddBar`: Adds a bar to an OHLCV +- `OhlcvGetBar`: Gets a bar from an OHLCV +- `OhlcvSma`: Calculates the Simple Moving Average (SMA) of an OHLCV + +```mermaid +sequenceDiagram + participant VM + participant DataStructureStore + participant OHLCVDS + VM->>VM: Pop timestamp, open, high, low, close, volume, and id from stack + VM->>DataStructureStore: Access ohlcv with id + alt OHLCV exists + DataStructureStore->>OHLCVDS: add_entry(timestamp, open, high, low, close, volume) + OHLCVDS-->>DataStructureStore: Result + else OHLCV does not exist + DataStructureStore-->>VM: Error: InvalidDataStructureOperation + end +``` + +### Hypergraph Operations + +The following hypergraph operations have been implemented: + +- `HyperAddNode`: Adds a node to a hypergraph +- `HyperAddEdge`: Adds an edge to a hypergraph +- `HyperAddNodeToEdge`: Adds a node to an edge in a hypergraph + +```mermaid +sequenceDiagram + participant VM + participant DataStructureStore + participant HypergraphDS + VM->>VM: Pop node_id, edge_id, and id from stack + VM->>DataStructureStore: Access hypergraph with id + alt Hypergraph exists + DataStructureStore->>HypergraphDS: add_node_to_edge(edge_id, node_id) + HypergraphDS-->>DataStructureStore: Result + else Hypergraph does not exist + DataStructureStore-->>VM: Error: InvalidDataStructureOperation + end +``` \ No newline at end of file diff --git a/lessvm-solana/src/vm/core.rs b/lessvm-solana/src/vm/core.rs index 461f11f..f2870d5 100644 --- a/lessvm-solana/src/vm/core.rs +++ b/lessvm-solana/src/vm/core.rs @@ -51,6 +51,9 @@ impl ReentrancyGuard { } } +/// Maximum number of data structures that can be created +const MAX_DATA_STRUCTURES: usize = 16; + #[repr(C, align(64))] struct DataStructureStore { btrees: Vec>, @@ -60,6 +63,48 @@ struct DataStructureStore { hypergraphs: Vec>, } +impl DataStructureStore { + fn new() -> Self { + Self { + btrees: Vec::with_capacity(MAX_DATA_STRUCTURES), + tries: Vec::with_capacity(MAX_DATA_STRUCTURES), + graphs: Vec::with_capacity(MAX_DATA_STRUCTURES), + ohlcvs: Vec::with_capacity(MAX_DATA_STRUCTURES), + hypergraphs: Vec::with_capacity(MAX_DATA_STRUCTURES), + } + } + + fn ensure_capacity(&mut self, ds_type: DataStructureType, id: usize) { + match ds_type { + DataStructureType::BTreeMap => { + if id >= self.btrees.len() { + self.btrees.resize_with(id + 1, || None); + } + }, + DataStructureType::Trie => { + if id >= self.tries.len() { + self.tries.resize_with(id + 1, || None); + } + }, + DataStructureType::Graph => { + if id >= self.graphs.len() { + self.graphs.resize_with(id + 1, || None); + } + }, + DataStructureType::OHLCV => { + if id >= self.ohlcvs.len() { + self.ohlcvs.resize_with(id + 1, || None); + } + }, + DataStructureType::Hypergraph => { + if id >= self.hypergraphs.len() { + self.hypergraphs.resize_with(id + 1, || None); + } + }, + } + } +} + #[repr(C, align(64))] pub struct VM<'a> { pc: usize, @@ -139,16 +184,17 @@ impl<'a> VM<'a> { return Err(VMError::StackUnderflow); } - // Get two vectors from the stack (4 values each) - let values1 = _mm256_loadu_si256(self.stack.as_simd_ptr()? as *const __m256i); + // Get the second vector from the stack (4 values) + let values2 = _mm256_loadu_si256(self.stack.as_simd_ptr()? as *const __m256i); - // Pop the first 4 values and get the next 4 values + // Pop the first 4 values to get to the first vector self.stack.pop()?; self.stack.pop()?; self.stack.pop()?; self.stack.pop()?; - let values2 = _mm256_loadu_si256(self.stack.as_simd_ptr()? as *const __m256i); + // Get the first vector from the stack (4 values) + let values1 = _mm256_loadu_si256(self.stack.as_simd_ptr()? as *const __m256i); // Add the two vectors let result = _mm256_add_epi64(values1, values2); @@ -754,6 +800,284 @@ impl<'a> VM<'a> { OpCode::Halt => { break; }, + // Solana Operations that were missing + OpCode::GetBalance => { + let account_idx = self.stack.pop()?.0 as usize; + if account_idx >= self.accounts.accounts.len() { + return Err(VMError::InvalidAccount.into()); + } + let account = &self.accounts.accounts[account_idx]; + self.stack.push(Value(account.lamports()))?; + }, + OpCode::GetOwner => { + let account_idx = self.stack.pop()?.0 as usize; + if account_idx >= self.accounts.accounts.len() { + return Err(VMError::InvalidAccount.into()); + } + let account = &self.accounts.accounts[account_idx]; + // Convert Pubkey to u64 for stack storage (using first 8 bytes) + let owner_bytes = account.owner.to_bytes(); + let mut value_bytes = [0u8; 8]; + value_bytes.copy_from_slice(&owner_bytes[0..8]); + self.stack.push(Value(u64::from_le_bytes(value_bytes)))?; + }, + OpCode::IsWritable => { + let account_idx = self.stack.pop()?.0 as usize; + if account_idx >= self.accounts.accounts.len() { + return Err(VMError::InvalidAccount.into()); + } + let account = &self.accounts.accounts[account_idx]; + self.stack.push(Value(if account.is_writable() { 1 } else { 0 }))?; + }, + OpCode::IsSigner => { + let account_idx = self.stack.pop()?.0 as usize; + if account_idx >= self.accounts.accounts.len() { + return Err(VMError::InvalidAccount.into()); + } + let account = &self.accounts.accounts[account_idx]; + self.stack.push(Value(if account.is_signer { 1 } else { 0 }))?; + }, + // Control Flow + OpCode::Revert => { + let error_code = self.stack.pop()?.0; + return Err(ProgramError::Custom(error_code as u32)); + }, + // Graph operations + OpCode::GraphAddEdge => { + let weight = self.stack.pop()?.0; + let to = self.stack.pop()?.0; + let from = self.stack.pop()?.0; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.graphs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(graph) = &mut self.data_structures.graphs[id] { + graph.add_edge(from, to, weight)?; + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + OpCode::GraphGetNode => { + let node_id = self.stack.pop()?.0; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.graphs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(graph) = &self.data_structures.graphs[id] { + match graph.get_node_value(node_id) { + Some(value) => self.stack.push(Value(value))?, + None => self.stack.push(Value(0))?, // Return 0 if node not found + } + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + OpCode::GraphSetNode => { + let value = self.stack.pop()?.0; + let node_id = self.stack.pop()?.0; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.graphs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(graph) = &mut self.data_structures.graphs[id] { + graph.set_node_value(node_id, value)?; + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + // Graph operations + OpCode::GraphGetNeighbors => { + let node_id = self.stack.pop()?.0; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.graphs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(graph) = &self.data_structures.graphs[id] { + let neighbors = graph.get_neighbors(node_id); + + // Store the number of neighbors on the stack + self.stack.push(Value(neighbors.len() as u64))?; + + // Store each neighbor and its weight on the stack + for (neighbor, weight) in neighbors.iter().rev() { + self.stack.push(Value(*weight))?; + self.stack.push(Value(*neighbor))?; + } + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + OpCode::GraphBfs => { + let start_node = self.stack.pop()?.0; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.graphs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(graph) = &self.data_structures.graphs[id] { + let bfs_result = graph.bfs(start_node); + + // Store the number of nodes in the BFS result on the stack + self.stack.push(Value(bfs_result.len() as u64))?; + + // Store each node in the BFS result on the stack (in reverse order so they come out in the right order when popped) + for node in bfs_result.iter().rev() { + self.stack.push(Value(*node))?; + } + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + OpCode::GraphClear => { + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.graphs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(graph) = &mut self.data_structures.graphs[id] { + graph.clear(); + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + // OHLCV operations + OpCode::OhlcvAddBar => { + let volume = self.stack.pop()?.0; + let close = self.stack.pop()?.0; + let low = self.stack.pop()?.0; + let high = self.stack.pop()?.0; + let open = self.stack.pop()?.0; + let timestamp = self.stack.pop()?.0; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.ohlcvs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + let entry = OHLCVEntry { + timestamp, + open, + high, + low, + close, + volume, + }; + + if let Some(ohlcv) = &mut self.data_structures.ohlcvs[id] { + ohlcv.add_entry(entry)?; + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + OpCode::OhlcvGetBar => { + let index = self.stack.pop()?.0 as usize; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.ohlcvs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(ohlcv) = &self.data_structures.ohlcvs[id] { + match ohlcv.get_entry(index) { + Some(entry) => { + // Push all values in reverse order so they come out in the right order when popped + self.stack.push(Value(entry.volume))?; + self.stack.push(Value(entry.close))?; + self.stack.push(Value(entry.low))?; + self.stack.push(Value(entry.high))?; + self.stack.push(Value(entry.open))?; + self.stack.push(Value(entry.timestamp))?; + }, + None => { + // Push zeros if entry not found + for _ in 0..6 { + self.stack.push(Value(0))?; + } + } + } + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + OpCode::OhlcvSma => { + let period = self.stack.pop()?.0 as usize; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.ohlcvs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(ohlcv) = &self.data_structures.ohlcvs[id] { + let sma_result = ohlcv.calculate_sma(period); + + // Push the number of SMA values + self.stack.push(Value(sma_result.len() as u64))?; + + // Push each SMA value and timestamp in reverse order + for (timestamp, value) in sma_result.iter().rev() { + self.stack.push(Value(*value))?; + self.stack.push(Value(*timestamp))?; + } + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + // Hypergraph operations + OpCode::HyperAddNode => { + let value = self.stack.pop()?.0; + let node_id = self.stack.pop()?.0; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.hypergraphs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(hypergraph) = &mut self.data_structures.hypergraphs[id] { + hypergraph.add_node(node_id, value)?; + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + OpCode::HyperAddEdge => { + let weight = self.stack.pop()?.0; + let edge_id = self.stack.pop()?.0; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.hypergraphs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(hypergraph) = &mut self.data_structures.hypergraphs[id] { + hypergraph.create_hyperedge(edge_id, weight)?; + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, + OpCode::HyperAddNodeToEdge => { + let node_id = self.stack.pop()?.0; + let edge_id = self.stack.pop()?.0; + let id = self.stack.pop()?.0 as usize; + + if id >= self.data_structures.hypergraphs.len() { + return Err(VMError::InvalidDataStructureOperation.into()); + } + + if let Some(hypergraph) = &mut self.data_structures.hypergraphs[id] { + hypergraph.add_node_to_edge(edge_id, node_id)?; + } else { + return Err(VMError::InvalidDataStructureOperation.into()); + } + }, _ => return Err(VMError::InvalidInstruction.into()), } } diff --git a/lessvm-solana/src/vm/data_structures.rs b/lessvm-solana/src/vm/data_structures.rs index 1a8f46b..6885a0c 100644 --- a/lessvm-solana/src/vm/data_structures.rs +++ b/lessvm-solana/src/vm/data_structures.rs @@ -14,6 +14,7 @@ use solana_program::msg; // Constants for data structure IDs pub const MAX_DATA_STRUCTURES: usize = 32; +pub const MAX_GRAPH_NODES: usize = 1024; // Maximum number of nodes in a graph /// The type of data structure #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/lessvm-solana/src/vm/memory.rs b/lessvm-solana/src/vm/memory.rs index 69c9653..6fa23da 100644 --- a/lessvm-solana/src/vm/memory.rs +++ b/lessvm-solana/src/vm/memory.rs @@ -26,8 +26,8 @@ impl Memory { #[inline(always)] pub fn ensure_capacity(&mut self, required_size: usize) { if required_size > self.data.len() { - // Double the capacity or use required size, whichever is larger - let new_capacity = self.data.len().max(required_size).max(1024) * 2; + // Use a more efficient growth strategy: grow by 50% or to required size, whichever is larger + let new_capacity = (self.data.len() * 3 / 2).max(required_size); self.data.resize(new_capacity, 0); } } diff --git a/lessvm-solana/src/vm/tests/data_structure_store_tests.rs b/lessvm-solana/src/vm/tests/data_structure_store_tests.rs new file mode 100644 index 0000000..fd5a024 --- /dev/null +++ b/lessvm-solana/src/vm/tests/data_structure_store_tests.rs @@ -0,0 +1,137 @@ +use super::super::*; +use super::super::data_structures::*; +use solana_program::clock::Epoch; +use solana_program::pubkey::Pubkey; +use solana_program::account_info::AccountInfo; + +fn create_test_account(lamports: u64) -> (Pubkey, Vec, AccountInfo<'static>) { + let key = Pubkey::new_unique(); + let mut lamports = lamports; + let mut data = vec![0; 32]; + + AccountInfo::new( + &key, + true, + true, + &mut lamports, + &mut data, + &Pubkey::new_unique(), + false, + Epoch::default(), + ) +} + +#[test] +fn test_data_structure_store_new() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let vm = VM::new(&program_id, &accounts, &[]); + + // Verify the data structures are initialized with the correct capacity + assert_eq!(vm.data_structures.btrees.capacity(), MAX_DATA_STRUCTURES); + assert_eq!(vm.data_structures.tries.capacity(), MAX_DATA_STRUCTURES); + assert_eq!(vm.data_structures.graphs.capacity(), MAX_DATA_STRUCTURES); + assert_eq!(vm.data_structures.ohlcvs.capacity(), MAX_DATA_STRUCTURES); + assert_eq!(vm.data_structures.hypergraphs.capacity(), MAX_DATA_STRUCTURES); + + // Verify the data structures are initially empty + assert_eq!(vm.data_structures.btrees.len(), 0); + assert_eq!(vm.data_structures.tries.len(), 0); + assert_eq!(vm.data_structures.graphs.len(), 0); + assert_eq!(vm.data_structures.ohlcvs.len(), 0); + assert_eq!(vm.data_structures.hypergraphs.len(), 0); +} + +#[test] +fn test_ensure_capacity_btree() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Initially the btrees vector is empty + assert_eq!(vm.data_structures.btrees.len(), 0); + + // Ensure capacity for index 5 + vm.data_structures.ensure_capacity(DataStructureType::BTreeMap, 5); + + // The vector should now have length 6 (indices 0-5) + assert_eq!(vm.data_structures.btrees.len(), 6); + + // All elements should be None + for i in 0..6 { + assert!(vm.data_structures.btrees[i].is_none()); + } + + // Create a BTreeMap at index 3 + vm.data_structures.btrees[3] = Some(BTreeMapDS::new()); + + // Ensure capacity for index 10 + vm.data_structures.ensure_capacity(DataStructureType::BTreeMap, 10); + + // The vector should now have length 11 (indices 0-10) + assert_eq!(vm.data_structures.btrees.len(), 11); + + // The BTreeMap at index 3 should still exist + assert!(vm.data_structures.btrees[3].is_some()); + + // Other elements should be None + for i in 0..11 { + if i != 3 { + assert!(vm.data_structures.btrees[i].is_none()); + } + } +} + +#[test] +fn test_ensure_capacity_all_types() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Test ensure_capacity for all data structure types + vm.data_structures.ensure_capacity(DataStructureType::BTreeMap, 3); + vm.data_structures.ensure_capacity(DataStructureType::Trie, 4); + vm.data_structures.ensure_capacity(DataStructureType::Graph, 5); + vm.data_structures.ensure_capacity(DataStructureType::OHLCV, 6); + vm.data_structures.ensure_capacity(DataStructureType::Hypergraph, 7); + + // Verify the lengths + assert_eq!(vm.data_structures.btrees.len(), 4); + assert_eq!(vm.data_structures.tries.len(), 5); + assert_eq!(vm.data_structures.graphs.len(), 6); + assert_eq!(vm.data_structures.ohlcvs.len(), 7); + assert_eq!(vm.data_structures.hypergraphs.len(), 8); +} + +#[test] +fn test_data_structure_operations_with_ensure_capacity() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Test BTreeClear with ensure_capacity + vm.stack.push(Value(10)).unwrap(); // id = 10 + let result = vm.execute(&[OpCode::BTreeClear as u8]); + assert!(result.is_ok()); + + // Verify that the BTreeMap was created at index 10 + assert_eq!(vm.data_structures.btrees.len(), 11); + assert!(vm.data_structures.btrees[10].is_some()); + + // Test GraphCreate with ensure_capacity + vm.stack.push(Value(15)).unwrap(); // id = 15 + let result = vm.execute(&[OpCode::GraphCreate as u8]); + assert!(result.is_ok()); + + // Verify that the Graph was created at index 15 + assert_eq!(vm.data_structures.graphs.len(), 16); + assert!(vm.data_structures.graphs[15].is_some()); +} \ No newline at end of file diff --git a/lessvm-solana/src/vm/tests/memory_tests.rs b/lessvm-solana/src/vm/tests/memory_tests.rs new file mode 100644 index 0000000..9e4f47a --- /dev/null +++ b/lessvm-solana/src/vm/tests/memory_tests.rs @@ -0,0 +1,135 @@ +use super::super::*; +use solana_program::clock::Epoch; +use solana_program::pubkey::Pubkey; +use solana_program::account_info::AccountInfo; + +fn create_test_account(lamports: u64) -> (Pubkey, Vec, AccountInfo<'static>) { + let key = Pubkey::new_unique(); + let mut lamports = lamports; + let mut data = vec![0; 32]; + + AccountInfo::new( + &key, + true, + true, + &mut lamports, + &mut data, + &Pubkey::new_unique(), + false, + Epoch::default(), + ) +} + +#[test] +fn test_memory_initial_capacity() { + // Test that memory is initialized with the correct capacity + let memory = Memory::new(); + assert_eq!(memory.capacity(), 1024); + assert_eq!(memory.size(), 0); +} + +#[test] +fn test_memory_growth_strategy() { + // Test the memory growth strategy + let mut memory = Memory::new(); + + // Initial capacity is 1024 bytes + assert_eq!(memory.capacity(), 1024); + + // Store data that fits within the initial capacity + let data = vec![1; 1000]; + memory.store(0, &data).unwrap(); + assert_eq!(memory.capacity(), 1024); + + // Store data that exceeds the initial capacity + let data = vec![2; 1024]; + memory.store(1000, &data).unwrap(); + + // Capacity should grow by 50% (1024 * 1.5 = 1536) or to the required size (1000 + 1024 = 2024), + // whichever is larger + assert_eq!(memory.capacity(), 2024); + + // Store more data to trigger another growth + let data = vec![3; 1000]; + memory.store(2024, &data).unwrap(); + + // New capacity should be 2024 * 1.5 = 3036 + assert_eq!(memory.capacity(), 3036); +} + +#[test] +fn test_memory_operations_with_growth() { + let mut memory = Memory::new(); + + // Fill memory with a pattern + for i in 0..1024 { + memory.store8(i, (i % 256) as u8).unwrap(); + } + + // Verify the pattern + for i in 0..1024 { + assert_eq!(memory.load8(i).unwrap(), (i % 256) as u8); + } + + // Store beyond the initial capacity + let large_data = vec![0xAA; 2000]; + memory.store(1024, &large_data).unwrap(); + + // Verify the original data is intact + for i in 0..1024 { + assert_eq!(memory.load8(i).unwrap(), (i % 256) as u8); + } + + // Verify the new data + for i in 0..2000 { + assert_eq!(memory.load8(1024 + i).unwrap(), 0xAA); + } + + // Test copy operation across the growth boundary + memory.copy(3000, 500, 100).unwrap(); + + // Verify the copied data + for i in 0..100 { + assert_eq!(memory.load8(3000 + i).unwrap(), ((500 + i) % 256) as u8); + } +} + +#[test] +fn test_memory_with_vm_operations() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Create a program that stores data beyond the initial memory capacity + // push8 , push8 , store, halt + let mut bytecode = vec![]; + + // Store a value at offset 2000 (beyond initial capacity) + bytecode.extend_from_slice(&[ + OpCode::Push8 as u8, + ]); + bytecode.extend_from_slice(&0x1234567890ABCDEFu64.to_le_bytes()); + bytecode.extend_from_slice(&[ + OpCode::Push8 as u8, + ]); + bytecode.extend_from_slice(&2000u64.to_le_bytes()); + bytecode.extend_from_slice(&[ + OpCode::Store as u8, + OpCode::Halt as u8, + ]); + + // Execute the program + let result = vm.execute(&bytecode); + assert!(result.is_ok()); + + // Verify the memory capacity has grown + assert!(vm.memory.capacity() > 1024); + + // Verify the stored value + vm.stack.push(Value(2000)).unwrap(); + let result = vm.execute(&[OpCode::Load as u8]); + assert!(result.is_ok()); + assert_eq!(vm.stack.pop().unwrap().0, 0x1234567890ABCDEF); +} \ No newline at end of file diff --git a/lessvm-solana/src/vm/tests/mod.rs b/lessvm-solana/src/vm/tests/mod.rs index 51c9b1f..bc2ab29 100644 --- a/lessvm-solana/src/vm/tests/mod.rs +++ b/lessvm-solana/src/vm/tests/mod.rs @@ -1,2 +1,14 @@ #[cfg(test)] -pub mod data_structures_tests; \ No newline at end of file +pub mod data_structures_tests; + +#[cfg(test)] +pub mod vector_add_tests; + +#[cfg(test)] +pub mod data_structure_store_tests; + +#[cfg(test)] +pub mod opcode_tests; + +#[cfg(test)] +pub mod memory_tests; \ No newline at end of file diff --git a/lessvm-solana/src/vm/tests/opcode_tests.rs b/lessvm-solana/src/vm/tests/opcode_tests.rs new file mode 100644 index 0000000..e23bbd5 --- /dev/null +++ b/lessvm-solana/src/vm/tests/opcode_tests.rs @@ -0,0 +1,379 @@ +use super::super::*; +use super::super::data_structures::*; +use solana_program::clock::Epoch; +use solana_program::pubkey::Pubkey; +use solana_program::account_info::AccountInfo; + +fn create_test_account(lamports: u64) -> (Pubkey, Vec, AccountInfo<'static>) { + let key = Pubkey::new_unique(); + let mut lamports = lamports; + let mut data = vec![0; 32]; + + AccountInfo::new( + &key, + true, + true, + &mut lamports, + &mut data, + &Pubkey::new_unique(), + false, + Epoch::default(), + ) +} + +// Test for Solana account operations +#[test] +fn test_solana_account_operations() { + let program_id = Pubkey::new_unique(); + let (_, _, account1) = create_test_account(1000000); + let (_, _, account2) = create_test_account(500000); + let accounts = vec![account1, account2]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Test GetBalance + vm.stack.push(Value(0)).unwrap(); // account index 0 + let result = vm.execute(&[OpCode::GetBalance as u8]); + assert!(result.is_ok()); + assert_eq!(vm.stack.pop().unwrap().0, 1000000); + + // Test IsWritable + vm.stack.push(Value(1)).unwrap(); // account index 1 + let result = vm.execute(&[OpCode::IsWritable as u8]); + assert!(result.is_ok()); + assert_eq!(vm.stack.pop().unwrap().0, 1); // account is writable + + // Test IsSigner + vm.stack.push(Value(0)).unwrap(); // account index 0 + let result = vm.execute(&[OpCode::IsSigner as u8]); + assert!(result.is_ok()); + assert_eq!(vm.stack.pop().unwrap().0, 1); // account is signer +} + +// Test for Graph operations +#[test] +fn test_graph_operations() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Create a graph + vm.stack.push(Value(0)).unwrap(); // graph id 0 + let result = vm.execute(&[OpCode::GraphCreate as u8]); + assert!(result.is_ok()); + + // Add nodes + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(1)).unwrap(); // node id + vm.stack.push(Value(100)).unwrap(); // node value + let result = vm.execute(&[OpCode::GraphAddNode as u8]); + assert!(result.is_ok()); + + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(2)).unwrap(); // node id + vm.stack.push(Value(200)).unwrap(); // node value + let result = vm.execute(&[OpCode::GraphAddNode as u8]); + assert!(result.is_ok()); + + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(3)).unwrap(); // node id + vm.stack.push(Value(300)).unwrap(); // node value + let result = vm.execute(&[OpCode::GraphAddNode as u8]); + assert!(result.is_ok()); + + // Add edges + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(1)).unwrap(); // from node + vm.stack.push(Value(2)).unwrap(); // to node + vm.stack.push(Value(10)).unwrap(); // weight + let result = vm.execute(&[OpCode::GraphAddEdge as u8]); + assert!(result.is_ok()); + + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(1)).unwrap(); // from node + vm.stack.push(Value(3)).unwrap(); // to node + vm.stack.push(Value(20)).unwrap(); // weight + let result = vm.execute(&[OpCode::GraphAddEdge as u8]); + assert!(result.is_ok()); + + // Get node value + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(2)).unwrap(); // node id + let result = vm.execute(&[OpCode::GraphGetNode as u8]); + assert!(result.is_ok()); + assert_eq!(vm.stack.pop().unwrap().0, 200); + + // Set node value + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(2)).unwrap(); // node id + vm.stack.push(Value(250)).unwrap(); // new value + let result = vm.execute(&[OpCode::GraphSetNode as u8]); + assert!(result.is_ok()); + + // Verify the new value + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(2)).unwrap(); // node id + let result = vm.execute(&[OpCode::GraphGetNode as u8]); + assert!(result.is_ok()); + assert_eq!(vm.stack.pop().unwrap().0, 250); + + // Get neighbors + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(1)).unwrap(); // node id + let result = vm.execute(&[OpCode::GraphGetNeighbors as u8]); + assert!(result.is_ok()); + + // Stack should have: [count, neighbor1, weight1, neighbor2, weight2] + let count = vm.stack.pop().unwrap().0; + assert_eq!(count, 2); + + // Pop in reverse order of how they were pushed + let neighbor2 = vm.stack.pop().unwrap().0; + let weight2 = vm.stack.pop().unwrap().0; + let neighbor1 = vm.stack.pop().unwrap().0; + let weight1 = vm.stack.pop().unwrap().0; + + // Check the neighbors (order may vary) + if neighbor1 == 2 { + assert_eq!(weight1, 10); + assert_eq!(neighbor2, 3); + assert_eq!(weight2, 20); + } else { + assert_eq!(neighbor1, 3); + assert_eq!(weight1, 20); + assert_eq!(neighbor2, 2); + assert_eq!(weight2, 10); + } + + // Test BFS + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(1)).unwrap(); // start node + let result = vm.execute(&[OpCode::GraphBfs as u8]); + assert!(result.is_ok()); + + // Stack should have: [count, node1, node2, node3] + let count = vm.stack.pop().unwrap().0; + assert_eq!(count, 3); + + // Pop nodes in BFS order + let node3 = vm.stack.pop().unwrap().0; + let node2 = vm.stack.pop().unwrap().0; + let node1 = vm.stack.pop().unwrap().0; + + assert_eq!(node1, 1); // Start node + // node2 and node3 could be in either order depending on implementation + assert!( + (node2 == 2 && node3 == 3) || + (node2 == 3 && node3 == 2) + ); + + // Test GraphClear + vm.stack.push(Value(0)).unwrap(); // graph id + let result = vm.execute(&[OpCode::GraphClear as u8]); + assert!(result.is_ok()); + + // Verify the graph is empty + vm.stack.push(Value(0)).unwrap(); // graph id + vm.stack.push(Value(1)).unwrap(); // node id + let result = vm.execute(&[OpCode::GraphGetNode as u8]); + assert!(result.is_ok()); + assert_eq!(vm.stack.pop().unwrap().0, 0); // Should return 0 for non-existent node +} + +// Test for OHLCV operations +#[test] +fn test_ohlcv_operations() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Create an OHLCV + vm.stack.push(Value(0)).unwrap(); // ohlcv id 0 + let result = vm.execute(&[OpCode::OhlcvCreate as u8]); + assert!(result.is_ok()); + + // Add bars + // Bar 1: timestamp=1000, open=100, high=110, low=90, close=105, volume=1000 + vm.stack.push(Value(0)).unwrap(); // ohlcv id + vm.stack.push(Value(1000)).unwrap(); // timestamp + vm.stack.push(Value(100)).unwrap(); // open + vm.stack.push(Value(110)).unwrap(); // high + vm.stack.push(Value(90)).unwrap(); // low + vm.stack.push(Value(105)).unwrap(); // close + vm.stack.push(Value(1000)).unwrap(); // volume + let result = vm.execute(&[OpCode::OhlcvAddBar as u8]); + assert!(result.is_ok()); + + // Bar 2: timestamp=2000, open=105, high=120, low=100, close=115, volume=1500 + vm.stack.push(Value(0)).unwrap(); // ohlcv id + vm.stack.push(Value(2000)).unwrap(); // timestamp + vm.stack.push(Value(105)).unwrap(); // open + vm.stack.push(Value(120)).unwrap(); // high + vm.stack.push(Value(100)).unwrap(); // low + vm.stack.push(Value(115)).unwrap(); // close + vm.stack.push(Value(1500)).unwrap(); // volume + let result = vm.execute(&[OpCode::OhlcvAddBar as u8]); + assert!(result.is_ok()); + + // Bar 3: timestamp=3000, open=115, high=130, low=110, close=125, volume=2000 + vm.stack.push(Value(0)).unwrap(); // ohlcv id + vm.stack.push(Value(3000)).unwrap(); // timestamp + vm.stack.push(Value(115)).unwrap(); // open + vm.stack.push(Value(130)).unwrap(); // high + vm.stack.push(Value(110)).unwrap(); // low + vm.stack.push(Value(125)).unwrap(); // close + vm.stack.push(Value(2000)).unwrap(); // volume + let result = vm.execute(&[OpCode::OhlcvAddBar as u8]); + assert!(result.is_ok()); + + // Get bar + vm.stack.push(Value(0)).unwrap(); // ohlcv id + vm.stack.push(Value(1)).unwrap(); // index + let result = vm.execute(&[OpCode::OhlcvGetBar as u8]); + assert!(result.is_ok()); + + // Stack should have: [timestamp, open, high, low, close, volume] + let volume = vm.stack.pop().unwrap().0; + let close = vm.stack.pop().unwrap().0; + let low = vm.stack.pop().unwrap().0; + let high = vm.stack.pop().unwrap().0; + let open = vm.stack.pop().unwrap().0; + let timestamp = vm.stack.pop().unwrap().0; + + assert_eq!(timestamp, 2000); + assert_eq!(open, 105); + assert_eq!(high, 120); + assert_eq!(low, 100); + assert_eq!(close, 115); + assert_eq!(volume, 1500); + + // Calculate SMA with period 2 + vm.stack.push(Value(0)).unwrap(); // ohlcv id + vm.stack.push(Value(2)).unwrap(); // period + let result = vm.execute(&[OpCode::OhlcvSma as u8]); + assert!(result.is_ok()); + + // Stack should have: [count, timestamp1, value1, timestamp2, value2] + let count = vm.stack.pop().unwrap().0; + assert_eq!(count, 2); + + // Pop SMA values + let timestamp2 = vm.stack.pop().unwrap().0; + let value2 = vm.stack.pop().unwrap().0; + let timestamp1 = vm.stack.pop().unwrap().0; + let value1 = vm.stack.pop().unwrap().0; + + assert_eq!(timestamp1, 2000); + assert_eq!(value1, (105 + 115) / 2); // (bar1.close + bar2.close) / 2 + assert_eq!(timestamp2, 3000); + assert_eq!(value2, (115 + 125) / 2); // (bar2.close + bar3.close) / 2 +} + +// Test for Hypergraph operations +#[test] +fn test_hypergraph_operations() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Create a hypergraph + vm.stack.push(Value(0)).unwrap(); // hypergraph id 0 + let result = vm.execute(&[OpCode::HyperCreate as u8]); + assert!(result.is_ok()); + + // Add nodes + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(1)).unwrap(); // node id + vm.stack.push(Value(100)).unwrap(); // node value + let result = vm.execute(&[OpCode::HyperAddNode as u8]); + assert!(result.is_ok()); + + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(2)).unwrap(); // node id + vm.stack.push(Value(200)).unwrap(); // node value + let result = vm.execute(&[OpCode::HyperAddNode as u8]); + assert!(result.is_ok()); + + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(3)).unwrap(); // node id + vm.stack.push(Value(300)).unwrap(); // node value + let result = vm.execute(&[OpCode::HyperAddNode as u8]); + assert!(result.is_ok()); + + // Create hyperedges + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(101)).unwrap(); // edge id + vm.stack.push(Value(10)).unwrap(); // weight + let result = vm.execute(&[OpCode::HyperAddEdge as u8]); + assert!(result.is_ok()); + + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(102)).unwrap(); // edge id + vm.stack.push(Value(20)).unwrap(); // weight + let result = vm.execute(&[OpCode::HyperAddEdge as u8]); + assert!(result.is_ok()); + + // Add nodes to edges + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(101)).unwrap(); // edge id + vm.stack.push(Value(1)).unwrap(); // node id + let result = vm.execute(&[OpCode::HyperAddNodeToEdge as u8]); + assert!(result.is_ok()); + + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(101)).unwrap(); // edge id + vm.stack.push(Value(2)).unwrap(); // node id + let result = vm.execute(&[OpCode::HyperAddNodeToEdge as u8]); + assert!(result.is_ok()); + + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(102)).unwrap(); // edge id + vm.stack.push(Value(2)).unwrap(); // node id + let result = vm.execute(&[OpCode::HyperAddNodeToEdge as u8]); + assert!(result.is_ok()); + + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(102)).unwrap(); // edge id + vm.stack.push(Value(3)).unwrap(); // node id + let result = vm.execute(&[OpCode::HyperAddNodeToEdge as u8]); + assert!(result.is_ok()); + + // Test auto-creation of nodes and edges + vm.stack.push(Value(0)).unwrap(); // hypergraph id + vm.stack.push(Value(103)).unwrap(); // new edge id + vm.stack.push(Value(4)).unwrap(); // new node id + let result = vm.execute(&[OpCode::HyperAddNodeToEdge as u8]); + assert!(result.is_ok()); +} + +// Test for Revert opcode +#[test] +fn test_revert_opcode() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Push error code 42 + vm.stack.push(Value(42)).unwrap(); + + // Execute Revert + let result = vm.execute(&[OpCode::Revert as u8]); + + // Should return a ProgramError::Custom with code 42 + assert!(result.is_err()); + if let Err(err) = result { + if let solana_program::program_error::ProgramError::Custom(code) = err { + assert_eq!(code, 42); + } else { + panic!("Expected ProgramError::Custom, got {:?}", err); + } + } +} \ No newline at end of file diff --git a/lessvm-solana/src/vm/tests/vector_add_tests.rs b/lessvm-solana/src/vm/tests/vector_add_tests.rs new file mode 100644 index 0000000..0e58608 --- /dev/null +++ b/lessvm-solana/src/vm/tests/vector_add_tests.rs @@ -0,0 +1,107 @@ +use super::super::*; +use solana_program::clock::Epoch; +use solana_program::pubkey::Pubkey; +use solana_program::account_info::AccountInfo; + +#[cfg(target_arch = "x86_64")] +use std::arch::x86_64::*; + +fn create_test_account(lamports: u64) -> (Pubkey, Vec, AccountInfo<'static>) { + let key = Pubkey::new_unique(); + let mut lamports = lamports; + let mut data = vec![0; 32]; + + AccountInfo::new( + &key, + true, + true, + &mut lamports, + &mut data, + &Pubkey::new_unique(), + false, + Epoch::default(), + ) +} + +#[test] +#[cfg(target_arch = "x86_64")] +fn test_vector_add() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Push 8 values to the stack: [1, 2, 3, 4, 5, 6, 7, 8] + for i in 1..=8 { + vm.stack.push(Value(i)).unwrap(); + } + + // Execute vector_add operation + unsafe { + vm.vector_add().unwrap(); + } + + // The stack should now have 4 values: [6, 8, 10, 12] + // These are the sums of [1+5, 2+6, 3+7, 4+8] + assert_eq!(vm.stack.depth(), 4); + assert_eq!(vm.stack.pop().unwrap().0, 12); + assert_eq!(vm.stack.pop().unwrap().0, 10); + assert_eq!(vm.stack.pop().unwrap().0, 8); + assert_eq!(vm.stack.pop().unwrap().0, 6); +} + +#[test] +#[cfg(target_arch = "x86_64")] +fn test_vector_add_with_different_values() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Push 8 values to the stack with different patterns + vm.stack.push(Value(10)).unwrap(); + vm.stack.push(Value(20)).unwrap(); + vm.stack.push(Value(30)).unwrap(); + vm.stack.push(Value(40)).unwrap(); + vm.stack.push(Value(5)).unwrap(); + vm.stack.push(Value(15)).unwrap(); + vm.stack.push(Value(25)).unwrap(); + vm.stack.push(Value(35)).unwrap(); + + // Execute vector_add operation + unsafe { + vm.vector_add().unwrap(); + } + + // The stack should now have 4 values: [15, 35, 55, 75] + // These are the sums of [10+5, 20+15, 30+25, 40+35] + assert_eq!(vm.stack.depth(), 4); + assert_eq!(vm.stack.pop().unwrap().0, 75); + assert_eq!(vm.stack.pop().unwrap().0, 55); + assert_eq!(vm.stack.pop().unwrap().0, 35); + assert_eq!(vm.stack.pop().unwrap().0, 15); +} + +#[test] +#[cfg(target_arch = "x86_64")] +fn test_vector_add_stack_underflow() { + let program_id = Pubkey::new_unique(); + let (_, _, account) = create_test_account(1000000); + let accounts = vec![account]; + + let mut vm = VM::new(&program_id, &accounts, &[]); + + // Push only 7 values to the stack (not enough for vector_add) + for i in 1..=7 { + vm.stack.push(Value(i)).unwrap(); + } + + // Execute vector_add operation - should fail with StackUnderflow + unsafe { + let result = vm.vector_add(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), VMError::StackUnderflow); + } +} \ No newline at end of file