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)    - RESERVED   -
9//! - u8      - u8      - u16 LE   - [u8; 8] SHA-256  - [u8; 4]    -
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 RESERVED_OFFSET: usize = 12;
33
34/// The 16-byte account header, overlay-safe.
35#[derive(Clone, Copy, PartialEq, Eq)]
36#[repr(C)]
37pub struct AccountHeader {
38    pub disc: u8,
39    pub version: u8,
40    pub flags: [u8; 2],
41    pub layout_id: [u8; 8],
42    pub reserved: [u8; 4],
43}
44
45const _: () = assert!(core::mem::size_of::<AccountHeader>() == HEADER_LEN);
46const _: () = assert!(core::mem::align_of::<AccountHeader>() == 1);
47
48// Bytemuck proof (Hopper Safety Audit Must-Fix #5). All fields are
49// `[u8; N]`-family types so every bit pattern decodes to a valid
50// `AccountHeader`.
51#[cfg(feature = "hopper-native-backend")]
52unsafe impl ::hopper_runtime::__hopper_native::bytemuck::Zeroable for AccountHeader {}
53#[cfg(feature = "hopper-native-backend")]
54unsafe impl ::hopper_runtime::__hopper_native::bytemuck::Pod for AccountHeader {}
55
56// SAFETY: #[repr(C)] of all-byte fields, all bit patterns valid.
57unsafe impl super::Pod for AccountHeader {}
58// Audit Step 5 seal: Hopper-authored primitive.
59unsafe impl ::hopper_runtime::__sealed::HopperZeroCopySealed for AccountHeader {}
60
61impl super::FixedLayout for AccountHeader {
62    const SIZE: usize = HEADER_LEN;
63}
64
65impl AccountHeader {
66    /// Create a new header.
67    #[inline(always)]
68    pub const fn new(disc: u8, version: u8, flags: u16, layout_id: [u8; 8]) -> Self {
69        Self {
70            disc,
71            version,
72            flags: flags.to_le_bytes(),
73            layout_id,
74            reserved: [0; 4],
75        }
76    }
77
78    /// Read the flags as a `u16`.
79    #[inline(always)]
80    pub const fn flags_u16(&self) -> u16 {
81        u16::from_le_bytes(self.flags)
82    }
83}
84
85/// Write a complete header to the beginning of `data`.
86///
87/// # Precondition
88/// `data.len() >= HEADER_LEN` and data should be zero-initialized first.
89#[inline(always)]
90pub fn write_header(
91    data: &mut [u8],
92    disc: u8,
93    version: u8,
94    layout_id: &[u8; 8],
95) -> Result<(), ProgramError> {
96    if data.len() < HEADER_LEN {
97        return Err(ProgramError::AccountDataTooSmall);
98    }
99    data[DISC_OFFSET] = disc;
100    data[VERSION_OFFSET] = version;
101    data[FLAGS_OFFSET..FLAGS_OFFSET + 2].copy_from_slice(&0u16.to_le_bytes());
102    data[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8].copy_from_slice(layout_id);
103    data[RESERVED_OFFSET..RESERVED_OFFSET + 4].copy_from_slice(&[0u8; 4]);
104    Ok(())
105}
106
107/// Validate the header against expected values.
108#[inline(always)]
109pub fn check_header(
110    data: &[u8],
111    expected_disc: u8,
112    min_version: u8,
113    layout_id: &[u8; 8],
114) -> Result<(), ProgramError> {
115    if data.len() < HEADER_LEN {
116        return Err(ProgramError::AccountDataTooSmall);
117    }
118    if data[DISC_OFFSET] != expected_disc {
119        return Err(ProgramError::InvalidAccountData);
120    }
121    if data[VERSION_OFFSET] < min_version {
122        return Err(ProgramError::InvalidAccountData);
123    }
124    if data[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8] != *layout_id {
125        return Err(ProgramError::InvalidAccountData);
126    }
127    Ok(())
128}
129
130/// Read the discriminator byte from raw account data.
131///
132/// This is a standalone utility for callers that need to peek at the
133/// disc without constructing an `AccountReader`. Returns the first
134/// byte or `AccountDataTooSmall` if `data` is empty.
135#[inline(always)]
136pub fn read_discriminator(data: &[u8]) -> Result<u8, ProgramError> {
137    data.first()
138        .copied()
139        .ok_or(ProgramError::AccountDataTooSmall)
140}
141
142/// Read the version byte.
143#[inline(always)]
144pub fn read_version(data: &[u8]) -> Result<u8, ProgramError> {
145    if data.len() < 2 {
146        return Err(ProgramError::AccountDataTooSmall);
147    }
148    Ok(data[VERSION_OFFSET])
149}
150
151/// Read the flags field as `u16`.
152#[inline(always)]
153pub fn read_header_flags(data: &[u8]) -> Result<u16, ProgramError> {
154    if data.len() < 4 {
155        return Err(ProgramError::AccountDataTooSmall);
156    }
157    Ok(u16::from_le_bytes([
158        data[FLAGS_OFFSET],
159        data[FLAGS_OFFSET + 1],
160    ]))
161}
162
163/// Read the 8-byte layout_id.
164#[inline(always)]
165pub fn read_layout_id(data: &[u8]) -> Result<[u8; 8], ProgramError> {
166    if data.len() < 12 {
167        return Err(ProgramError::AccountDataTooSmall);
168    }
169    let mut id = [0u8; 8];
170    id.copy_from_slice(&data[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8]);
171    Ok(id)
172}