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}