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 // SAFETY: `u8`, `i8`, `[u8; N]`, and `()` have alignment 1, no
78 // padding, no pointers, and accept every bit pattern: the seal's
79 // contract, stated on the trait above.
80 //
81 // Framework-provided primitives. Every Rust-level `Pod` integer
82 // and `[u8; N]` is Hopper-owned by virtue of being in the
83 // substrate, so stamp the seal here. Users reading/writing these
84 // via `ForeignLens::field::<T, OFFSET>` or equivalent paths get
85 // `ZeroCopy` for free.
86 unsafe impl HopperZeroCopySealed for u8 {}
87 unsafe impl HopperZeroCopySealed for i8 {}
88 unsafe impl<const N: usize> HopperZeroCopySealed for [u8; N] {}
89 unsafe impl HopperZeroCopySealed for () {}
90}
91
92// ══════════════════════════════════════════════════════════════════════
93// ZeroCopy
94// ══════════════════════════════════════════════════════════════════════
95
96/// Canonical marker for types that may be overlaid on raw bytes.
97///
98/// # Safety
99///
100/// The contract is the same four-point obligation as [`Pod`]:
101///
102/// 1. Every `[u8; size_of::<T>()]` bit pattern decodes to a valid `T`.
103/// 2. `align_of::<T>() == 1`.
104/// 3. `T` contains no padding.
105/// 4. `T` contains no internal pointers or references.
106///
107/// # Sealing
108///
109/// `ZeroCopy` is gated behind the doc-hidden
110/// [`__sealed::HopperZeroCopySealed`] marker. Types authored through
111/// `#[hopper::pod]`, `#[hopper::state]`, `hopper_layout!`, or one of
112/// the framework's own primitive wire types (`WireU64`, `WireBool`,
113/// `TypedAddress<T>`, etc.) stamp themselves with the seal
114/// automatically. A user bypassing the macros with a bare
115/// `unsafe impl Pod` does **not** get `ZeroCopy` for free. `ZeroCopy`
116/// is implemented only through the framework-owned sealed path.
117pub unsafe trait ZeroCopy: Pod + 'static + __sealed::HopperZeroCopySealed {}
118
119// SAFETY: `ZeroCopy` has the contract of `Pod`, which the bound
120// supplies; the seal bound restricts who can reach the impl and adds no
121// obligation of its own.
122//
123// Blanket: any `Pod + 'static` type that also carries the seal gets
124// `ZeroCopy`. Every framework-defined surface carries the seal; the
125// blanket plus the seal together mean the trait is free for
126// framework users and opaque to bypassing code.
127unsafe impl<T> ZeroCopy for T where T: Pod + 'static + __sealed::HopperZeroCopySealed {}
128
129// ══════════════════════════════════════════════════════════════════════
130// WireLayout
131// ══════════════════════════════════════════════════════════════════════
132
133/// A `ZeroCopy` type with a compile-time-known wire size.
134///
135/// The associated constant defaults to `size_of::<Self>()`. The blanket
136/// implementation below applies that value to each `ZeroCopy` type.
137pub trait WireLayout: ZeroCopy {
138 /// Size of the on-wire representation, in bytes.
139 const WIRE_SIZE: usize = core::mem::size_of::<Self>();
140}
141
142// Blanket: every `ZeroCopy` type gets `WireLayout` with the default
143// `WIRE_SIZE`. Keeps the trait free for user code.
144impl<T: ZeroCopy> WireLayout for T {}
145
146// ══════════════════════════════════════════════════════════════════════
147// AccountLayout
148// ══════════════════════════════════════════════════════════════════════
149
150/// Hopper account layout identity, the top of the unified trait stack.
151///
152/// `WIRE_FINGERPRINT` is the first 8 bytes of the canonical SHA-256
153/// wire descriptor emitted by the `#[hopper::state]` expansion in the
154/// `hopper-derive` package, reinterpreted as a little-endian `u64`, so
155/// the runtime can compare against the on-account header byte-for-byte.
156///
157/// `SCHEMA_EPOCH` defaults to `1`; programs that publish later epochs
158/// via their on-chain manifest bump it to signal a version transition.
159pub trait AccountLayout: WireLayout {
160 /// On-chain discriminator (header byte 0).
161 const DISC: u8;
162 /// Layout version (header byte 1).
163 const VERSION: u8;
164 /// Canonical wire fingerprint (header bytes 4..12, little-endian).
165 const WIRE_FINGERPRINT: u64;
166 /// Schema-evolution epoch (header bytes 12..16).
167 const SCHEMA_EPOCH: u32 = 1;
168 /// Offset at which `Self` starts inside the account buffer.
169 /// `0` for header-inclusive layouts, `HEADER_LEN` for body-only.
170 const TYPE_OFFSET: usize;
171
172 /// Total data length an account must carry to hold `Self`.
173 #[inline(always)]
174 fn required_len() -> usize {
175 Self::TYPE_OFFSET + Self::WIRE_SIZE
176 }
177}
178
179// Blanket: every `LayoutContract` type automatically is an
180// `AccountLayout`. This makes the transition source-compatible -
181// `#[hopper::state]` emits `LayoutContract` today; downstream can
182// reach for either trait interchangeably.
183//
184// Fingerprint translation: `LayoutContract::LAYOUT_ID` is already a
185// `[u8; 8]` produced by the canonical wire-descriptor hash. We reinterpret
186// it as a little-endian `u64` for the `WIRE_FINGERPRINT` slot.
187impl<T: LayoutContract + ZeroCopy> AccountLayout for T {
188 const DISC: u8 = <T as LayoutContract>::DISC;
189 const VERSION: u8 = <T as LayoutContract>::VERSION;
190 const WIRE_FINGERPRINT: u64 = u64::from_le_bytes(<T as LayoutContract>::LAYOUT_ID);
191 const SCHEMA_EPOCH: u32 = <T as LayoutContract>::SCHEMA_EPOCH;
192 const TYPE_OFFSET: usize = <T as LayoutContract>::TYPE_OFFSET;
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn require_zero_copy<T: ZeroCopy>() {}
200 fn require_wire<T: WireLayout>() {}
201
202 #[test]
203 fn primitives_are_zero_copy_and_wire() {
204 require_zero_copy::<u8>();
205 require_zero_copy::<i8>();
206 require_zero_copy::<[u8; 32]>();
207 require_wire::<u8>();
208 require_wire::<i8>();
209 require_wire::<[u8; 32]>();
210 assert_eq!(<i8 as WireLayout>::WIRE_SIZE, 1);
211 assert_eq!(<[u8; 32] as WireLayout>::WIRE_SIZE, 32);
212 }
213
214 #[test]
215 fn address_is_zero_copy() {
216 require_zero_copy::<crate::address::Address>();
217 assert_eq!(<crate::address::Address as WireLayout>::WIRE_SIZE, 32);
218 }
219}