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. Native hot-path throughput. Responsibility shifts fully to the handler author. |
17//! | [`HopperProgramProfile::TINY`] | Binary-size profile for compact programs: one-byte instruction discriminators and no handler-level modifier instrumentation. |
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, allow_arbitrary_cpi)]`. 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. Native hot-path 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, allow_arbitrary_cpi, 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    /// Marks a handler as intentionally able to invoke arbitrary external
167    /// programs, for governance/proposal executors and plugin dispatchers.
168    /// Hopper does not forbid this path; the flag makes the capability visible
169    /// to generated schema, review tools, and audit-oriented explain output.
170    pub allow_arbitrary_cpi: bool,
171
172    /// Count of leading instruction args the dispatcher threads to the
173    /// typed context's `bind_with_args(...)`. `0` means the context
174    /// (if any) is bound via `bind(ctx)?` and no args participate in
175    /// constraint evaluation. which is the legacy shape and matches
176    /// Anchor's non-`#[instruction]` accounts struct. When a context
177    /// was declared with `#[instruction(name: Type, ...)]`, the handler
178    /// must set `ctx_args` equal to the number of declared args. Generated
179    /// code also pins identical names and order, so every seed, constraint,
180    /// and exact-cell selector resolves to the same wire value off chain and
181    /// on chain.
182    pub ctx_args: u8,
183}
184
185impl HopperInstructionPolicy {
186    /// Inherit every lever from the program-level policy.
187    pub const INHERIT: Self = Self {
188        unsafe_memory: false,
189        skip_token_checks: false,
190        allow_arbitrary_cpi: false,
191        ctx_args: 0,
192    };
193}
194
195impl Default for HopperInstructionPolicy {
196    fn default() -> Self {
197        Self::INHERIT
198    }
199}
200
201#[cfg(test)]
202// These tests assert the field values of `const` policy profiles; the constant
203// value of each assertion is precisely the invariant under test.
204#[allow(clippy::assertions_on_constants)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn named_modes_differ_on_every_lever() {
210        assert!(HopperProgramPolicy::STRICT.strict);
211        assert!(HopperProgramPolicy::STRICT.enforce_token_checks);
212        assert!(HopperProgramPolicy::STRICT.allow_unsafe);
213
214        assert!(HopperProgramPolicy::SEALED.strict);
215        assert!(HopperProgramPolicy::SEALED.enforce_token_checks);
216        assert!(!HopperProgramPolicy::SEALED.allow_unsafe);
217
218        assert!(!HopperProgramPolicy::RAW.strict);
219        assert!(!HopperProgramPolicy::RAW.enforce_token_checks);
220        assert!(HopperProgramPolicy::RAW.allow_unsafe);
221    }
222
223    #[test]
224    fn program_profiles_are_stable() {
225        assert_eq!(HopperProgramProfile::TINY as u8, 0);
226        assert_eq!(HopperProgramProfile::STRICT as u8, 1);
227        assert_eq!(HopperProgramProfile::AUDIT as u8, 2);
228        assert_eq!(HopperProgramProfile::RAW as u8, 3);
229    }
230
231    #[test]
232    fn default_policy_is_strict() {
233        assert_eq!(HopperProgramPolicy::default(), HopperProgramPolicy::STRICT);
234        assert_eq!(
235            HopperProgramPolicy::default_policy(),
236            HopperProgramPolicy::STRICT
237        );
238    }
239
240    #[test]
241    fn instruction_inherit_zeroes_every_lever() {
242        assert!(!HopperInstructionPolicy::INHERIT.unsafe_memory);
243        assert!(!HopperInstructionPolicy::INHERIT.skip_token_checks);
244        assert!(!HopperInstructionPolicy::INHERIT.allow_arbitrary_cpi);
245        assert_eq!(HopperInstructionPolicy::INHERIT.ctx_args, 0);
246        assert_eq!(
247            HopperInstructionPolicy::default(),
248            HopperInstructionPolicy::INHERIT
249        );
250    }
251
252    #[test]
253    fn instruction_ctx_args_round_trips() {
254        let p = HopperInstructionPolicy {
255            unsafe_memory: false,
256            skip_token_checks: false,
257            allow_arbitrary_cpi: false,
258            ctx_args: 3,
259        };
260        assert_eq!(p.ctx_args, 3);
261        assert_ne!(p, HopperInstructionPolicy::INHERIT);
262    }
263}