Skip to main content

hopper_core/
policy.rs

1//! Policy-Aware Capabilities -- tie instruction behavior to validation requirements.
2//!
3//! A `Capability` declares what an instruction intends to do (mutate treasury,
4//! touch journal, call external programs, etc.). An `InstructionPolicy` binds
5//! capabilities to the validation rules they require.
6//!
7//! ## How It Works
8//!
9//! 1. Declare which capabilities your instruction needs:
10//!    ```ignore
11//!    const DEPOSIT_CAPS: CapabilitySet = CapabilitySet::new()
12//!        .with(Capability::MutatesState)
13//!        .with(Capability::TouchesJournal);
14//!    ```
15//!
16//! 2. Define the policy that maps capabilities → validation requirements:
17//!    ```ignore
18//!    const POLICY: InstructionPolicy<4> = InstructionPolicy::new()
19//!        .when(Capability::MutatesState, PolicyRequirement::Authority)
20//!        .when(Capability::TouchesJournal, PolicyRequirement::JournalCapacity)
21//!        .when(Capability::ExternalCall, PolicyRequirement::PostMutationCheck);
22//!    ```
23//!
24//! 3. At runtime, enforce the policy against the instruction's declared caps:
25//!    ```ignore
26//!    policy.enforce(&DEPOSIT_CAPS, &ctx)?;
27//!    ```
28//!
29//! This makes Hopper **smart** -- capabilities automatically trigger the
30//! correct set of validation guards.
31
32/// Instruction capability flags.
33///
34/// Each capability is a single bit in a u32 bitmask.
35/// Programs declare which capabilities an instruction requires.
36#[derive(Clone, Copy, PartialEq, Eq)]
37#[repr(u8)]
38pub enum Capability {
39    /// Instruction reads owned account data.
40    ReadsState = 0,
41    /// Instruction mutates owned account data.
42    MutatesState = 1,
43    /// Instruction writes to a journal segment.
44    TouchesJournal = 2,
45    /// Instruction calls another program via CPI.
46    ExternalCall = 3,
47    /// Instruction modifies treasury/vault balances.
48    MutatesTreasury = 4,
49    /// Instruction performs account reallocation.
50    ReallocatesAccount = 5,
51    /// Instruction creates a new account.
52    CreatesAccount = 6,
53    /// Instruction closes an account.
54    ClosesAccount = 7,
55    /// Instruction modifies permissions/authority.
56    ModifiesAuthority = 8,
57    /// Instruction triggers a state machine transition.
58    TransitionsState = 9,
59}
60
61impl Capability {
62    /// Convert to bitmask.
63    #[inline(always)]
64    pub const fn mask(self) -> u32 {
65        1u32 << (self as u8)
66    }
67}
68
69/// A set of capabilities declared for an instruction.
70///
71/// Const-constructible bitmask.
72#[derive(Clone, Copy, PartialEq, Eq)]
73pub struct CapabilitySet {
74    bits: u32,
75}
76
77impl CapabilitySet {
78    /// Empty capability set (read-only instruction).
79    #[inline(always)]
80    pub const fn new() -> Self {
81        Self { bits: 0 }
82    }
83
84    /// Add a capability to the set.
85    #[inline(always)]
86    pub const fn with(self, cap: Capability) -> Self {
87        Self {
88            bits: self.bits | cap.mask(),
89        }
90    }
91
92    /// Check if a capability is present.
93    #[inline(always)]
94    pub const fn has(&self, cap: Capability) -> bool {
95        self.bits & cap.mask() != 0
96    }
97
98    /// Raw bitmask value.
99    #[inline(always)]
100    pub const fn bits(&self) -> u32 {
101        self.bits
102    }
103
104    /// Number of capabilities in the set.
105    #[inline(always)]
106    pub const fn count(&self) -> u32 {
107        self.bits.count_ones()
108    }
109
110    /// Union of two capability sets.
111    #[inline(always)]
112    pub const fn union(self, other: Self) -> Self {
113        Self {
114            bits: self.bits | other.bits,
115        }
116    }
117
118    /// Whether this set is a subset of another.
119    #[inline(always)]
120    pub const fn is_subset_of(&self, other: &Self) -> bool {
121        (self.bits & other.bits) == self.bits
122    }
123}
124
125/// What validation is required when a capability is active.
126#[derive(Clone, Copy, PartialEq, Eq)]
127#[repr(u8)]
128pub enum PolicyRequirement {
129    /// Must have a signer authority account.
130    Authority = 0,
131    /// Must verify journal segment has capacity.
132    JournalCapacity = 1,
133    /// Must run post-mutation validation bundle.
134    PostMutationCheck = 2,
135    /// Must pass CPI guard (assert_no_cpi or explicit allow).
136    CpiGuard = 3,
137    /// Must verify rent exemption after resize.
138    RentExemption = 4,
139    /// Must run invariant set after execution.
140    InvariantCheck = 5,
141    /// Must snapshot state before mutation (for receipts/rollback).
142    StateSnapshot = 6,
143    /// Must verify lamport conservation.
144    LamportConservation = 7,
145}
146
147impl PolicyRequirement {
148    /// Convert to bitmask.
149    #[inline(always)]
150    pub const fn mask(self) -> u32 {
151        1u32 << (self as u8)
152    }
153}
154
155/// A set of active policy requirements.
156#[derive(Clone, Copy, PartialEq, Eq)]
157pub struct RequirementSet {
158    bits: u32,
159}
160
161impl RequirementSet {
162    /// Empty.
163    #[inline(always)]
164    pub const fn new() -> Self {
165        Self { bits: 0 }
166    }
167
168    /// Add a requirement.
169    #[inline(always)]
170    pub const fn with(self, req: PolicyRequirement) -> Self {
171        Self {
172            bits: self.bits | req.mask(),
173        }
174    }
175
176    /// Check if a requirement is active.
177    #[inline(always)]
178    pub const fn has(&self, req: PolicyRequirement) -> bool {
179        self.bits & req.mask() != 0
180    }
181
182    /// Raw bitmask.
183    #[inline(always)]
184    pub const fn bits(&self) -> u32 {
185        self.bits
186    }
187}
188
189/// A policy rule: when capability C is active, requirement R must be met.
190#[derive(Clone, Copy)]
191pub struct PolicyRule {
192    pub capability: Capability,
193    pub requirement: PolicyRequirement,
194}
195
196/// Instruction policy -- maps capabilities to validation requirements.
197///
198/// Const-constructible, stack-allocated. At most N rules.
199pub struct InstructionPolicy<const N: usize> {
200    rules: [PolicyRule; N],
201    count: usize,
202}
203
204impl<const N: usize> InstructionPolicy<N> {
205    /// Create an empty policy.
206    #[inline(always)]
207    pub const fn new() -> Self {
208        Self {
209            rules: [PolicyRule {
210                capability: Capability::ReadsState,
211                requirement: PolicyRequirement::Authority,
212            }; N],
213            count: 0,
214        }
215    }
216
217    /// Add a policy rule: when `cap` is declared, `req` must be satisfied.
218    #[inline(always)]
219    pub const fn when(mut self, cap: Capability, req: PolicyRequirement) -> Self {
220        assert!(self.count < N, "policy rule overflow");
221        self.rules[self.count] = PolicyRule {
222            capability: cap,
223            requirement: req,
224        };
225        self.count += 1;
226        self
227    }
228
229    /// Resolve which requirements are needed for a given capability set.
230    ///
231    /// Returns the union of all requirements triggered by the declared capabilities.
232    #[inline]
233    pub const fn resolve(&self, caps: &CapabilitySet) -> RequirementSet {
234        let mut reqs = RequirementSet::new();
235        let mut i = 0;
236        while i < self.count {
237            if caps.has(self.rules[i].capability) {
238                reqs = reqs.with(self.rules[i].requirement);
239            }
240            i += 1;
241        }
242        reqs
243    }
244
245    /// Number of rules in this policy.
246    #[inline(always)]
247    pub const fn rule_count(&self) -> usize {
248        self.count
249    }
250}
251
252impl Default for CapabilitySet {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258impl Default for RequirementSet {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264impl<const N: usize> Default for InstructionPolicy<N> {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270// ---------------------------------------------------------------------------
271// Named Policy Packs
272// ---------------------------------------------------------------------------
273//
274// Pre-built policies for common instruction patterns. Use these directly or
275// as starting points. Each pack encodes the capabilities and validation
276// requirements that experienced Solana developers would wire by hand.
277
278/// Capabilities for an instruction that writes to treasury/vault balances.
279///
280/// Triggers: authority check + state snapshot + lamport conservation + invariants.
281pub const TREASURY_WRITE_POLICY: InstructionPolicy<4> = InstructionPolicy::new()
282    .when(Capability::MutatesState, PolicyRequirement::Authority)
283    .when(Capability::MutatesState, PolicyRequirement::StateSnapshot)
284    .when(
285        Capability::MutatesTreasury,
286        PolicyRequirement::LamportConservation,
287    )
288    .when(
289        Capability::MutatesTreasury,
290        PolicyRequirement::InvariantCheck,
291    );
292
293/// Capabilities for a treasury write instruction.
294pub const TREASURY_WRITE_CAPS: CapabilitySet = CapabilitySet::new()
295    .with(Capability::MutatesState)
296    .with(Capability::MutatesTreasury);
297
298/// Capabilities for an instruction that appends to a journal segment.
299///
300/// Triggers: authority check + journal capacity guard + state snapshot.
301pub const JOURNAL_TOUCH_POLICY: InstructionPolicy<3> = InstructionPolicy::new()
302    .when(Capability::MutatesState, PolicyRequirement::Authority)
303    .when(
304        Capability::TouchesJournal,
305        PolicyRequirement::JournalCapacity,
306    )
307    .when(Capability::TouchesJournal, PolicyRequirement::StateSnapshot);
308
309/// Capabilities for a journal-writing instruction.
310pub const JOURNAL_TOUCH_CAPS: CapabilitySet = CapabilitySet::new()
311    .with(Capability::MutatesState)
312    .with(Capability::TouchesJournal);
313
314/// Capabilities for an instruction that makes external calls via CPI.
315///
316/// Triggers: CPI guard + post-mutation check + state snapshot.
317pub const EXTERNAL_CALL_POLICY: InstructionPolicy<3> = InstructionPolicy::new()
318    .when(Capability::ExternalCall, PolicyRequirement::CpiGuard)
319    .when(
320        Capability::ExternalCall,
321        PolicyRequirement::PostMutationCheck,
322    )
323    .when(Capability::ExternalCall, PolicyRequirement::StateSnapshot);
324
325/// Capabilities for a CPI-invoking instruction.
326pub const EXTERNAL_CALL_CAPS: CapabilitySet = CapabilitySet::new().with(Capability::ExternalCall);
327
328/// Capabilities for an instruction that modifies shard data in a sharded account.
329///
330/// Triggers: authority + state snapshot + invariants.
331pub const SHARD_MUTATION_POLICY: InstructionPolicy<3> = InstructionPolicy::new()
332    .when(Capability::MutatesState, PolicyRequirement::Authority)
333    .when(Capability::MutatesState, PolicyRequirement::StateSnapshot)
334    .when(Capability::MutatesState, PolicyRequirement::InvariantCheck);
335
336/// Capabilities for a shard-modifying instruction.
337pub const SHARD_MUTATION_CAPS: CapabilitySet = CapabilitySet::new().with(Capability::MutatesState);
338
339/// Capabilities for an instruction that reallocates an account (migration-sensitive).
340///
341/// Triggers: authority + rent exemption + state snapshot + invariants.
342pub const MIGRATION_SENSITIVE_POLICY: InstructionPolicy<4> = InstructionPolicy::new()
343    .when(Capability::ReallocatesAccount, PolicyRequirement::Authority)
344    .when(
345        Capability::ReallocatesAccount,
346        PolicyRequirement::RentExemption,
347    )
348    .when(
349        Capability::ReallocatesAccount,
350        PolicyRequirement::StateSnapshot,
351    )
352    .when(
353        Capability::ReallocatesAccount,
354        PolicyRequirement::InvariantCheck,
355    );
356
357/// Capabilities for a migration/realloc instruction.
358pub const MIGRATION_SENSITIVE_CAPS: CapabilitySet = CapabilitySet::new()
359    .with(Capability::MutatesState)
360    .with(Capability::ReallocatesAccount);
361
362/// Capabilities for an instruction that modifies authority/permissions.
363///
364/// Triggers: authority + CPI guard + post-mutation check + invariants.
365pub const AUTHORITY_CHANGE_POLICY: InstructionPolicy<4> = InstructionPolicy::new()
366    .when(Capability::ModifiesAuthority, PolicyRequirement::Authority)
367    .when(Capability::ModifiesAuthority, PolicyRequirement::CpiGuard)
368    .when(
369        Capability::ModifiesAuthority,
370        PolicyRequirement::PostMutationCheck,
371    )
372    .when(
373        Capability::ModifiesAuthority,
374        PolicyRequirement::InvariantCheck,
375    );
376
377/// Capabilities for an authority change instruction.
378pub const AUTHORITY_CHANGE_CAPS: CapabilitySet = CapabilitySet::new()
379    .with(Capability::MutatesState)
380    .with(Capability::ModifiesAuthority);
381
382/// Capabilities for a read-only audit/inspection instruction.
383///
384/// Triggers: state snapshot only. No mutating capabilities.
385pub const READ_ONLY_AUDIT_POLICY: InstructionPolicy<1> =
386    InstructionPolicy::new().when(Capability::ReadsState, PolicyRequirement::StateSnapshot);
387
388/// Capabilities for a read-only audit instruction.
389pub const READ_ONLY_AUDIT_CAPS: CapabilitySet = CapabilitySet::new().with(Capability::ReadsState);
390
391/// Capabilities for an account initialization instruction.
392///
393/// Triggers: authority + rent exemption + invariants.
394pub const ACCOUNT_INIT_POLICY: InstructionPolicy<3> = InstructionPolicy::new()
395    .when(Capability::CreatesAccount, PolicyRequirement::Authority)
396    .when(Capability::CreatesAccount, PolicyRequirement::RentExemption)
397    .when(
398        Capability::CreatesAccount,
399        PolicyRequirement::InvariantCheck,
400    );
401
402/// Capabilities for an account init instruction.
403pub const ACCOUNT_INIT_CAPS: CapabilitySet = CapabilitySet::new().with(Capability::CreatesAccount);
404
405/// Capabilities for an account close instruction.
406///
407/// Triggers: authority + state snapshot + lamport conservation.
408pub const ACCOUNT_CLOSE_POLICY: InstructionPolicy<3> = InstructionPolicy::new()
409    .when(Capability::ClosesAccount, PolicyRequirement::Authority)
410    .when(Capability::ClosesAccount, PolicyRequirement::StateSnapshot)
411    .when(
412        Capability::ClosesAccount,
413        PolicyRequirement::LamportConservation,
414    );
415
416/// Capabilities for an account close instruction.
417pub const ACCOUNT_CLOSE_CAPS: CapabilitySet = CapabilitySet::new().with(Capability::ClosesAccount);
418
419// ---------------------------------------------------------------------------
420// Policy Pack Registry (for schema/manifest export)
421// ---------------------------------------------------------------------------
422
423/// Descriptor for a named policy pack with full metadata.
424///
425/// Used by schema export, CLI tooling, and Manager to describe each
426/// pre-built policy pack with its capabilities, validation requirements,
427/// receipt expectations, and invariant hints.
428#[derive(Clone, Copy)]
429pub struct PolicyPackDescriptor {
430    /// Short name (e.g. "TreasuryWrite").
431    pub name: &'static str,
432    /// Human-readable description of when to use this pack.
433    pub description: &'static str,
434    /// Capability set this pack covers.
435    pub capabilities: &'static CapabilitySet,
436    /// The policy (rule table) this pack enforces.
437    pub requirements: &'static [(&'static str, &'static str)],
438    /// Whether instructions using this pack should emit a receipt.
439    pub receipt_expected: bool,
440    /// Invariant hints (e.g. "balance_conservation", "authority_present").
441    pub invariant_hints: &'static [&'static str],
442}
443
444/// All named policy packs with full descriptors, in order.
445pub const NAMED_POLICY_PACKS: &[PolicyPackDescriptor] = &[
446    PolicyPackDescriptor {
447        name: "TreasuryWrite",
448        description: "Vault/treasury balance mutations. Enforces authority, snapshot, lamport conservation, and invariant checks.",
449        capabilities: &TREASURY_WRITE_CAPS,
450        requirements: &[
451            ("MutatesState", "Authority, StateSnapshot"),
452            ("MutatesTreasury", "LamportConservation, InvariantCheck"),
453        ],
454        receipt_expected: true,
455        invariant_hints: &["balance_conservation", "authority_present"],
456    },
457    PolicyPackDescriptor {
458        name: "JournalTouch",
459        description: "Journal segment writes. Enforces authority, capacity guard, and snapshot.",
460        capabilities: &JOURNAL_TOUCH_CAPS,
461        requirements: &[
462            ("MutatesState", "Authority"),
463            ("TouchesJournal", "JournalCapacity, StateSnapshot"),
464        ],
465        receipt_expected: true,
466        invariant_hints: &["journal_not_full"],
467    },
468    PolicyPackDescriptor {
469        name: "ExternalCall",
470        description: "CPI-invoking instructions. Enforces CPI guard, post-mutation check, and snapshot.",
471        capabilities: &EXTERNAL_CALL_CAPS,
472        requirements: &[
473            ("ExternalCall", "CpiGuard, PostMutationCheck, StateSnapshot"),
474        ],
475        receipt_expected: true,
476        invariant_hints: &["cpi_allowlisted"],
477    },
478    PolicyPackDescriptor {
479        name: "ShardMutation",
480        description: "Shard data modifications. Enforces authority, snapshot, and invariant checks.",
481        capabilities: &SHARD_MUTATION_CAPS,
482        requirements: &[
483            ("MutatesState", "Authority, StateSnapshot, InvariantCheck"),
484        ],
485        receipt_expected: true,
486        invariant_hints: &[],
487    },
488    PolicyPackDescriptor {
489        name: "MigrationSensitive",
490        description: "Account reallocation/migration. Enforces authority, rent exemption, snapshot, and invariant checks.",
491        capabilities: &MIGRATION_SENSITIVE_CAPS,
492        requirements: &[
493            ("ReallocatesAccount", "Authority, RentExemption, StateSnapshot, InvariantCheck"),
494        ],
495        receipt_expected: true,
496        invariant_hints: &["rent_exempt_after_realloc"],
497    },
498    PolicyPackDescriptor {
499        name: "AuthorityChange",
500        description: "Authority/permission modifications. Enforces authority, CPI guard, post-mutation, and invariant checks.",
501        capabilities: &AUTHORITY_CHANGE_CAPS,
502        requirements: &[
503            ("ModifiesAuthority", "Authority, CpiGuard, PostMutationCheck, InvariantCheck"),
504        ],
505        receipt_expected: true,
506        invariant_hints: &["new_authority_valid"],
507    },
508    PolicyPackDescriptor {
509        name: "ReadOnlyAudit",
510        description: "Read-only inspection/audit. Only requires snapshot for traceability.",
511        capabilities: &READ_ONLY_AUDIT_CAPS,
512        requirements: &[
513            ("ReadsState", "StateSnapshot"),
514        ],
515        receipt_expected: false,
516        invariant_hints: &[],
517    },
518    PolicyPackDescriptor {
519        name: "AccountInit",
520        description: "Account creation. Enforces authority, rent exemption, and invariant checks.",
521        capabilities: &ACCOUNT_INIT_CAPS,
522        requirements: &[
523            ("CreatesAccount", "Authority, RentExemption, InvariantCheck"),
524        ],
525        receipt_expected: true,
526        invariant_hints: &["header_initialized"],
527    },
528    PolicyPackDescriptor {
529        name: "AccountClose",
530        description: "Account closure. Enforces authority, snapshot, and lamport conservation.",
531        capabilities: &ACCOUNT_CLOSE_CAPS,
532        requirements: &[
533            ("ClosesAccount", "Authority, StateSnapshot, LamportConservation"),
534        ],
535        receipt_expected: true,
536        invariant_hints: &["sentinel_written"],
537    },
538];
539
540/// Capability name lookup.
541impl Capability {
542    /// Human-readable name for this capability.
543    #[inline]
544    pub const fn name(self) -> &'static str {
545        match self {
546            Self::ReadsState => "ReadsState",
547            Self::MutatesState => "MutatesState",
548            Self::TouchesJournal => "TouchesJournal",
549            Self::ExternalCall => "ExternalCall",
550            Self::MutatesTreasury => "MutatesTreasury",
551            Self::ReallocatesAccount => "ReallocatesAccount",
552            Self::CreatesAccount => "CreatesAccount",
553            Self::ClosesAccount => "ClosesAccount",
554            Self::ModifiesAuthority => "ModifiesAuthority",
555            Self::TransitionsState => "TransitionsState",
556        }
557    }
558}
559
560/// Requirement name lookup.
561impl PolicyRequirement {
562    /// Human-readable name for this requirement.
563    #[inline]
564    pub const fn name(self) -> &'static str {
565        match self {
566            Self::Authority => "Authority",
567            Self::JournalCapacity => "JournalCapacity",
568            Self::PostMutationCheck => "PostMutationCheck",
569            Self::CpiGuard => "CpiGuard",
570            Self::RentExemption => "RentExemption",
571            Self::InvariantCheck => "InvariantCheck",
572            Self::StateSnapshot => "StateSnapshot",
573            Self::LamportConservation => "LamportConservation",
574        }
575    }
576}
577
578// ---------------------------------------------------------------------------
579// Policy Class -- categorize what kind of operation a policy governs
580// ---------------------------------------------------------------------------
581
582/// High-level classification of what a policy governs.
583///
584/// Enables Manager, CLI, and receipt narration to group and describe policies
585/// meaningfully without parsing individual capability/requirement pairs.
586#[derive(Clone, Copy, Debug, PartialEq, Eq)]
587#[repr(u8)]
588pub enum PolicyClass {
589    /// Read-only inspection or audit.
590    Read = 0,
591    /// General state mutation.
592    Write = 1,
593    /// Financial operation (balance, treasury, token transfers).
594    Financial = 2,
595    /// Administrative operation (authority changes, permissions).
596    Administrative = 3,
597    /// Account lifecycle (create, close, migrate).
598    Lifecycle = 4,
599    /// Cross-program invocation.
600    CrossProgram = 5,
601    /// Governance or threshold operation (multisig, voting).
602    Governance = 6,
603}
604
605impl PolicyClass {
606    /// Human-readable name.
607    pub const fn name(self) -> &'static str {
608        match self {
609            Self::Read => "read",
610            Self::Write => "write",
611            Self::Financial => "financial",
612            Self::Administrative => "administrative",
613            Self::Lifecycle => "lifecycle",
614            Self::CrossProgram => "cross-program",
615            Self::Governance => "governance",
616        }
617    }
618
619    /// Whether this class involves any state mutation.
620    pub const fn is_mutating(self) -> bool {
621        !matches!(self, Self::Read)
622    }
623
624    /// Whether this class should require receipt emission.
625    pub const fn expects_receipt(self) -> bool {
626        !matches!(self, Self::Read)
627    }
628
629    /// Infer the policy class from a capability set.
630    pub const fn from_capabilities(caps: &CapabilitySet) -> Self {
631        if caps.has(Capability::MutatesTreasury) {
632            return Self::Financial;
633        }
634        if caps.has(Capability::ModifiesAuthority) {
635            return Self::Administrative;
636        }
637        if caps.has(Capability::CreatesAccount)
638            || caps.has(Capability::ClosesAccount)
639            || caps.has(Capability::ReallocatesAccount)
640        {
641            return Self::Lifecycle;
642        }
643        if caps.has(Capability::ExternalCall) {
644            return Self::CrossProgram;
645        }
646        if caps.has(Capability::MutatesState)
647            || caps.has(Capability::TouchesJournal)
648            || caps.has(Capability::TransitionsState)
649        {
650            return Self::Write;
651        }
652        Self::Read
653    }
654}
655
656impl core::fmt::Display for PolicyClass {
657    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
658        f.write_str(self.name())
659    }
660}