Skip to main content

hopper_core/account/
header.rs

1//! 16-byte account header.
2//!
3//! Wire format (all fields little-endian where multi-byte):
4//! ```text
5//! ----------------------------------------------------------------
6//! - byte 0  - byte 1  - bytes 2-3- bytes 4-11       - bytes 12-15-
7//! ----------------------------------------------------------------
8//! - DISC    - VERSION - FLAGS    - LAYOUT_ID (8)    - SCHEMA EPOCH-
9//! - u8      - u8      - u16 LE   - [u8; 8] SHA-256  - u32 LE      -
10//! ----------------------------------------------------------------
11//! ```
12//!
13//! The layout_id is the first 8 bytes of:
14//! `SHA-256("hopper:v1:" + name + ":" + version + ":" + canonical_field_string)`
15//!
16//! Canonical field string: `"field_name:canonical_type:size,"` per field with trailing comma.
17//! Field order is declaration order.
18
19use hopper_runtime::error::ProgramError;
20
21/// Header length in bytes.
22pub const HEADER_LEN: usize = 16;
23
24/// Header format version. Bump only if the header wire format itself changes.
25pub const HEADER_FORMAT: u8 = 1;
26
27// Offsets within the header
28const DISC_OFFSET: usize = 0;
29const VERSION_OFFSET: usize = 1;
30const FLAGS_OFFSET: usize = 2;
31const LAYOUT_ID_OFFSET: usize = 4;
32const SCHEMA_EPOCH_OFFSET: usize = 12;
33
34/// Default schema epoch stamped into freshly initialized headers.
35pub const DEFAULT_SCHEMA_EPOCH: u32 = hopper_runtime::layout::DEFAULT_SCHEMA_EPOCH;
36
37/// The 16-byte account header, overlay-safe.
38#[derive(Clone, Copy, PartialEq, Eq)]
39#[repr(C)]
40pub struct AccountHeader {
41    pub disc: u8,
42    pub version: u8,
43    pub flags: [u8; 2],
44    pub layout_id: [u8; 8],
45    pub schema_epoch: [u8; 4],
46}
47
48const _: () = assert!(core::mem::size_of::<AccountHeader>() == HEADER_LEN);
49const _: () = assert!(core::mem::align_of::<AccountHeader>() == 1);
50
51// Bytemuck proof (Hopper Safety Audit Must-Fix #5). All fields are
52// `[u8; N]`-family types so every bit pattern decodes to a valid
53// `AccountHeader`.
54#[cfg(feature = "hopper-native-backend")]
55unsafe impl ::hopper_runtime::__hopper_native::bytemuck::Zeroable for AccountHeader {}
56#[cfg(feature = "hopper-native-backend")]
57unsafe impl ::hopper_runtime::__hopper_native::bytemuck::Pod for AccountHeader {}
58
59// SAFETY: #[repr(C)] of all-byte fields, all bit patterns valid.
60unsafe impl super::Pod for AccountHeader {}
61// Audit Step 5 seal: Hopper-authored primitive.
62unsafe impl ::hopper_runtime::__sealed::HopperZeroCopySealed for AccountHeader {}
63
64impl super::FixedLayout for AccountHeader {
65    const SIZE: usize = HEADER_LEN;
66}
67
68impl AccountHeader {
69    /// Create a new header.
70    #[inline(always)]
71    pub const fn new(disc: u8, version: u8, flags: u16, layout_id: [u8; 8]) -> Self {
72        Self::new_with_schema_epoch(disc, version, flags, layout_id, DEFAULT_SCHEMA_EPOCH)
73    }
74
75    /// Create a new header with an explicit schema epoch.
76    #[inline(always)]
77    pub const fn new_with_schema_epoch(
78        disc: u8,
79        version: u8,
80        flags: u16,
81        layout_id: [u8; 8],
82        schema_epoch: u32,
83    ) -> Self {
84        Self {
85            disc,
86            version,
87            flags: flags.to_le_bytes(),
88            layout_id,
89            schema_epoch: schema_epoch.to_le_bytes(),
90        }
91    }
92
93    /// Read the flags as a `u16`.
94    #[inline(always)]
95    pub const fn flags_u16(&self) -> u16 {
96        u16::from_le_bytes(self.flags)
97    }
98
99    /// Read the schema epoch as a `u32`.
100    #[inline(always)]
101    pub const fn schema_epoch_u32(&self) -> u32 {
102        u32::from_le_bytes(self.schema_epoch)
103    }
104}
105
106/// Write a complete header to the beginning of `data`.
107///
108/// # Precondition
109/// `data.len() >= HEADER_LEN` and data should be zero-initialized first.
110#[inline(always)]
111pub fn write_header(
112    data: &mut [u8],
113    disc: u8,
114    version: u8,
115    layout_id: &[u8; 8],
116) -> Result<(), ProgramError> {
117    if data.len() < HEADER_LEN {
118        return Err(ProgramError::AccountDataTooSmall);
119    }
120    data[DISC_OFFSET] = disc;
121    data[VERSION_OFFSET] = version;
122    data[FLAGS_OFFSET..FLAGS_OFFSET + 2].copy_from_slice(&0u16.to_le_bytes());
123    data[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8].copy_from_slice(layout_id);
124    data[SCHEMA_EPOCH_OFFSET..SCHEMA_EPOCH_OFFSET + 4]
125        .copy_from_slice(&DEFAULT_SCHEMA_EPOCH.to_le_bytes());
126    Ok(())
127}
128
129/// Validate the header against expected values.
130#[inline(always)]
131pub fn check_header(
132    data: &[u8],
133    expected_disc: u8,
134    min_version: u8,
135    layout_id: &[u8; 8],
136) -> Result<(), ProgramError> {
137    if data.len() < HEADER_LEN {
138        return Err(ProgramError::AccountDataTooSmall);
139    }
140    if data[DISC_OFFSET] != expected_disc {
141        return Err(ProgramError::InvalidAccountData);
142    }
143    if data[VERSION_OFFSET] < min_version {
144        return Err(ProgramError::InvalidAccountData);
145    }
146    if data[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8] != *layout_id {
147        return Err(ProgramError::InvalidAccountData);
148    }
149    Ok(())
150}
151
152/// Read the discriminator byte from raw account data.
153///
154/// This is a standalone utility for callers that need to peek at the
155/// disc without constructing an `AccountReader`. Returns the first
156/// byte or `AccountDataTooSmall` if `data` is empty.
157#[inline(always)]
158pub fn read_discriminator(data: &[u8]) -> Result<u8, ProgramError> {
159    data.first()
160        .copied()
161        .ok_or(ProgramError::AccountDataTooSmall)
162}
163
164/// Read the version byte.
165#[inline(always)]
166pub fn read_version(data: &[u8]) -> Result<u8, ProgramError> {
167    if data.len() < 2 {
168        return Err(ProgramError::AccountDataTooSmall);
169    }
170    Ok(data[VERSION_OFFSET])
171}
172
173/// Read the flags field as `u16`.
174#[inline(always)]
175pub fn read_header_flags(data: &[u8]) -> Result<u16, ProgramError> {
176    if data.len() < 4 {
177        return Err(ProgramError::AccountDataTooSmall);
178    }
179    Ok(u16::from_le_bytes([
180        data[FLAGS_OFFSET],
181        data[FLAGS_OFFSET + 1],
182    ]))
183}
184
185/// Read the 8-byte layout_id.
186#[inline(always)]
187pub fn read_layout_id(data: &[u8]) -> Result<[u8; 8], ProgramError> {
188    if data.len() < 12 {
189        return Err(ProgramError::AccountDataTooSmall);
190    }
191    let mut id = [0u8; 8];
192    id.copy_from_slice(&data[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8]);
193    Ok(id)
194}