Skip to main content

hopper_core/virtual_state/
mod.rs

1//! Account Virtualization.
2//!
3//! Virtual state lets protocols model logical state that spans multiple
4//! Solana accounts. Use cases:
5//!
6//! - Protocol state larger than the 10 MiB account limit
7//! - Sharded systems (order books, AMM pools, registries)
8//! - Multi-account logical entities (e.g. a "Market" = OrderBook + Pool + Config)
9//!
10//! ## How It Works
11//!
12//! A `VirtualState` maps N logical slots to physical accounts in the
13//! instruction's account array. At runtime it provides unified typed
14//! access across all constituent accounts.
15//!
16//! ```text
17//! +--------------+  +--------------+  +--------------+
18//! |  Account 0   |  |  Account 1   |  |  Account 2   |
19//! |  MarketCore  |  |  OrderBook   |  |  PoolState   |
20//! +------+-------+  +------+-------+  +------+-------+
21//!        |                 |                 |
22//!        +-----------------+-----------------+
23//!                          |
24//!                +---------v---------+
25//!                |  VirtualState     |
26//!                |  "Market"         |
27//!                |  - core: 0        |
28//!                |  - orders: 1      |
29//!                |  - pool: 2        |
30//!                +-------------------+
31//! ```
32//!
33//! ## Usage
34//!
35//! ```ignore
36//! // Define the virtual mapping
37//! let vstate = VirtualState::<3>::new()
38//!     .map(0, CORE_IDX)     // slot 0 -> account CORE_IDX
39//!     .map(1, ORDERS_IDX)   // slot 1 -> account ORDERS_IDX
40//!     .map(2, POOL_IDX);    // slot 2 -> account POOL_IDX
41//!
42//! // Read from any slot through the virtual view
43//! let core = vstate.overlay::<MarketCore>(accounts, 0)?;
44//! let book = vstate.overlay::<OrderBook>(accounts, 1)?;
45//! ```
46
47use crate::account::{FixedLayout, Pod};
48use hopper_runtime::{error::ProgramError, AccountView, Address, Ref, RefMut};
49
50// -- Virtual Slot --
51
52/// A mapping from virtual slot index to account index.
53#[derive(Clone, Copy)]
54pub struct VirtualSlot {
55    /// Index into the instruction's account array.
56    pub account_index: u8,
57    /// Expected owner (0 = skip owner check, program_id used).
58    pub require_owned: bool,
59    /// Whether this slot must be writable.
60    pub require_writable: bool,
61}
62
63impl VirtualSlot {
64    /// Create a read-only virtual slot.
65    #[inline(always)]
66    pub const fn read_only(account_index: u8) -> Self {
67        Self {
68            account_index,
69            require_owned: true,
70            require_writable: false,
71        }
72    }
73
74    /// Create a writable virtual slot.
75    #[inline(always)]
76    pub const fn writable(account_index: u8) -> Self {
77        Self {
78            account_index,
79            require_owned: true,
80            require_writable: true,
81        }
82    }
83
84    /// Create an unowned slot (for foreign account reads).
85    #[inline(always)]
86    pub const fn foreign(account_index: u8) -> Self {
87        Self {
88            account_index,
89            require_owned: false,
90            require_writable: false,
91        }
92    }
93}
94
95// -- Virtual State --
96
97/// A virtual state assembly mapping `N` slots to accounts.
98///
99/// Stack-allocated, const-generic. No heap, no alloc.
100pub struct VirtualState<const N: usize> {
101    slots: [VirtualSlot; N],
102    count: usize,
103}
104
105impl<const N: usize> VirtualState<N> {
106    /// Create a new empty virtual state.
107    #[inline(always)]
108    pub const fn new() -> Self {
109        Self {
110            slots: [VirtualSlot {
111                account_index: 0,
112                require_owned: false,
113                require_writable: false,
114            }; N],
115            count: 0,
116        }
117    }
118
119    /// Map a virtual slot to an account index (read-only owned).
120    #[inline(always)]
121    pub const fn map(mut self, slot: usize, account_index: u8) -> Self {
122        assert!(slot < N, "slot index out of bounds");
123        self.slots[slot] = VirtualSlot::read_only(account_index);
124        if slot >= self.count {
125            self.count = slot + 1;
126        }
127        self
128    }
129
130    /// Map a writable virtual slot.
131    #[inline(always)]
132    pub const fn map_mut(mut self, slot: usize, account_index: u8) -> Self {
133        assert!(slot < N, "slot index out of bounds");
134        self.slots[slot] = VirtualSlot::writable(account_index);
135        if slot >= self.count {
136            self.count = slot + 1;
137        }
138        self
139    }
140
141    /// Map a foreign (unowned) virtual slot.
142    #[inline(always)]
143    pub const fn map_foreign(mut self, slot: usize, account_index: u8) -> Self {
144        assert!(slot < N, "slot index out of bounds");
145        self.slots[slot] = VirtualSlot::foreign(account_index);
146        if slot >= self.count {
147            self.count = slot + 1;
148        }
149        self
150    }
151
152    /// Set a slot directly. Used by the `hopper_virtual!` macro for
153    /// custom slot configurations that don't fit the standard map/map_mut/map_foreign
154    /// builder methods (e.g., writable but unowned).
155    #[inline(always)]
156    pub const fn set_slot(mut self, slot: usize, vs: VirtualSlot) -> Self {
157        assert!(slot < N, "slot index out of bounds");
158        self.slots[slot] = vs;
159        if slot >= self.count {
160            self.count = slot + 1;
161        }
162        self
163    }
164
165    /// Number of mapped slots (highest slot index + 1).
166    #[inline(always)]
167    pub const fn slot_count(&self) -> usize {
168        self.count
169    }
170
171    /// Validate all slots against the instruction accounts.
172    ///
173    /// Checks: account bounds, ownership, writability.
174    #[inline]
175    pub fn validate(
176        &self,
177        accounts: &[AccountView],
178        program_id: &Address,
179    ) -> Result<(), ProgramError> {
180        let mut i = 0;
181        while i < self.count {
182            let slot = &self.slots[i];
183            let idx = slot.account_index as usize;
184            if idx >= accounts.len() {
185                return Err(ProgramError::NotEnoughAccountKeys);
186            }
187            let acc = &accounts[idx];
188
189            if slot.require_owned {
190                crate::check::check_owner(acc, program_id)?;
191            }
192            if slot.require_writable {
193                crate::check::check_writable(acc)?;
194            }
195            i += 1;
196        }
197        Ok(())
198    }
199
200    /// Get a typed immutable overlay from a virtual slot.
201    #[inline]
202    pub fn overlay<'a, T: Pod + FixedLayout>(
203        &self,
204        accounts: &'a [AccountView],
205        slot: usize,
206    ) -> Result<Ref<'a, T>, ProgramError> {
207        if slot >= self.count {
208            return Err(ProgramError::InvalidArgument);
209        }
210        let idx = self.slots[slot].account_index as usize;
211        if idx >= accounts.len() {
212            return Err(ProgramError::NotEnoughAccountKeys);
213        }
214        let acc = &accounts[idx];
215        // SAFETY: Pod + FixedLayout types have valid bit patterns;
216        // the backend borrow guard still enforces per-account aliasing.
217        unsafe { acc.raw_ref::<T>() }
218    }
219
220    /// Get a typed mutable overlay from a virtual slot.
221    ///
222    /// # Safety rationale for `mut_from_ref`
223    /// The `&self` receiver is sound because hopper-native's `AccountView` uses
224    /// Solana runtime interior mutability (pointer-based access to account data).
225    /// The slot's `require_writable` flag is checked to ensure we only mutate
226    /// accounts the runtime has granted write access to.
227    #[inline]
228    #[allow(clippy::mut_from_ref)]
229    pub fn overlay_mut<'a, T: Pod + FixedLayout>(
230        &self,
231        accounts: &'a [AccountView],
232        slot: usize,
233    ) -> Result<RefMut<'a, T>, ProgramError> {
234        if slot >= self.count {
235            return Err(ProgramError::InvalidArgument);
236        }
237        let vs = &self.slots[slot];
238        if !vs.require_writable {
239            return Err(ProgramError::InvalidArgument);
240        }
241        let idx = vs.account_index as usize;
242        if idx >= accounts.len() {
243            return Err(ProgramError::NotEnoughAccountKeys);
244        }
245        let acc = &accounts[idx];
246        // SAFETY: Pod + FixedLayout types have valid bit patterns;
247        // writable check is done above; backend borrow guard enforces aliasing.
248        unsafe { acc.raw_mut::<T>() }
249    }
250
251    /// Get raw immutable data from a virtual slot.
252    #[inline]
253    pub fn data<'a>(
254        &self,
255        accounts: &'a [AccountView],
256        slot: usize,
257    ) -> Result<Ref<'a, [u8]>, ProgramError> {
258        if slot >= self.count {
259            return Err(ProgramError::InvalidArgument);
260        }
261        let idx = self.slots[slot].account_index as usize;
262        if idx >= accounts.len() {
263            return Err(ProgramError::NotEnoughAccountKeys);
264        }
265        accounts[idx].try_borrow()
266    }
267
268    /// Get the AccountView for a virtual slot.
269    #[inline]
270    pub fn account<'a>(
271        &self,
272        accounts: &'a [AccountView],
273        slot: usize,
274    ) -> Result<&'a AccountView, ProgramError> {
275        if slot >= self.count {
276            return Err(ProgramError::InvalidArgument);
277        }
278        let idx = self.slots[slot].account_index as usize;
279        accounts.get(idx).ok_or(ProgramError::NotEnoughAccountKeys)
280    }
281}
282
283impl<const N: usize> Default for VirtualState<N> {
284    fn default() -> Self {
285        Self::new()
286    }
287}
288
289// -- Sharded Collection --
290
291/// A sharded collection that distributes entries across multiple accounts.
292///
293/// Each shard is an account containing a `FixedVec<T>`. The shard index
294/// is determined by a key hash.
295///
296/// This enables collections that exceed single-account size limits.
297pub struct ShardedAccess<'a, const SHARDS: usize> {
298    accounts: &'a [AccountView],
299    shard_indices: [u8; SHARDS],
300    shard_count: usize,
301}
302
303impl<'a, const SHARDS: usize> ShardedAccess<'a, SHARDS> {
304    /// Create a sharded access from account indices.
305    #[inline]
306    pub fn new(accounts: &'a [AccountView], shard_indices: &[u8]) -> Result<Self, ProgramError> {
307        if shard_indices.len() > SHARDS {
308            return Err(ProgramError::InvalidArgument);
309        }
310        let mut indices = [0u8; SHARDS];
311        let mut i = 0;
312        while i < shard_indices.len() {
313            if shard_indices[i] as usize >= accounts.len() {
314                return Err(ProgramError::NotEnoughAccountKeys);
315            }
316            indices[i] = shard_indices[i];
317            i += 1;
318        }
319        Ok(Self {
320            accounts,
321            shard_indices: indices,
322            shard_count: shard_indices.len(),
323        })
324    }
325
326    /// Determine which shard a key maps to (simple modular hashing).
327    #[inline(always)]
328    pub fn shard_for_key(&self, key: &[u8]) -> usize {
329        // FNV-1a hash for shard selection
330        let mut hash: u32 = 0x811c_9dc5;
331        let mut i = 0;
332        while i < key.len() {
333            hash ^= key[i] as u32;
334            hash = hash.wrapping_mul(0x0100_0193);
335            i += 1;
336        }
337        (hash as usize) % self.shard_count
338    }
339
340    /// Get the account for a given shard index.
341    #[inline]
342    pub fn shard_account(&self, shard: usize) -> Result<&'a AccountView, ProgramError> {
343        if shard >= self.shard_count {
344            return Err(ProgramError::InvalidArgument);
345        }
346        let idx = self.shard_indices[shard] as usize;
347        self.accounts
348            .get(idx)
349            .ok_or(ProgramError::NotEnoughAccountKeys)
350    }
351
352    /// Get the account data for the shard that owns a given key.
353    #[inline]
354    pub fn data_for_key(&self, key: &[u8]) -> Result<Ref<'a, [u8]>, ProgramError> {
355        let shard = self.shard_for_key(key);
356        let acc = self.shard_account(shard)?;
357        acc.try_borrow()
358    }
359
360    /// Number of shards.
361    #[inline(always)]
362    pub fn shard_count(&self) -> usize {
363        self.shard_count
364    }
365}