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(ProgramError::AccountDataTooSmall);
61        }
62        if data.len() != Self::COMPACT_LEN {
63            return Err(ProgramError::InvalidAccountData);
64        }
65        if data[0] != Self::DISC {
66            return Err(ProgramError::InvalidAccountData);
67        }
68        Ok(())
69    }
70}
71
72/// A compact account with a fixed zero-copy head followed by a dynamic tail:
73///
74/// ```text
75/// byte 0            : disc (u8)
76/// bytes 1..1+H      : fixed head (this Pod struct), H = size_of::<Self>()
77/// bytes 1+H..       : dynamic tail (u32 LE length prefix + payload)
78/// ```
79///
80/// This is the 1-byte-header analogue of the headered `dynamic_tail` layout:
81/// it gives an account a Quasar-class `[disc][fixed_head][tail]` shape with
82/// **no** 16-byte universal header, while keeping Hopper's registry, schema,
83/// and fingerprint tooling.
84///
85/// Unlike [`CompactLayout`], the wire length is **not** fixed, the tail may
86/// be empty or grow via `resize`. The fixed head is still overlaid zero-copy
87/// at [`COMPACT_BODY_OFFSET`]; the tail is read/written through the
88/// macro-generated `tail_*` helpers, which operate on a length-prefixed
89/// payload anchored at [`Self::TAIL_OFFSET`] (the same offset-parameterized
90/// `read_tail`/`write_tail` runtime the headered path uses).
91///
92/// # Safety
93///
94/// As with [`CompactLayout`], the fixed head is overlaid directly on account
95/// bytes starting at [`COMPACT_BODY_OFFSET`]; the `Pod` supertrait carries the
96/// alignment-1 / no-padding / valid-for-all-bits obligation for that head.
97/// The tail bytes beyond the head are never reinterpreted as `Self`.
98pub trait CompactDynamicLayout: Sized + Copy + crate::Pod {
99    /// Discriminator stored at byte 0 of the account.
100    const DISC: u8;
101
102    /// Fixed head size in bytes (the zero-copy struct), excluding the
103    /// discriminator and the tail.
104    const FIXED_HEAD_SIZE: usize = core::mem::size_of::<Self>();
105
106    /// Minimum wire length: 1 discriminator byte + the fixed head. The tail
107    /// may be empty, so this is the floor, not the exact length.
108    const MIN_LEN: usize = COMPACT_BODY_OFFSET + Self::FIXED_HEAD_SIZE;
109
110    /// Byte offset of the tail region (its `u32` LE length prefix),
111    /// immediately after the fixed head. Equal to [`Self::MIN_LEN`].
112    const TAIL_OFFSET: usize = Self::MIN_LEN;
113
114    /// Validate that `data` is a compact-dynamic account of this type: it is
115    /// at least [`MIN_LEN`](Self::MIN_LEN) bytes (discriminator + fixed head
116    /// present) and the discriminator at byte 0 matches.
117    ///
118    /// Deliberately does **not** require an exact length: trailing tail bytes
119    /// are expected. The tail's own length prefix and payload bounds are
120    /// validated by the tail accessors when used.
121    #[inline(always)]
122    fn validate_compact_dynamic(data: &[u8]) -> ProgramResult {
123        if data.len() < Self::MIN_LEN {
124            return Err(ProgramError::AccountDataTooSmall);
125        }
126        if data[0] != Self::DISC {
127            return Err(ProgramError::InvalidAccountData);
128        }
129        Ok(())
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::pod::{Pod, Zeroable};
137
138    #[repr(C)]
139    #[derive(Clone, Copy)]
140    struct Body {
141        authority: [u8; 32],
142        balance: [u8; 8],
143    }
144    // SAFETY: alignment-1 byte arrays, all bit patterns valid, no padding.
145    unsafe impl Zeroable for Body {}
146    unsafe impl Pod for Body {}
147    impl CompactLayout for Body {
148        const DISC: u8 = 7;
149    }
150
151    #[test]
152    fn compact_len_is_one_plus_body() {
153        assert_eq!(Body::BODY_SIZE, 40);
154        assert_eq!(Body::COMPACT_LEN, 41);
155    }
156
157    #[test]
158    fn validate_checks_len_and_disc() {
159        let mut buf = [0u8; 41];
160        buf[0] = 7;
161        assert!(Body::validate_compact(&buf).is_ok());
162
163        buf[0] = 8;
164        assert!(matches!(
165            Body::validate_compact(&buf),
166            Err(ProgramError::InvalidAccountData)
167        ));
168
169        buf[0] = 7;
170        assert!(matches!(
171            Body::validate_compact(&buf[..40]),
172            Err(ProgramError::AccountDataTooSmall)
173        ));
174
175        let mut oversized = [0u8; 42];
176        oversized[0] = 7;
177        assert!(matches!(
178            Body::validate_compact(&oversized),
179            Err(ProgramError::InvalidAccountData)
180        ));
181    }
182}