Skip to main content

hopper_core/account/
dynamic.rs

1//! Inline dynamic fields using prefix-based variable-length data.
2//!
3//! For accounts with 1-3 variable-length fields (strings, byte arrays),
4//! inline dynamic fields are more efficient than a full segment table.
5//!
6//! Wire format:
7//! ```text
8//! [fixed_prefix][prefix_1: LenType][data_1: N bytes][prefix_2: LenType][data_2: M bytes]...
9//! ```
10//!
11//! Each dynamic field is preceded by a length prefix (u8, u16, or u32)
12//! indicating the actual length of the data that follows. Maximum length
13//! is set at layout definition time.
14//!
15//! ## Key Design
16//!
17//! - **Offset caching**: On first parse, walk the byte stream once and cache
18//!   cumulative offsets in a `[u32; N]` stack array. All subsequent field
19//!   accesses are O(1) via the cached offsets.
20//! - **Layout_id integration**: Dynamic field names/types/max sizes are part
21//!   of the layout_id hash, so schema changes are detected.
22//! - **Zero-copy reads**: String/byte slice access returns `&[u8]` directly
23//!   from account data -- no copy needed for reads.
24//!
25//! ## Comparison to Segments
26//!
27//! | | Segments | Inline Dynamic |
28//! |---|---|---|
29//! | Overhead per field | 12 bytes (descriptor) | 1-4 bytes (prefix) |
30//! | Best for | Multiple large arrays | 1-3 small variable fields |
31//! | Capacity tracking | Explicit | Implicit (max from layout) |
32//! | Cross-program readable | Self-describing | Requires schema knowledge |
33
34use hopper_runtime::error::ProgramError;
35
36/// Read a u8-prefixed dynamic field: returns (data_slice, next_offset).
37#[inline(always)]
38pub fn read_dynamic_u8(data: &[u8], offset: usize) -> Result<(&[u8], usize), ProgramError> {
39    if offset >= data.len() {
40        return Err(ProgramError::AccountDataTooSmall);
41    }
42    let len = data[offset] as usize;
43    let data_start = offset + 1;
44    let data_end = data_start + len;
45    if data_end > data.len() {
46        return Err(ProgramError::AccountDataTooSmall);
47    }
48    Ok((&data[data_start..data_end], data_end))
49}
50
51/// Read a u16-prefixed dynamic field.
52#[inline(always)]
53pub fn read_dynamic_u16(data: &[u8], offset: usize) -> Result<(&[u8], usize), ProgramError> {
54    if offset + 2 > data.len() {
55        return Err(ProgramError::AccountDataTooSmall);
56    }
57    let len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
58    let data_start = offset + 2;
59    let data_end = data_start + len;
60    if data_end > data.len() {
61        return Err(ProgramError::AccountDataTooSmall);
62    }
63    Ok((&data[data_start..data_end], data_end))
64}
65
66/// Read a u32-prefixed dynamic field.
67#[inline(always)]
68pub fn read_dynamic_u32(data: &[u8], offset: usize) -> Result<(&[u8], usize), ProgramError> {
69    if offset + 4 > data.len() {
70        return Err(ProgramError::AccountDataTooSmall);
71    }
72    let len = u32::from_le_bytes([
73        data[offset],
74        data[offset + 1],
75        data[offset + 2],
76        data[offset + 3],
77    ]) as usize;
78    let data_start = offset + 4;
79    let data_end = data_start + len;
80    if data_end > data.len() {
81        return Err(ProgramError::AccountDataTooSmall);
82    }
83    Ok((&data[data_start..data_end], data_end))
84}
85
86/// Write a u8-prefixed dynamic field. Returns next offset after written data.
87#[inline(always)]
88pub fn write_dynamic_u8(
89    data: &mut [u8],
90    offset: usize,
91    value: &[u8],
92    max_len: usize,
93) -> Result<usize, ProgramError> {
94    if value.len() > max_len || value.len() > 255 {
95        return Err(ProgramError::InvalidInstructionData);
96    }
97    let data_start = offset + 1;
98    let data_end = data_start + value.len();
99    if data_end > data.len() {
100        return Err(ProgramError::AccountDataTooSmall);
101    }
102    data[offset] = value.len() as u8;
103    data[data_start..data_end].copy_from_slice(value);
104    Ok(data_end)
105}
106
107/// Write a u16-prefixed dynamic field.
108#[inline(always)]
109pub fn write_dynamic_u16(
110    data: &mut [u8],
111    offset: usize,
112    value: &[u8],
113    max_len: usize,
114) -> Result<usize, ProgramError> {
115    if value.len() > max_len || value.len() > 65535 {
116        return Err(ProgramError::InvalidInstructionData);
117    }
118    let data_start = offset + 2;
119    let data_end = data_start + value.len();
120    if data_end > data.len() {
121        return Err(ProgramError::AccountDataTooSmall);
122    }
123    let len_bytes = (value.len() as u16).to_le_bytes();
124    data[offset] = len_bytes[0];
125    data[offset + 1] = len_bytes[1];
126    data[data_start..data_end].copy_from_slice(value);
127    Ok(data_end)
128}
129
130/// Write a u32-prefixed dynamic field.
131#[inline(always)]
132pub fn write_dynamic_u32(
133    data: &mut [u8],
134    offset: usize,
135    value: &[u8],
136    max_len: usize,
137) -> Result<usize, ProgramError> {
138    if value.len() > max_len {
139        return Err(ProgramError::InvalidInstructionData);
140    }
141    let data_start = offset + 4;
142    let data_end = data_start + value.len();
143    if data_end > data.len() {
144        return Err(ProgramError::AccountDataTooSmall);
145    }
146    let len_bytes = (value.len() as u32).to_le_bytes();
147    data[offset] = len_bytes[0];
148    data[offset + 1] = len_bytes[1];
149    data[offset + 2] = len_bytes[2];
150    data[offset + 3] = len_bytes[3];
151    data[data_start..data_end].copy_from_slice(value);
152    Ok(data_end)
153}
154
155/// An inline dynamic view that caches offsets for O(1) field access.
156///
157/// Created by walking the prefix bytes once, then all accessors
158/// use the cached offsets.
159///
160/// Generic over N = number of dynamic fields.
161pub struct DynamicView<'a, const N: usize> {
162    /// Raw account data
163    data: &'a [u8],
164    /// Cached byte offsets: offsets[i] = byte offset where dynamic field i starts
165    /// (after its length prefix)
166    offsets: [u32; N],
167    /// Cached lengths for each dynamic field
168    lengths: [u32; N],
169}
170
171impl<'a, const N: usize> DynamicView<'a, N> {
172    /// Parse dynamic fields starting at `base_offset` in `data`.
173    ///
174    /// `prefix_sizes` indicates prefix type per field: 1 = u8, 2 = u16, 4 = u32.
175    #[inline]
176    pub fn parse(
177        data: &'a [u8],
178        base_offset: usize,
179        prefix_sizes: &[u8; N],
180    ) -> Result<Self, ProgramError> {
181        let mut offsets = [0u32; N];
182        let mut lengths = [0u32; N];
183        let mut cursor = base_offset;
184
185        let mut i = 0;
186        while i < N {
187            let prefix_size = prefix_sizes[i] as usize;
188            if cursor + prefix_size > data.len() {
189                return Err(ProgramError::AccountDataTooSmall);
190            }
191            let len = match prefix_size {
192                1 => data[cursor] as u32,
193                2 => u16::from_le_bytes([data[cursor], data[cursor + 1]]) as u32,
194                4 => u32::from_le_bytes([
195                    data[cursor],
196                    data[cursor + 1],
197                    data[cursor + 2],
198                    data[cursor + 3],
199                ]),
200                _ => return Err(ProgramError::InvalidInstructionData),
201            };
202            let data_start = cursor + prefix_size;
203            let data_end = data_start + len as usize;
204            if data_end > data.len() {
205                return Err(ProgramError::AccountDataTooSmall);
206            }
207            offsets[i] = data_start as u32;
208            lengths[i] = len;
209            cursor = data_end;
210            i += 1;
211        }
212
213        Ok(Self {
214            data,
215            offsets,
216            lengths,
217        })
218    }
219
220    /// Get the byte slice for dynamic field at index. O(1) after initial parse.
221    #[inline(always)]
222    pub fn field(&self, index: usize) -> &[u8] {
223        let offset = self.offsets[index] as usize;
224        let len = self.lengths[index] as usize;
225        &self.data[offset..offset + len]
226    }
227
228    /// Get the length of dynamic field at index.
229    #[inline(always)]
230    pub fn field_len(&self, index: usize) -> usize {
231        self.lengths[index] as usize
232    }
233
234    /// Try to interpret a dynamic field as a UTF-8 string.
235    #[inline]
236    pub fn field_as_str(&self, index: usize) -> Result<&str, ProgramError> {
237        core::str::from_utf8(self.field(index)).map_err(|_| ProgramError::InvalidAccountData)
238    }
239
240    /// Total bytes consumed by all dynamic fields (including prefixes).
241    #[inline]
242    pub fn total_dynamic_bytes(&self) -> usize {
243        if N == 0 {
244            return 0;
245        }
246        let last_offset = self.offsets[N - 1] as usize;
247        let last_len = self.lengths[N - 1] as usize;
248        last_offset + last_len
249    }
250}
251
252/// Mutable inline dynamic view for writing dynamic fields.
253pub struct DynamicViewMut<'a, const N: usize> {
254    data: &'a mut [u8],
255    offsets: [u32; N],
256    lengths: [u32; N],
257}
258
259impl<'a, const N: usize> DynamicViewMut<'a, N> {
260    /// Parse dynamic fields starting at `base_offset` in mutable `data`.
261    #[inline]
262    pub fn parse(
263        data: &'a mut [u8],
264        base_offset: usize,
265        prefix_sizes: &[u8; N],
266    ) -> Result<Self, ProgramError> {
267        let mut offsets = [0u32; N];
268        let mut lengths = [0u32; N];
269        let mut cursor = base_offset;
270
271        let mut i = 0;
272        while i < N {
273            let prefix_size = prefix_sizes[i] as usize;
274            if cursor + prefix_size > data.len() {
275                return Err(ProgramError::AccountDataTooSmall);
276            }
277            let len = match prefix_size {
278                1 => data[cursor] as u32,
279                2 => u16::from_le_bytes([data[cursor], data[cursor + 1]]) as u32,
280                4 => u32::from_le_bytes([
281                    data[cursor],
282                    data[cursor + 1],
283                    data[cursor + 2],
284                    data[cursor + 3],
285                ]),
286                _ => return Err(ProgramError::InvalidInstructionData),
287            };
288            let data_start = cursor + prefix_size;
289            let data_end = data_start + len as usize;
290            if data_end > data.len() {
291                return Err(ProgramError::AccountDataTooSmall);
292            }
293            offsets[i] = data_start as u32;
294            lengths[i] = len;
295            cursor = data_end;
296            i += 1;
297        }
298
299        Ok(Self {
300            data,
301            offsets,
302            lengths,
303        })
304    }
305
306    /// Get immutable reference to a dynamic field.
307    #[inline(always)]
308    pub fn field(&self, index: usize) -> &[u8] {
309        let offset = self.offsets[index] as usize;
310        let len = self.lengths[index] as usize;
311        &self.data[offset..offset + len]
312    }
313
314    /// Get the length of a dynamic field.
315    #[inline(always)]
316    pub fn field_len(&self, index: usize) -> usize {
317        self.lengths[index] as usize
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn dynamic_u8_roundtrip() {
327        let mut buf = [0u8; 64];
328        let next = write_dynamic_u8(&mut buf, 0, b"hello", 32).unwrap();
329        assert_eq!(next, 6); // 1 prefix + 5 data
330        let (data, next2) = read_dynamic_u8(&buf, 0).unwrap();
331        assert_eq!(data, b"hello");
332        assert_eq!(next2, 6);
333    }
334
335    #[test]
336    fn dynamic_u16_roundtrip() {
337        let mut buf = [0u8; 64];
338        let next = write_dynamic_u16(&mut buf, 0, b"world!", 32).unwrap();
339        assert_eq!(next, 8); // 2 prefix + 6 data
340        let (data, next2) = read_dynamic_u16(&buf, 0).unwrap();
341        assert_eq!(data, b"world!");
342        assert_eq!(next2, 8);
343    }
344
345    #[test]
346    fn dynamic_view_parse_and_access() {
347        let mut buf = [0u8; 128];
348        // Write two u8-prefixed fields
349        let off = write_dynamic_u8(&mut buf, 0, b"alice", 32).unwrap();
350        let _off2 = write_dynamic_u8(&mut buf, off, b"this is a bio", 128).unwrap();
351
352        let view = DynamicView::<2>::parse(&buf, 0, &[1, 1]).unwrap();
353        assert_eq!(view.field(0), b"alice");
354        assert_eq!(view.field(1), b"this is a bio");
355        assert_eq!(view.field_as_str(0).unwrap(), "alice");
356    }
357
358    #[test]
359    fn dynamic_view_mixed_prefixes() {
360        let mut buf = [0u8; 128];
361        // u8 prefix for short string, u16 prefix for longer data
362        let off1 = write_dynamic_u8(&mut buf, 0, b"hi", 32).unwrap();
363        let _off2 = write_dynamic_u16(&mut buf, off1, b"longer data here", 256).unwrap();
364
365        let view = DynamicView::<2>::parse(&buf, 0, &[1, 2]).unwrap();
366        assert_eq!(view.field(0), b"hi");
367        assert_eq!(view.field(1), b"longer data here");
368    }
369}