hopper_runtime/pod.rs
1//! `Pod`, the canonical runtime-layer "safe to interpret from raw bytes" marker.
2//!
3//! Hopper's typed access primitives (`segment_ref`, `segment_mut`,
4//! `raw_ref`, `raw_mut`, `read_data`) all overlay a `T` on a slice of
5//! account bytes. That overlay is only sound if **every bit pattern of
6//! the right size** decodes to a valid `T` and the type has alignment 1
7//! (so the offset within the BPF input buffer is always valid for `T`).
8//!
9//! The Hopper Safety Audit flagged that requiring only `T: Copy` is too
10//! loose: `bool`, `char`, references, and structs with padding are all
11//! `Copy + Sized` but **not** safe to overlay on raw bytes. This module
12//! carries the tightened marker.
13//!
14//! ## Contract
15//!
16//! Implementing `Pod` for a type `T` asserts all of:
17//!
18//! 1. Every `[u8; size_of::<T>()]` byte pattern represents a valid `T`.
19//! No "niches", no enum-discriminant invariants, no `bool`-style
20//! forbidden bit patterns.
21//! 2. `align_of::<T>() == 1`, the type can be read from any byte
22//! offset of an account buffer without alignment fault.
23//! 3. `T` contains no padding (`#[repr(C)]` with alignment-1 fields, or
24//! `#[repr(transparent)]` over a `Pod` type).
25//! 4. `T` contains no internal pointers / references, overlay always
26//! yields data that's safe to `Copy`.
27//!
28//! Hopper's higher-layer macros (`#[hopper::state]`, `#[hopper::pod]`,
29//! `hopper_layout!`) enforce these conditions at compile time and emit
30//! the derived `unsafe impl Pod`. Hand-authored layouts opt in via
31//! `unsafe impl Pod for MyLayout {}`.
32//!
33//! ## Compile-fail demonstration (Hopper Safety Audit regression)
34//!
35//! With the `bytemuck` feature on (default), the following mis-use
36//! patterns are all rejected at compile time. The audit's Must-Fix #5
37//!, "enforce field-level Pod proof at macro expansion time", is
38//! now mechanically enforced by bytemuck's own `Pod + Zeroable`
39//! bounds, so every zero-copy access path rejects them automatically.
40//!
41//! `bool` is not Pod (the bit patterns `0x02..=0xFF` don't decode to
42//! a valid `bool`):
43//!
44//! ```compile_fail
45//! # use hopper_runtime::{AccountView, segment_borrow::SegmentBorrowRegistry};
46//! # fn example(account: &AccountView, borrows: &mut SegmentBorrowRegistry) {
47//! let _ = account.segment_ref::<bool>(borrows, 16, 1);
48//! # }
49//! ```
50//!
51//! `char` is not Pod (valid Unicode scalar values form a sparse set):
52//!
53//! ```compile_fail
54//! # use hopper_runtime::{AccountView, segment_borrow::SegmentBorrowRegistry};
55//! # fn example(account: &AccountView, borrows: &mut SegmentBorrowRegistry) {
56//! let _ = account.segment_ref::<char>(borrows, 16, 4);
57//! # }
58//! ```
59//!
60//! A `#[repr(C)]` struct with implicit padding is not bytemuck-Pod
61//!, bytemuck's derive / Pod bound rejects the padding bytes because
62//! they'd leak uninitialised data through `bytes_of`:
63//!
64//! ```compile_fail
65//! # use hopper_runtime::{AccountView, segment_borrow::SegmentBorrowRegistry};
66//! # fn example(account: &AccountView, borrows: &mut SegmentBorrowRegistry) {
67//! #[derive(Copy, Clone)]
68//! #[repr(C)]
69//! struct Padded {
70//! a: u8,
71//! // implicit 7 bytes of padding to align b
72//! b: u64,
73//! }
74//! let _ = account.segment_ref::<Padded>(borrows, 16, 16);
75//! # }
76//! ```
77//!
78//! A type-level user mis-spelling `unsafe impl Pod for Padded {}`
79//! without also satisfying `bytemuck::Pod + Zeroable` would fail at
80//! the `Pod` supertrait bound. The compile-fail block above exercises
81//! that path: no explicit `impl Pod` exists, and the access-path
82//! generic requires it.
83//!
84//! A well-formed primitive or wire type is accepted:
85//!
86//! ```ignore
87//! # use hopper_runtime::{AccountView, segment_borrow::SegmentBorrowRegistry};
88//! # fn example(account: &AccountView, borrows: &mut SegmentBorrowRegistry) {
89//! let _: Result<hopper_runtime::SegRef<'_, u64>, _> =
90//! account.segment_ref::<u64>(borrows, 16, 8);
91//! # }
92//! ```
93//!
94//! ## Trait identity across layers
95//!
96//! When `hopper-native-backend` is active (the default), this trait is
97//! a direct re-export of [`hopper_native::Pod`]. That keeps the entire
98//! Hopper stack, substrate, runtime, core, macros, on a single Pod
99//! trait: one `unsafe impl Pod for MyStruct {}` unlocks every Hopper
100//! access API from the lowest-level `AccountView::raw_mut` up to
101//! `#[hopper::state]`-generated accessors, across all crates, with no
102//! orphan-rule gymnastics. When a non-native backend is selected
103//! (`legacy-pinocchio-compat`, `solana-program-backend`), the trait is
104//! defined locally with the same contract so user code compiles
105//! unchanged.
106
107// ── Trait identity: native backend path ──────────────────────────────
108//
109// Re-export `hopper_native::Pod` directly so the "one canonical Pod"
110// invariant holds end-to-end.
111#[cfg(feature = "hopper-native-backend")]
112pub use hopper_native::Pod;
113
114// ── Trait identity: non-native backend path ─────────────────────────
115//
116// Define the trait locally for test harnesses and alternate backends
117// that don't pull in `hopper-native`. The contract is identical.
118#[cfg(not(feature = "hopper-native-backend"))]
119pub unsafe trait Pod: Copy + Sized {}
120
121#[cfg(not(feature = "hopper-native-backend"))]
122mod local_impls {
123 use super::Pod;
124 unsafe impl Pod for u8 {}
125 unsafe impl Pod for u16 {}
126 unsafe impl Pod for u32 {}
127 unsafe impl Pod for u64 {}
128 unsafe impl Pod for u128 {}
129 unsafe impl Pod for i8 {}
130 unsafe impl Pod for i16 {}
131 unsafe impl Pod for i32 {}
132 unsafe impl Pod for i64 {}
133 unsafe impl Pod for i128 {}
134 unsafe impl<const N: usize> Pod for [u8; N] {}
135 unsafe impl Pod for () {}
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn assert_pod<T: Pod>() {}
143
144 #[test]
145 fn primitives_are_pod() {
146 assert_pod::<u8>();
147 assert_pod::<u16>();
148 assert_pod::<u32>();
149 assert_pod::<u64>();
150 assert_pod::<u128>();
151 assert_pod::<i8>();
152 assert_pod::<i16>();
153 assert_pod::<i32>();
154 assert_pod::<i64>();
155 assert_pod::<i128>();
156 assert_pod::<[u8; 32]>();
157 }
158
159 #[test]
160 fn address_satisfies_pod() {
161 // `Address` is declared `#[repr(transparent)] [u8; 32]` with a
162 // hand-rolled `unsafe impl Pod`. Under the native backend that
163 // impl is on `hopper_native::Pod`; here we're just checking the
164 // re-export plumbing lands.
165 assert_pod::<crate::address::Address>();
166 }
167}