hopper_runtime/zerocopy.rs
1//! Unified zero-copy trait family.
2//!
3//! This module consolidates `Pod`, `FixedLayout`, `Projectable`, `SafeProjectable`,
4//! `LayoutContract`, header metadata, and schema export into one
5//! coherent trait stack. This module delivers the foundation:
6//!
7//! - [`ZeroCopy`], the canonical "safe to overlay on raw bytes"
8//! marker. Equivalent-in-contract to [`Pod`], using
9//! Hopper's owned `Pod` / `Zeroable` proof layer.
10//! A blanket implementation covers `Pod` types that also carry Hopper's
11//! sealed marker, including layouts emitted by Hopper's macros.
12//!
13//! - [`WireLayout`], a `ZeroCopy` type whose wire size is
14//! `size_of::<Self>()` under the current blanket implementation.
15//!
16//! - [`AccountLayout`], a `WireLayout` that also carries Hopper's
17//! account header identity (disc, version, wire fingerprint, schema
18//! epoch, type offset). This is the top-level account-layout
19//! trait, with an explicit member list so the contract is
20//! frozen-in-place for migrations and client generation.
21//!
22//! ## Why three traits, not one
23//!
24//! The layering mirrors a real capability hierarchy. Every account
25//! layout is a wire layout; every wire layout is zero-copy; but not
26//! every zero-copy type is a full account layout (`WireU64`, `WireBool`,
27//! `TypedAddress<T>` are zero-copy but carry no header). Splitting
28//! the traits lets generic helpers demand just what they need.
29//!
30//! ## Relation to `LayoutContract`
31//!
32//! The existing [`crate::layout::LayoutContract`] trait predates this
33//! module. `LayoutContract` and `AccountLayout` intentionally overlap:
34//! both describe "a Hopper layout with disc/version/layout_id".
35//! `AccountLayout` presents the same identity through a unified trait stack.
36//! A blanket implementation covers types that implement both
37//! `LayoutContract` and `ZeroCopy`.
38
39use crate::layout::LayoutContract;
40use crate::pod::Pod;
41
42// ══════════════════════════════════════════════════════════════════════
43// Seal
44// ══════════════════════════════════════════════════════════════════════
45
46/// Internal marker every framework-defined zero-copy type stamps itself
47/// with. Sealed by convention: it lives in a doc-hidden module so
48/// downstream code cannot name it except through the canonical
49/// Hopper entry points (`#[hopper::pod]`, `#[hopper::state]`,
50/// `hopper_layout!`, and the framework's own primitive wire types).
51///
52/// A user bypassing the macro system with a hand-rolled
53/// `unsafe impl Pod for Foo {}` cannot accidentally pick up
54/// [`ZeroCopy`] for free. The `ZeroCopy` blanket below additionally
55/// requires `HopperZeroCopySealed`, which only framework-defined
56/// surfaces implement.
57///
58/// Users who legitimately need to extend `ZeroCopy` for a custom
59/// primitive can declare `unsafe impl ::hopper_runtime::__sealed::HopperZeroCopySealed for MyType {}`
60/// manually, but the path-through-doc-hidden-module signals clearly
61/// that they are opting out of the macro's field-level proof.
62#[doc(hidden)]
63pub mod __sealed {
64 /// See the module-level documentation. Do not implement directly
65 /// unless you understand the full Hopper `Pod` + `Zeroable` +
66 /// alignment + no-padding + no-interior-pointers contract.
67 ///
68 /// # Safety
69 ///
70 /// Implementors promise the type is a fixed-size, `#[repr(C)]`,
71 /// alignment-1 plain-old-data value with no padding bytes and no
72 /// interior pointers, so that any byte pattern of the correct length is
73 /// a valid instance. Implementing this for a type that violates the
74 /// contract makes every downstream [`super::ZeroCopy`] cast unsound.
75 pub unsafe trait HopperZeroCopySealed {}
76
77 // Framework-provided primitives. Every Rust-level `Pod` integer
78 // and `[u8; N]` is Hopper-owned by virtue of being in the
79 // substrate, so stamp the seal here. Users reading/writing these
80 // via `ForeignLens::field::<T, OFFSET>` or equivalent paths get
81 // `ZeroCopy` for free.
82 unsafe impl HopperZeroCopySealed for u8 {}
83 unsafe impl HopperZeroCopySealed for i8 {}
84 unsafe impl<const N: usize> HopperZeroCopySealed for [u8; N] {}
85 unsafe impl HopperZeroCopySealed for () {}
86}
87
88// ══════════════════════════════════════════════════════════════════════
89// ZeroCopy
90// ══════════════════════════════════════════════════════════════════════
91
92/// Canonical marker for types that may be overlaid on raw bytes.
93///
94/// # Safety
95///
96/// The contract is the same four-point obligation as [`Pod`]:
97///
98/// 1. Every `[u8; size_of::<T>()]` bit pattern decodes to a valid `T`.
99/// 2. `align_of::<T>() == 1`.
100/// 3. `T` contains no padding.
101/// 4. `T` contains no internal pointers or references.
102///
103/// # Sealing
104///
105/// `ZeroCopy` is gated behind the doc-hidden
106/// [`__sealed::HopperZeroCopySealed`] marker. Types authored through
107/// `#[hopper::pod]`, `#[hopper::state]`, `hopper_layout!`, or one of
108/// the framework's own primitive wire types (`WireU64`, `WireBool`,
109/// `TypedAddress<T>`, etc.) stamp themselves with the seal
110/// automatically. A user bypassing the macros with a bare
111/// `unsafe impl Pod` does **not** get `ZeroCopy` for free. `ZeroCopy`
112/// is implemented only through the framework-owned sealed path.
113pub unsafe trait ZeroCopy: Pod + 'static + __sealed::HopperZeroCopySealed {}
114
115// Blanket: any `Pod + 'static` type that also carries the seal gets
116// `ZeroCopy`. Every framework-defined surface carries the seal; the
117// blanket plus the seal together mean the trait is free for
118// framework users and opaque to bypassing code.
119unsafe impl<T> ZeroCopy for T where T: Pod + 'static + __sealed::HopperZeroCopySealed {}
120
121// ══════════════════════════════════════════════════════════════════════
122// WireLayout
123// ══════════════════════════════════════════════════════════════════════
124
125/// A `ZeroCopy` type with a compile-time-known wire size.
126///
127/// The associated constant defaults to `size_of::<Self>()`. The blanket
128/// implementation below applies that value to each `ZeroCopy` type.
129pub trait WireLayout: ZeroCopy {
130 /// Size of the on-wire representation, in bytes.
131 const WIRE_SIZE: usize = core::mem::size_of::<Self>();
132}
133
134// Blanket: every `ZeroCopy` type gets `WireLayout` with the default
135// `WIRE_SIZE`. Keeps the trait free for user code.
136impl<T: ZeroCopy> WireLayout for T {}
137
138// ══════════════════════════════════════════════════════════════════════
139// AccountLayout
140// ══════════════════════════════════════════════════════════════════════
141
142/// Hopper account layout identity, the top of the unified trait stack.
143///
144/// `WIRE_FINGERPRINT` is the first 8 bytes of the canonical SHA-256
145/// wire descriptor emitted by the `#[hopper::state]` expansion in the
146/// `hopper-derive` package, reinterpreted as a little-endian `u64`, so
147/// the runtime can compare against the on-account header byte-for-byte.
148///
149/// `SCHEMA_EPOCH` defaults to `1`; programs that publish later epochs
150/// via their on-chain manifest bump it to signal a version transition.
151pub trait AccountLayout: WireLayout {
152 /// On-chain discriminator (header byte 0).
153 const DISC: u8;
154 /// Layout version (header byte 1).
155 const VERSION: u8;
156 /// Canonical wire fingerprint (header bytes 4..12, little-endian).
157 const WIRE_FINGERPRINT: u64;
158 /// Schema-evolution epoch (header bytes 12..16).
159 const SCHEMA_EPOCH: u32 = 1;
160 /// Offset at which `Self` starts inside the account buffer.
161 /// `0` for header-inclusive layouts, `HEADER_LEN` for body-only.
162 const TYPE_OFFSET: usize;
163
164 /// Total data length an account must carry to hold `Self`.
165 #[inline(always)]
166 fn required_len() -> usize {
167 Self::TYPE_OFFSET + Self::WIRE_SIZE
168 }
169}
170
171// Blanket: every `LayoutContract` type automatically is an
172// `AccountLayout`. This makes the transition source-compatible -
173// `#[hopper::state]` emits `LayoutContract` today; downstream can
174// reach for either trait interchangeably.
175//
176// Fingerprint translation: `LayoutContract::LAYOUT_ID` is already a
177// `[u8; 8]` produced by the canonical wire-descriptor hash. We reinterpret
178// it as a little-endian `u64` for the `WIRE_FINGERPRINT` slot.
179impl<T: LayoutContract + ZeroCopy> AccountLayout for T {
180 const DISC: u8 = <T as LayoutContract>::DISC;
181 const VERSION: u8 = <T as LayoutContract>::VERSION;
182 const WIRE_FINGERPRINT: u64 = u64::from_le_bytes(<T as LayoutContract>::LAYOUT_ID);
183 const SCHEMA_EPOCH: u32 = <T as LayoutContract>::SCHEMA_EPOCH;
184 const TYPE_OFFSET: usize = <T as LayoutContract>::TYPE_OFFSET;
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 fn require_zero_copy<T: ZeroCopy>() {}
192 fn require_wire<T: WireLayout>() {}
193
194 #[test]
195 fn primitives_are_zero_copy_and_wire() {
196 require_zero_copy::<u8>();
197 require_zero_copy::<i8>();
198 require_zero_copy::<[u8; 32]>();
199 require_wire::<u8>();
200 require_wire::<i8>();
201 require_wire::<[u8; 32]>();
202 assert_eq!(<i8 as WireLayout>::WIRE_SIZE, 1);
203 assert_eq!(<[u8; 32] as WireLayout>::WIRE_SIZE, 32);
204 }
205
206 #[test]
207 fn address_is_zero_copy() {
208 require_zero_copy::<crate::address::Address>();
209 assert_eq!(<crate::address::Address as WireLayout>::WIRE_SIZE, 32);
210 }
211}