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}