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//!
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}