Skip to main content

hopper_runtime/
option_byte.rs

1//! Zero-copy, tag-validated optional values for instruction args.
2//!
3//! Rust's `Option<T>` has niche-optimizing layout rules that make it
4//! unsafe to pointer-cast from raw instruction bytes. `Option<u8>` is
5//! two bytes with an undefined tag range; `Option<&T>` uses null for
6//! `None`. Neither is a layout the caller controls.
7//!
8//! `OptionByte<T>` is the Hopper replacement for args. Layout:
9//!
10//! ```text
11//! #[repr(C)]
12//! { tag: u8, value: T }
13//! ```
14//!
15//! `tag == 0` is `None`, `tag == 1` is `Some`. Any other tag byte is
16//! a protocol error and [`OptionByte::get`] surfaces it as
17//! `ProgramError::InvalidInstructionData`. This mirrors Quasar's
18//! `OptionZc<T>::validate_zc` contract with one fewer type parameter
19//! and no `MaybeUninit` escape hatch.
20//!
21//! ## Usage
22//!
23//! ```ignore
24//! #[hopper::args]
25//! #[repr(C)]
26//! pub struct SwapArgs {
27//!     pub amount: u64,
28//!     pub referrer: OptionByte<[u8; 32]>,
29//!     pub slippage_bps: u16,
30//! }
31//!
32//! fn handler(ctx: Context<Swap>, args: &SwapArgs) -> ProgramResult {
33//!     if let Some(referrer) = args.referrer.get()? {
34//!         // referrer is &[u8; 32]
35//!     }
36//!     Ok(())
37//! }
38//! ```
39
40use crate::{error::ProgramError, result::ProgramResult};
41
42/// Zero-copy tagged optional. See module docs for the layout and
43/// usage contract.
44#[repr(C)]
45#[derive(Copy, Clone)]
46pub struct OptionByte<T: Copy> {
47    tag: u8,
48    value: T,
49}
50
51impl<T: Copy> OptionByte<T> {
52    /// Construct a `None` variant. Because the struct is `#[repr(C)]`
53    /// with a Pod value field, the `value` payload must still be
54    /// bitwise valid; the caller provides a default value that is
55    /// ignored by [`OptionByte::get`].
56    #[inline(always)]
57    pub const fn none(default_value: T) -> Self {
58        Self {
59            tag: 0,
60            value: default_value,
61        }
62    }
63
64    /// Construct a `Some(value)` variant.
65    #[inline(always)]
66    pub const fn some(value: T) -> Self {
67        Self { tag: 1, value }
68    }
69
70    /// The tag byte as the sender encoded it. Callers should never
71    /// inspect this directly; use [`OptionByte::get`] so the tag is
72    /// validated first.
73    #[inline(always)]
74    pub const fn raw_tag(&self) -> u8 {
75        self.tag
76    }
77
78    /// Validate the tag byte and return the appropriate Rust `Option`.
79    ///
80    /// Returns `Err(ProgramError::InvalidInstructionData)` when the
81    /// tag is neither `0` nor `1`. Any other byte indicates malformed
82    /// instruction data and is the exact surface a Quasar `OptionZc`
83    /// would flag in `validate_zc`.
84    #[inline]
85    pub fn get(&self) -> Result<Option<&T>, ProgramError> {
86        match self.tag {
87            0 => Ok(None),
88            1 => Ok(Some(&self.value)),
89            _ => Err(ProgramError::InvalidInstructionData),
90        }
91    }
92
93    /// Validate-only: confirms the tag byte is 0 or 1. Useful for
94    /// callers who want to reject malformed input early without
95    /// taking a reference to the payload.
96    #[inline]
97    pub fn validate_tag(&self) -> ProgramResult {
98        match self.tag {
99            0 | 1 => Ok(()),
100            _ => Err(ProgramError::InvalidInstructionData),
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    /// 8-byte-aligned scratch buffer for the pointer-cast tests below.
110    /// `[u8; 9]` on the stack has alignment 1, which means a raw
111    /// reinterpret to `&OptionByte<u64>` (alignment 8) trips the
112    /// rustc 1.78+ debug-build "misaligned pointer dereference"
113    /// check on whichever fraction of stack frames don't happen to
114    /// land on an 8-aligned address. Wrapping the bytes in a
115    /// repr(align(8)) struct removes the alignment lottery.
116    #[repr(C, align(8))]
117    struct AlignedNine([u8; 9]);
118
119    #[test]
120    fn none_reads_as_none() {
121        let o: OptionByte<u64> = OptionByte::none(0);
122        assert!(o.get().unwrap().is_none());
123    }
124
125    #[test]
126    fn some_reads_back() {
127        let o = OptionByte::some(42u64);
128        assert_eq!(*o.get().unwrap().unwrap(), 42);
129    }
130
131    #[test]
132    fn malformed_tag_rejects() {
133        // Simulate a pointer-cast from hostile bytes: a 0xFF tag is
134        // neither 0 nor 1.
135        let mut buf = AlignedNine([0u8; 9]);
136        buf.0[0] = 0xFF;
137        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
138        let o: &OptionByte<u64> = unsafe { &*(buf.0.as_ptr() as *const OptionByte<u64>) };
139        assert_eq!(o.get().unwrap_err(), ProgramError::InvalidInstructionData);
140        assert_eq!(
141            o.validate_tag().unwrap_err(),
142            ProgramError::InvalidInstructionData
143        );
144    }
145
146    #[test]
147    fn zero_tag_ignores_value_payload() {
148        // A None with garbage value bytes still decodes cleanly.
149        let mut buf = AlignedNine([0u8; 9]);
150        buf.0[1..9].copy_from_slice(&0x1234_5678_9ABC_DEF0u64.to_le_bytes());
151        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
152        let o: &OptionByte<u64> = unsafe { &*(buf.0.as_ptr() as *const OptionByte<u64>) };
153        assert!(o.get().unwrap().is_none());
154    }
155}