Skip to main content

hopper_runtime/
policy.rs

1//! Program-level safety policy.
2//!
3//! Hopper's "policy-driven zero-copy runtime" model exposes each
4//! safety lever as a bit in a compile-time const struct. The
5//! `#[hopper::program(...)]` macro parses the attribute args and
6//! emits `pub const HOPPER_PROGRAM_POLICY: HopperProgramPolicy = ...;`
7//! inside the annotated module. Users read it back through
8//! [`HopperProgramPolicy`] to specialize handler paths.
9//!
10//! ## Named modes
11//!
12//! | Mode | Levers |
13//! |---|---|
14//! | [`HopperProgramPolicy::STRICT`] | `strict`, `enforce_token_checks`, `allow_unsafe` all on. Recommended default. |
15//! | [`HopperProgramPolicy::SEALED`] | `strict` + `enforce_token_checks` on, `allow_unsafe` off. Zero-`unsafe`-in-handlers programs. |
16//! | [`HopperProgramPolicy::RAW`] | Every lever off. Pinocchio-parity throughput. Responsibility shifts fully to the handler author. |
17//! | [`HopperProgramProfile::TINY`] | Binary-size intent marker for compact programs that still keep Hopper's safety envelope. |
18//!
19//! ## Zero runtime cost
20//!
21//! The policy is consumed by the program macro at compile time.
22//! `allow_unsafe = false` emits `#[deny(unsafe_code)]` on each
23//! handler so a stray `unsafe` block fails to compile. `strict`
24//! toggles auto-injection of `ContextSpec::bind(ctx)?` (which in turn
25//! calls `validate(ctx)?`). `enforce_token_checks` is a load-bearing
26//! promise read back by the author from
27//! `HOPPER_PROGRAM_POLICY.enforce_token_checks` to decide whether to
28//! invoke the `*Checked` token CPI pre-check helpers in handlers that
29//! reach outside the typed-context envelope.
30//!
31//! No runtime flag, no thread-local, no syscall. Users who need to
32//! branch on the policy inside a handler read the const directly:
33//!
34//! ```ignore
35//! if super::HOPPER_PROGRAM_POLICY.enforce_token_checks {
36//!     hopper_runtime::require!(authority.is_signer());
37//! }
38//! ```
39//!
40//! ## Per-instruction overrides
41//!
42//! A handler can override the program-level policy with
43//! `#[instruction(N, unsafe_memory, skip_token_checks)]`. The macro
44//! emits `pub const <HANDLER>_POLICY: HopperInstructionPolicy = ...;`
45//! alongside the handler so the same const-branch pattern works at
46//! the per-instruction grain.
47
48/// Program-level safety policy emitted by `#[hopper::program(...)]`.
49///
50/// Each field is a *compile-time* lever. The const value ends up
51/// inlined at every call site the program evaluates it from, so the
52/// branches fold away when a lever is known to be on or off at
53/// compile time.
54#[derive(Copy, Clone, Debug, PartialEq, Eq)]
55pub struct HopperProgramPolicy {
56    /// Program-level intent marker: handlers in this program run
57    /// under Hopper's full enforcement envelope.
58    ///
59    /// The actual per-handler behaviour is controlled by the
60    /// handler's context parameter type. A handler typed as
61    /// `Context<MyAccounts>` always runs `MyAccounts::bind(ctx)?`
62    /// (which chains into `validate(ctx)?`) regardless of policy. A
63    /// handler typed as `&mut Context<'_>` always receives the
64    /// context raw. `strict = true` is the documentation contract
65    /// that every handler in the module opts into the typed form;
66    /// `strict = false` signals the author intends to use raw
67    /// contexts and accepts the responsibility of calling
68    /// `validate()` manually where needed.
69    ///
70    /// The flag is read back by callers at compile time
71    /// (`HOPPER_PROGRAM_POLICY.strict`) to specialize code paths that
72    /// depend on whether the enforcement envelope is active.
73    pub strict: bool,
74
75    /// Token CPI authors must pair every raw invocation with the
76    /// matching `*Checked` builder (which carries the `decimals: u8`
77    /// byte the SPL Token program validates against the mint).
78    /// Handlers that do their own SPL plumbing read this back to
79    /// decide whether the signer + owner invariants are already
80    /// upheld elsewhere.
81    pub enforce_token_checks: bool,
82
83    /// Permit `unsafe { ... }` blocks inside handler bodies. When
84    /// false the program macro wraps each handler in
85    /// `#[deny(unsafe_code)]` so the compiler rejects any raw pointer
86    /// detour.
87    pub allow_unsafe: bool,
88}
89
90/// Program-size/audit profile emitted by `#[hopper::program(profile = "...")]`.
91#[derive(Copy, Clone, Debug, PartialEq, Eq)]
92#[repr(u8)]
93pub enum HopperProgramProfile {
94    Tiny = 0,
95    Strict = 1,
96    Audit = 2,
97    Raw = 3,
98}
99
100impl HopperProgramProfile {
101    pub const TINY: Self = Self::Tiny;
102    pub const STRICT: Self = Self::Strict;
103    pub const AUDIT: Self = Self::Audit;
104    pub const RAW: Self = Self::Raw;
105}
106
107impl HopperProgramPolicy {
108    /// Every safety lever engaged. The shipping default.
109    pub const STRICT: Self = Self {
110        strict: true,
111        enforce_token_checks: true,
112        allow_unsafe: true,
113    };
114
115    /// Strict + token checks + no `unsafe` in handlers. The zero-escape
116    /// mode for programs that never want to drop to raw pointers.
117    pub const SEALED: Self = Self {
118        strict: true,
119        enforce_token_checks: true,
120        allow_unsafe: false,
121    };
122
123    /// Every lever disengaged. Pinocchio-parity throughput with
124    /// responsibility pushed to the handler author.
125    pub const RAW: Self = Self {
126        strict: false,
127        enforce_token_checks: false,
128        allow_unsafe: true,
129    };
130
131    /// The shipping default, identical to [`HopperProgramPolicy::STRICT`].
132    ///
133    /// Exposed as a `const fn` so downstream macro expansion can
134    /// reach it from `const` context without an intermediate binding.
135    #[inline(always)]
136    pub const fn default_policy() -> Self {
137        Self::STRICT
138    }
139}
140
141impl Default for HopperProgramPolicy {
142    fn default() -> Self {
143        Self::default_policy()
144    }
145}
146
147/// Per-instruction policy override.
148///
149/// The `#[instruction(N, unsafe_memory, skip_token_checks, ctx_args = K)]`
150/// attribute emits `pub const <HANDLER>_POLICY: HopperInstructionPolicy = ...;`
151/// alongside the handler. All fields default to the inherit-from-program
152/// behaviour (`false` / `0`) so handlers without overrides get the program
153/// policy unchanged.
154#[derive(Copy, Clone, Debug, PartialEq, Eq)]
155pub struct HopperInstructionPolicy {
156    /// Opt this handler out of `#[deny(unsafe_code)]` even when the
157    /// program-level `allow_unsafe` is false. Used for the one or two
158    /// "fast path" handlers in an otherwise-sealed program.
159    pub unsafe_memory: bool,
160
161    /// Skip the program-level token-check promise for this handler.
162    /// The handler still compiles, but authors must document why the
163    /// token invariants are upheld through some other mechanism.
164    pub skip_token_checks: bool,
165
166    /// Count of leading instruction args the dispatcher threads to the
167    /// typed context's `bind_with_args(...)`. `0` means the context
168    /// (if any) is bound via `bind(ctx)?` and no args participate in
169    /// constraint evaluation. which is the legacy shape and matches
170    /// Anchor's non-`#[instruction]` accounts struct. When a context
171    /// was declared with `#[instruction(name: Type, ...)]`, the handler
172    /// must set `ctx_args` ≥ the number of declared args so that every
173    /// arg referenced by a seed / constraint resolves to a real typed
174    /// binding inside `bind_with_args`.
175    pub ctx_args: u8,
176}
177
178impl HopperInstructionPolicy {
179    /// Inherit every lever from the program-level policy.
180    pub const INHERIT: Self = Self {
181        unsafe_memory: false,
182        skip_token_checks: false,
183        ctx_args: 0,
184    };
185}
186
187impl Default for HopperInstructionPolicy {
188    fn default() -> Self {
189        Self::INHERIT
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn named_modes_differ_on_every_lever() {
199        assert!(HopperProgramPolicy::STRICT.strict);
200        assert!(HopperProgramPolicy::STRICT.enforce_token_checks);
201        assert!(HopperProgramPolicy::STRICT.allow_unsafe);
202
203        assert!(HopperProgramPolicy::SEALED.strict);
204        assert!(HopperProgramPolicy::SEALED.enforce_token_checks);
205        assert!(!HopperProgramPolicy::SEALED.allow_unsafe);
206
207        assert!(!HopperProgramPolicy::RAW.strict);
208        assert!(!HopperProgramPolicy::RAW.enforce_token_checks);
209        assert!(HopperProgramPolicy::RAW.allow_unsafe);
210    }
211
212    #[test]
213    fn program_profiles_are_stable() {
214        assert_eq!(HopperProgramProfile::TINY as u8, 0);
215        assert_eq!(HopperProgramProfile::STRICT as u8, 1);
216        assert_eq!(HopperProgramProfile::AUDIT as u8, 2);
217        assert_eq!(HopperProgramProfile::RAW as u8, 3);
218    }
219
220    #[test]
221    fn default_policy_is_strict() {
222        assert_eq!(HopperProgramPolicy::default(), HopperProgramPolicy::STRICT);
223        assert_eq!(
224            HopperProgramPolicy::default_policy(),
225            HopperProgramPolicy::STRICT
226        );
227    }
228
229    #[test]
230    fn instruction_inherit_zeroes_every_lever() {
231        assert!(!HopperInstructionPolicy::INHERIT.unsafe_memory);
232        assert!(!HopperInstructionPolicy::INHERIT.skip_token_checks);
233        assert_eq!(HopperInstructionPolicy::INHERIT.ctx_args, 0);
234        assert_eq!(
235            HopperInstructionPolicy::default(),
236            HopperInstructionPolicy::INHERIT
237        );
238    }
239
240    #[test]
241    fn instruction_ctx_args_round_trips() {
242        let p = HopperInstructionPolicy {
243            unsafe_memory: false,
244            skip_token_checks: false,
245            ctx_args: 3,
246        };
247        assert_eq!(p.ctx_args, 3);
248        assert_ne!(p, HopperInstructionPolicy::INHERIT);
249    }
250}