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