Skip to main content

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}