Skip to main content

hopper_runtime/
compact.rs

1//! Tier 1 of the three-tier metadata model: compact account access.
2//!
3//! See the repository's
4//! [three-tier metadata design](https://github.com/BluefootLabs/Hopper-Solana-Zero-copy-State-Framework/blob/main/docs/THREE_TIER_METADATA.md).
5//!
6//! A *compact* account stores exactly one discriminator byte followed by
7//! the zero-copy body:
8//!
9//! ```text
10//! byte 0   : disc (u8)
11//! bytes 1..: zero-copy body (alignment-1 Pod fields)
12//! ```
13//!
14//! There is **no 16-byte universal header**. The hot path is
15//! `check_len_exact` + `check_disc` + cast-body-at-offset-1: no layout_id read,
16//! no schema-epoch comparison, no registry fetch. Identity of the layout
17//! behind a discriminator is a *program-level* fact (Tier 2 / Tier 3),
18//! not a per-account one.
19//!
20//! This is additive: the 16-byte-header [`crate::layout::LayoutContract`]
21//! path is unchanged and remains the default. A type opts into compact
22//! by implementing [`CompactLayout`]; the two are distinguished by which
23//! loader the caller invokes.
24
25use crate::error::ProgramError;
26use crate::ProgramResult;
27
28/// Byte offset of a compact account body (immediately after the 1-byte
29/// discriminator).
30pub const COMPACT_BODY_OFFSET: usize = 1;
31
32/// A zero-copy account layout stored in compact `[disc:u8][body]` form.
33///
34/// # Safety
35///
36/// The blanket access methods overlay `Self` directly on account bytes
37/// starting at [`COMPACT_BODY_OFFSET`]. Implementing this trait asserts
38/// the same contract as [`crate::Pod`] for the body: alignment 1, no
39/// padding, every bit pattern valid, no internal pointers. The `Pod`
40/// supertrait carries that obligation; `CompactLayout` only adds the
41/// discriminator and the compact wire-length math.
42pub trait CompactLayout: Sized + Copy + crate::Pod {
43    /// Discriminator stored at byte 0 of the account.
44    const DISC: u8;
45
46    /// Body size in bytes (the zero-copy struct).
47    const BODY_SIZE: usize = core::mem::size_of::<Self>();
48
49    /// Total compact wire length: 1 discriminator byte + body.
50    const COMPACT_LEN: usize = COMPACT_BODY_OFFSET + Self::BODY_SIZE;
51
52    /// Validate that `data` is a compact account of this type.
53    ///
54    /// Checks the buffer has exactly the fixed compact wire length and
55    /// the discriminator at byte 0 matches. Deliberately does **not**
56    /// read a layout_id or epoch.
57    #[inline(always)]
58    fn validate_compact(data: &[u8]) -> ProgramResult {
59        if data.len() != Self::COMPACT_LEN {
60            return Err(compact_len_error(data.len(), Self::COMPACT_LEN));
61        }
62        if data[0] != Self::DISC {
63            return Err(ProgramError::InvalidAccountData);
64        }
65        Ok(())
66    }
67}
68
69/// The error for a compact account of the wrong length: too small when it
70/// is short, invalid when it is long. Cold and out of line, so the accepted
71/// path tests the length once.
72#[cold]
73#[inline(never)]
74pub(crate) fn compact_len_error(len: usize, expected: usize) -> ProgramError {
75    if len < expected {
76        ProgramError::AccountDataTooSmall
77    } else {
78        ProgramError::InvalidAccountData
79    }
80}
81
82/// A compact account with a fixed zero-copy head followed by a dynamic tail:
83///
84/// ```text
85/// byte 0            : disc (u8)
86/// bytes 1..1+H      : fixed head (this Pod struct), H = size_of::<Self>()
87/// bytes 1+H..       : dynamic tail (u32 LE length prefix + payload)
88/// ```
89///
90/// This is the 1-byte-header analogue of the headered `dynamic_tail` layout:
91/// it gives an account a Quasar-class `[disc][fixed_head][tail]` shape with
92/// **no** 16-byte universal header, while keeping Hopper's registry, schema,
93/// and fingerprint tooling.
94///
95/// Unlike [`CompactLayout`], the wire length is **not** fixed, the tail may
96/// be empty or grow via `resize`. The fixed head is still overlaid zero-copy
97/// at [`COMPACT_BODY_OFFSET`]; the tail is read/written through the
98/// macro-generated `tail_*` helpers, which operate on a length-prefixed
99/// payload anchored at [`Self::TAIL_OFFSET`] (the same offset-parameterized
100/// `read_tail`/`write_tail` runtime the headered path uses).
101///
102/// # Safety
103///
104/// As with [`CompactLayout`], the fixed head is overlaid directly on account
105/// bytes starting at [`COMPACT_BODY_OFFSET`]; the `Pod` supertrait carries the
106/// alignment-1 / no-padding / valid-for-all-bits obligation for that head.
107/// The tail bytes beyond the head are never reinterpreted as `Self`.
108pub trait CompactDynamicLayout: Sized + Copy + crate::Pod {
109    /// Discriminator stored at byte 0 of the account.
110    const DISC: u8;
111
112    /// Fixed head size in bytes (the zero-copy struct), excluding the
113    /// discriminator and the tail.
114    const FIXED_HEAD_SIZE: usize = core::mem::size_of::<Self>();
115
116    /// Minimum wire length: 1 discriminator byte + the fixed head. The tail
117    /// may be empty, so this is the floor, not the exact length.
118    const MIN_LEN: usize = COMPACT_BODY_OFFSET + Self::FIXED_HEAD_SIZE;
119
120    /// Byte offset of the tail region (its `u32` LE length prefix),
121    /// immediately after the fixed head. Equal to [`Self::MIN_LEN`].
122    const TAIL_OFFSET: usize = Self::MIN_LEN;
123
124    /// Validate that `data` is a compact-dynamic account of this type: it is
125    /// at least [`MIN_LEN`](Self::MIN_LEN) bytes (discriminator + fixed head
126    /// present) and the discriminator at byte 0 matches.
127    ///
128    /// Deliberately does **not** require an exact length: trailing tail bytes
129    /// are expected. The tail's own length prefix and payload bounds are
130    /// validated by the tail accessors when used.
131    #[inline(always)]
132    fn validate_compact_dynamic(data: &[u8]) -> ProgramResult {
133        if data.len() < Self::MIN_LEN {
134            return Err(ProgramError::AccountDataTooSmall);
135        }
136        if data[0] != Self::DISC {
137            return Err(ProgramError::InvalidAccountData);
138        }
139        Ok(())
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::pod::{Pod, Zeroable};
147
148    #[repr(C)]
149    #[derive(Clone, Copy)]
150    struct Body {
151        authority: [u8; 32],
152        balance: [u8; 8],
153    }
154    // SAFETY: alignment-1 byte arrays, all bit patterns valid, no padding.
155    unsafe impl Zeroable for Body {}
156    unsafe impl Pod for Body {}
157    impl CompactLayout for Body {
158        const DISC: u8 = 7;
159    }
160
161    #[test]
162    fn compact_len_is_one_plus_body() {
163        assert_eq!(Body::BODY_SIZE, 40);
164        assert_eq!(Body::COMPACT_LEN, 41);
165    }
166
167    #[test]
168    fn validate_checks_len_and_disc() {
169        let mut buf = [0u8; 41];
170        buf[0] = 7;
171        assert!(Body::validate_compact(&buf).is_ok());
172
173        buf[0] = 8;
174        assert!(matches!(
175            Body::validate_compact(&buf),
176            Err(ProgramError::InvalidAccountData)
177        ));
178
179        buf[0] = 7;
180        assert!(matches!(
181            Body::validate_compact(&buf[..40]),
182            Err(ProgramError::AccountDataTooSmall)
183        ));
184
185        let mut oversized = [0u8; 42];
186        oversized[0] = 7;
187        assert!(matches!(
188            Body::validate_compact(&oversized),
189            Err(ProgramError::InvalidAccountData)
190        ));
191    }
192}