hopper_runtime/zerocopy.rs
1//! Unified zero-copy trait family.
2//!
3//! The Hopper Safety Audit's "structural" recommendation was to
4//! consolidate `Pod`, `FixedLayout`, `Projectable`, `SafeProjectable`,
5//! `LayoutContract`, header metadata, and schema export into one
6//! coherent trait stack. This module delivers the foundation:
7//!
8//! - [`ZeroCopy`], the canonical "safe to overlay on raw bytes"
9//! marker. Equivalent-in-contract to [`Pod`](crate::pod::Pod), which
10//! (under the default `hopper-native-backend` + `bytemuck` features)
11//! is a sub-trait of `bytemuck::Pod + bytemuck::Zeroable`.
12//! `ZeroCopy` is implemented for every `Pod` type via a blanket
13//! impl, so existing layouts participate automatically.
14//!
15//! - [`WireLayout`], a `ZeroCopy` type with a fixed wire size.
16//! Declared once via `const WIRE_SIZE = size_of::<Self>()` by
17//! default; macros may override if the in-memory and on-wire sizes
18//! diverge (none do today, but the hook is there for future
19//! compressed / tagged encodings).
20//!
21//! - [`AccountLayout`], a `WireLayout` that also carries Hopper's
22//! account header identity (disc, version, wire fingerprint, schema
23//! epoch, type offset). This is the audit's proposed top-level
24//! trait, matching its exact member list so the contract is
25//! frozen-in-place for migrations and client generation.
26//!
27//! ## Why three traits, not one
28//!
29//! The layering mirrors a real capability hierarchy. Every account
30//! layout is a wire layout; every wire layout is zero-copy; but not
31//! every zero-copy type is a full account layout (`u64`, `WireBool`,
32//! `TypedAddress<T>` are zero-copy but carry no header). Splitting
33//! the traits lets generic helpers demand just what they need.
34//!
35//! ## Relation to `LayoutContract`
36//!
37//! The existing [`crate::layout::LayoutContract`] trait predates this
38//! module. `LayoutContract` and `AccountLayout` intentionally overlap:
39//! both describe "a Hopper layout with disc/version/layout_id".
40//! `AccountLayout` is the audit-blessed name with the richer member
41//! list; `LayoutContract` is kept for backward compatibility and gets
42//! a blanket impl so any type deriving the latter automatically
43//! satisfies the former. New authoring surfaces (the proposed
44//! `#[hopper::state]` v2 expansion) should reach for `AccountLayout`.
45
46use crate::layout::LayoutContract;
47use crate::pod::Pod;
48
49// ══════════════════════════════════════════════════════════════════════
50// Seal (audit final-API Step 5)
51// ══════════════════════════════════════════════════════════════════════
52
53/// Internal marker every Hopper-authored zero-copy type stamps itself
54/// with. Sealed by convention: it lives in a doc-hidden module so
55/// downstream code cannot name it except through the canonical
56/// Hopper entry points (`#[hopper::pod]`, `#[hopper::state]`,
57/// `hopper_layout!`, and the framework's own primitive wire types).
58///
59/// This closes the Hopper Safety Audit's final-API-design Step 5:
60/// a user bypassing the macro system with a hand-rolled
61/// `unsafe impl Pod for Foo {}` cannot accidentally pick up
62/// [`ZeroCopy`] for free. The `ZeroCopy` blanket below additionally
63/// requires `HopperZeroCopySealed`, which only Hopper-authored
64/// surfaces implement.
65///
66/// Users who legitimately need to extend `ZeroCopy` for a custom
67/// primitive can declare `unsafe impl ::hopper_runtime::__sealed::HopperZeroCopySealed for MyType {}`
68/// manually, but the path-through-doc-hidden-module signals clearly
69/// that they are opting out of the macro's field-level proof.
70#[doc(hidden)]
71pub mod __sealed {
72 /// See the module-level documentation. Do not implement directly
73 /// unless you understand the full `Pod` + `bytemuck::Pod` +
74 /// alignment-1 + no-padding + no-interior-pointers contract.
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 u16 {}
84 unsafe impl HopperZeroCopySealed for u32 {}
85 unsafe impl HopperZeroCopySealed for u64 {}
86 unsafe impl HopperZeroCopySealed for u128 {}
87 unsafe impl HopperZeroCopySealed for i8 {}
88 unsafe impl HopperZeroCopySealed for i16 {}
89 unsafe impl HopperZeroCopySealed for i32 {}
90 unsafe impl HopperZeroCopySealed for i64 {}
91 unsafe impl HopperZeroCopySealed for i128 {}
92 unsafe impl<const N: usize> HopperZeroCopySealed for [u8; N] {}
93 unsafe impl HopperZeroCopySealed for () {}
94}
95
96// ══════════════════════════════════════════════════════════════════════
97// ZeroCopy
98// ══════════════════════════════════════════════════════════════════════
99
100/// Canonical marker for types that may be overlaid on raw bytes.
101///
102/// # Safety
103///
104/// The contract is the same four-point obligation as [`Pod`]:
105///
106/// 1. Every `[u8; size_of::<T>()]` bit pattern decodes to a valid `T`.
107/// 2. `align_of::<T>() == 1`.
108/// 3. `T` contains no padding.
109/// 4. `T` contains no internal pointers or references.
110///
111/// # Sealing
112///
113/// `ZeroCopy` is gated behind the doc-hidden
114/// [`__sealed::HopperZeroCopySealed`] marker. Types authored through
115/// `#[hopper::pod]`, `#[hopper::state]`, `hopper_layout!`, or one of
116/// the framework's own primitive wire types (`WireU64`, `WireBool`,
117/// `TypedAddress<T>`, etc.) stamp themselves with the seal
118/// automatically. A user bypassing the macros with a bare
119/// `unsafe impl Pod` does **not** get `ZeroCopy` for free, which
120/// closes the Hopper Safety Audit's Step 5 ("you cannot implement
121/// `ZeroCopy` manually, only via macro").
122pub unsafe trait ZeroCopy: Pod + 'static + __sealed::HopperZeroCopySealed {}
123
124// Blanket: any `Pod + 'static` type that also carries the seal gets
125// `ZeroCopy`. Every Hopper-authored surface carries the seal; the
126// blanket plus the seal together mean the trait is free for
127// framework users and opaque to bypassing code.
128unsafe impl<T> ZeroCopy for T where T: Pod + 'static + __sealed::HopperZeroCopySealed {}
129
130// ══════════════════════════════════════════════════════════════════════
131// WireLayout
132// ══════════════════════════════════════════════════════════════════════
133
134/// A `ZeroCopy` type with a compile-time-known wire size.
135///
136/// The default associated-const body returns `size_of::<Self>()`,
137/// which matches every Hopper layout today. Macros may override it
138/// in a future revision if the in-memory and on-wire representations
139/// ever diverge (e.g. compact trailing tags for optional fields).
140pub trait WireLayout: ZeroCopy {
141 /// Size of the on-wire representation, in bytes.
142 const WIRE_SIZE: usize = core::mem::size_of::<Self>();
143}
144
145// Blanket: every `ZeroCopy` type gets `WireLayout` with the default
146// `WIRE_SIZE`. Keeps the trait free for user code.
147impl<T: ZeroCopy> WireLayout for T {}
148
149// ══════════════════════════════════════════════════════════════════════
150// AccountLayout
151// ══════════════════════════════════════════════════════════════════════
152
153/// Hopper account layout identity, the top of the unified trait stack.
154///
155/// This is the audit-blessed trait: its member list matches the PDF's
156/// "proposed trait model" section exactly, so Hopper's long-term ABI
157/// story is anchored in the vocabulary the audit uses.
158///
159/// `WIRE_FINGERPRINT` is the first 8 bytes of the canonical SHA-256
160/// wire descriptor (see `hopper_macros_proc::state::layout_id_bytes`)
161/// reinterpreted as a little-endian `u64`, so the runtime can compare
162/// against the on-account header byte-for-byte.
163///
164/// `SCHEMA_EPOCH` defaults to `1`; programs that publish later epochs
165/// via their on-chain manifest bump it to signal a version transition.
166pub trait AccountLayout: WireLayout {
167 /// On-chain discriminator (header byte 0).
168 const DISC: u8;
169 /// Layout version (header byte 1).
170 const VERSION: u8;
171 /// Canonical wire fingerprint (header bytes 4..12, little-endian).
172 const WIRE_FINGERPRINT: u64;
173 /// Schema-evolution epoch (header bytes 12..16).
174 const SCHEMA_EPOCH: u32 = 1;
175 /// Offset at which `Self` starts inside the account buffer.
176 /// `0` for header-inclusive layouts, `HEADER_LEN` for body-only.
177 const TYPE_OFFSET: usize;
178
179 /// Total data length an account must carry to hold `Self`.
180 #[inline(always)]
181 fn required_len() -> usize {
182 Self::TYPE_OFFSET + Self::WIRE_SIZE
183 }
184}
185
186// Blanket: every `LayoutContract` type automatically is an
187// `AccountLayout`. This makes the transition source-compatible -
188// `#[hopper::state]` emits `LayoutContract` today; downstream can
189// reach for either trait interchangeably.
190//
191// Fingerprint translation: `LayoutContract::LAYOUT_ID` is already a
192// `[u8; 8]` produced by the canonical wire-descriptor hash. We reinterpret
193// it as a little-endian `u64` for the `WIRE_FINGERPRINT` slot.
194impl<T: LayoutContract + ZeroCopy> AccountLayout for T {
195 const DISC: u8 = <T as LayoutContract>::DISC;
196 const VERSION: u8 = <T as LayoutContract>::VERSION;
197 const WIRE_FINGERPRINT: u64 = u64::from_le_bytes(<T as LayoutContract>::LAYOUT_ID);
198 const SCHEMA_EPOCH: u32 = 1;
199 const TYPE_OFFSET: usize = <T as LayoutContract>::TYPE_OFFSET;
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 fn require_zero_copy<T: ZeroCopy>() {}
207 fn require_wire<T: WireLayout>() {}
208
209 #[test]
210 fn primitives_are_zero_copy_and_wire() {
211 require_zero_copy::<u8>();
212 require_zero_copy::<u64>();
213 require_zero_copy::<[u8; 32]>();
214 require_wire::<u8>();
215 require_wire::<u64>();
216 require_wire::<[u8; 32]>();
217 assert_eq!(<u64 as WireLayout>::WIRE_SIZE, 8);
218 assert_eq!(<[u8; 32] as WireLayout>::WIRE_SIZE, 32);
219 }
220
221 #[test]
222 fn address_is_zero_copy() {
223 require_zero_copy::<crate::address::Address>();
224 assert_eq!(<crate::address::Address as WireLayout>::WIRE_SIZE, 32);
225 }
226}