Skip to main content

hopper_core/check/
graph.rs

1//! Composable Validation Pipeline.
2//!
3//! Three layers of validation composition:
4//!
5//! 1. **Atomic rules** -- `fn` pointers and closures (combinators).
6//!    `ValidationGraph` stores `fn` pointers for static rule sets.
7//!    `require_signer_at()` and friends return closures for inline use.
8//!
9//! 2. **Named groups and bundles** -- `ValidationGroup` bundles related rules under
10//!    a label for reuse. `ValidationBundle` composes groups into a single check.
11//!    `TransitionRulePack` dispatches rules by instruction tag.
12//!
13//! 3. **Post-mutation checks** -- `PostMutationValidator` holds checks that run
14//!    after account writes. Balance conservation, solvency invariants, authority
15//!    immutability -- anything that needs the final state to verify.
16//!
17//! `AccountConstraint` and `TransactionConstraint` provide builder-pattern
18//! validation for single accounts and global instruction-level checks.
19//!
20//! ```ignore
21//! // Named group for reuse across instructions:
22//! let mut signer_checks = ValidationGroup::<2>::new("signer_checks");
23//! signer_checks.add(validate_authority)?;
24//! signer_checks.add(validate_fee_payer)?;
25//!
26//! // Bundle groups together:
27//! let mut bundle = ValidationBundle::<2>::new();
28//! bundle.add(&signer_checks)?;
29//! bundle.add(&tx_constraint)?;
30//! bundle.run(&ctx)?;
31//!
32//! // Instruction-specific rules:
33//! let mut rules = TransitionRulePack::<8>::new();
34//! rules.add(0, validate_init)?;
35//! rules.add(1, validate_deposit)?;
36//! rules.run_for(instruction_tag, &ctx)?;
37//!
38//! // Post-mutation invariants:
39//! let mut post = PostMutationValidator::<2>::new();
40//! post.add(check_vault_solvent)?;
41//! post.run(accounts, program_id)?;
42//! ```
43
44use hopper_runtime::{error::ProgramError, AccountView, Address, ProgramResult};
45
46// -- Validation Node --
47
48/// A validation function signature.
49///
50/// Receives the full account slice and instruction data.
51/// Returns Ok(()) if validation passes, Err otherwise.
52pub type ValidateFn = fn(ctx: &ValidationContext) -> ProgramResult;
53
54/// Context passed to each validation node.
55pub struct ValidationContext<'a> {
56    /// The program ID.
57    pub program_id: &'a Address,
58    /// All accounts in the instruction.
59    pub accounts: &'a [AccountView],
60    /// Instruction data.
61    pub data: &'a [u8],
62}
63
64impl<'a> ValidationContext<'a> {
65    /// Create a new validation context.
66    #[inline(always)]
67    pub fn new(program_id: &'a Address, accounts: &'a [AccountView], data: &'a [u8]) -> Self {
68        Self {
69            program_id,
70            accounts,
71            data,
72        }
73    }
74
75    /// Get an account by index.
76    #[inline(always)]
77    pub fn account(&self, index: usize) -> Result<&'a AccountView, ProgramError> {
78        self.accounts
79            .get(index)
80            .ok_or(ProgramError::NotEnoughAccountKeys)
81    }
82
83    /// Require all account addresses to be unique.
84    #[inline(always)]
85    pub fn require_all_unique_accounts(&self) -> ProgramResult {
86        crate::check::guards::require_all_unique(self.accounts)
87    }
88
89    /// Require that duplicated addresses are never writable aliases.
90    #[inline(always)]
91    pub fn require_unique_writable_accounts(&self) -> ProgramResult {
92        crate::check::guards::require_unique_writable(self.accounts)
93    }
94
95    /// Require that duplicated addresses are never used as signer aliases.
96    #[inline(always)]
97    pub fn require_unique_signer_accounts(&self) -> ProgramResult {
98        crate::check::guards::require_unique_signers(self.accounts)
99    }
100}
101
102// -- Validation Pipeline (const-generic, stack-only) --
103
104/// A stack-allocated validation graph with up to `N` nodes.
105///
106/// Nodes execute sequentially. The graph can run in two modes:
107/// - **Fail-fast** (`run`): stops at first error
108/// - **Accumulate** (`run_all`): runs all nodes, returns first error
109pub struct ValidationGraph<const N: usize> {
110    nodes: [Option<ValidateFn>; N],
111    count: usize,
112}
113
114impl<const N: usize> ValidationGraph<N> {
115    /// Create an empty validation graph.
116    #[inline(always)]
117    pub fn new() -> Self {
118        Self {
119            nodes: [None; N],
120            count: 0,
121        }
122    }
123
124    /// Add a validation node.
125    #[inline]
126    pub fn add(&mut self, node: ValidateFn) -> Result<(), ProgramError> {
127        if self.count >= N {
128            return Err(ProgramError::InvalidArgument);
129        }
130        self.nodes[self.count] = Some(node);
131        self.count += 1;
132        Ok(())
133    }
134
135    /// Number of nodes in the graph.
136    #[inline(always)]
137    pub fn len(&self) -> usize {
138        self.count
139    }
140
141    /// Whether the graph has no nodes.
142    #[inline(always)]
143    pub fn is_empty(&self) -> bool {
144        self.count == 0
145    }
146
147    /// Run all validations, fail-fast on first error.
148    #[inline]
149    pub fn run(&self, ctx: &ValidationContext) -> ProgramResult {
150        let mut i = 0;
151        while i < self.count {
152            if let Some(node) = self.nodes[i] {
153                node(ctx)?;
154            }
155            i += 1;
156        }
157        Ok(())
158    }
159
160    /// Run all validations, accumulate results. Returns the first error found.
161    #[inline]
162    pub fn run_all(&self, ctx: &ValidationContext) -> ProgramResult {
163        let mut first_error: Option<ProgramError> = None;
164        let mut i = 0;
165        while i < self.count {
166            if let Some(node) = self.nodes[i] {
167                if let Err(e) = node(ctx) {
168                    if first_error.is_none() {
169                        first_error = Some(e);
170                    }
171                }
172            }
173            i += 1;
174        }
175        match first_error {
176            Some(e) => Err(e),
177            None => Ok(()),
178        }
179    }
180}
181
182// -- Validation Combinators --
183
184/// Validate that a specific account is a signer.
185#[inline(always)]
186pub fn require_signer_at(index: usize) -> impl Fn(&ValidationContext) -> ProgramResult {
187    move |ctx| {
188        let acc = ctx.account(index)?;
189        crate::check::check_signer(acc)
190    }
191}
192
193/// Validate that a specific account is writable.
194#[inline(always)]
195pub fn require_writable_at(index: usize) -> impl Fn(&ValidationContext) -> ProgramResult {
196    move |ctx| {
197        let acc = ctx.account(index)?;
198        crate::check::check_writable(acc)
199    }
200}
201
202/// Validate that a specific account is owned by the program.
203#[inline(always)]
204pub fn require_owned_at(index: usize) -> impl Fn(&ValidationContext) -> ProgramResult {
205    move |ctx| {
206        let acc = ctx.account(index)?;
207        crate::check::check_owner(acc, ctx.program_id)
208    }
209}
210
211/// Validate minimum instruction data length.
212#[inline(always)]
213pub fn require_data_min(min: usize) -> impl Fn(&ValidationContext) -> ProgramResult {
214    move |ctx| {
215        if ctx.data.len() < min {
216            Err(ProgramError::InvalidInstructionData)
217        } else {
218            Ok(())
219        }
220    }
221}
222
223/// Validate two accounts have the same key (e.g., stored address == provided account).
224#[inline(always)]
225pub fn require_keys_equal(a: usize, b: usize) -> impl Fn(&ValidationContext) -> ProgramResult {
226    move |ctx| {
227        let acc_a = ctx.account(a)?;
228        let acc_b = ctx.account(b)?;
229        crate::check::check_keys_eq(acc_a, acc_b)
230    }
231}
232
233/// Validate two accounts are different (no duplicates).
234#[inline(always)]
235pub fn require_unique(a: usize, b: usize) -> impl Fn(&ValidationContext) -> ProgramResult {
236    move |ctx| {
237        let acc_a = ctx.account(a)?;
238        let acc_b = ctx.account(b)?;
239        crate::check::check_accounts_unique(acc_a, acc_b)
240    }
241}
242
243/// Validate that all account addresses are unique.
244#[inline(always)]
245pub fn require_all_unique_accounts() -> impl Fn(&ValidationContext) -> ProgramResult {
246    move |ctx| ctx.require_all_unique_accounts()
247}
248
249/// Validate that no duplicated account is writable.
250#[inline(always)]
251pub fn require_unique_writable_accounts() -> impl Fn(&ValidationContext) -> ProgramResult {
252    move |ctx| ctx.require_unique_writable_accounts()
253}
254
255/// Validate that no duplicated account is used as a signer.
256#[inline(always)]
257pub fn require_unique_signer_accounts() -> impl Fn(&ValidationContext) -> ProgramResult {
258    move |ctx| ctx.require_unique_signer_accounts()
259}
260
261/// Validate that an account has at least `min` lamports.
262#[inline(always)]
263pub fn require_lamports_gte(
264    index: usize,
265    min: u64,
266) -> impl Fn(&ValidationContext) -> ProgramResult {
267    move |ctx| {
268        let acc = ctx.account(index)?;
269        crate::check::check_lamports_gte(acc, min)
270    }
271}
272
273// -- Constraint Builder --
274
275/// A builder for constructing validation constraints on a single account.
276///
277/// ```ignore
278/// AccountConstraint::on(0)
279///     .signer()
280///     .writable()
281///     .owned_by_program()
282///     .validate(&ctx)?;
283/// ```
284pub struct AccountConstraint {
285    index: usize,
286    require_signer: bool,
287    require_writable: bool,
288    require_owned: bool,
289    require_executable: bool,
290}
291
292impl AccountConstraint {
293    /// Start building constraints for an account at the given index.
294    #[inline(always)]
295    pub const fn on(index: usize) -> Self {
296        Self {
297            index,
298            require_signer: false,
299            require_writable: false,
300            require_owned: false,
301            require_executable: false,
302        }
303    }
304
305    /// Require the account to be a signer.
306    #[inline(always)]
307    pub const fn signer(mut self) -> Self {
308        self.require_signer = true;
309        self
310    }
311
312    /// Require the account to be writable.
313    #[inline(always)]
314    pub const fn writable(mut self) -> Self {
315        self.require_writable = true;
316        self
317    }
318
319    /// Require the account to be owned by the program.
320    #[inline(always)]
321    pub const fn owned(mut self) -> Self {
322        self.require_owned = true;
323        self
324    }
325
326    /// Require the account to be executable.
327    #[inline(always)]
328    pub const fn executable(mut self) -> Self {
329        self.require_executable = true;
330        self
331    }
332
333    /// Validate all constraints against the context.
334    #[inline]
335    pub fn validate(&self, ctx: &ValidationContext) -> ProgramResult {
336        let acc = ctx.account(self.index)?;
337
338        if self.require_signer {
339            crate::check::check_signer(acc)?;
340        }
341        if self.require_writable {
342            crate::check::check_writable(acc)?;
343        }
344        if self.require_owned {
345            crate::check::check_owner(acc, ctx.program_id)?;
346        }
347        if self.require_executable {
348            crate::check::check_executable(acc)?;
349        }
350
351        Ok(())
352    }
353}
354
355// -- Transaction-Level Validator --
356
357/// Transaction-level constraint that validates global properties.
358pub struct TransactionConstraint {
359    min_accounts: usize,
360    min_data_len: usize,
361    require_all_unique: bool,
362    require_unique_writable: bool,
363    require_unique_signers: bool,
364}
365
366impl TransactionConstraint {
367    /// Create a new transaction constraint.
368    #[inline(always)]
369    pub const fn new() -> Self {
370        Self {
371            min_accounts: 0,
372            min_data_len: 0,
373            require_all_unique: false,
374            require_unique_writable: false,
375            require_unique_signers: false,
376        }
377    }
378
379    /// Require at least N accounts.
380    #[inline(always)]
381    pub const fn min_accounts(mut self, n: usize) -> Self {
382        self.min_accounts = n;
383        self
384    }
385
386    /// Require at least N bytes of instruction data.
387    #[inline(always)]
388    pub const fn min_data(mut self, n: usize) -> Self {
389        self.min_data_len = n;
390        self
391    }
392
393    /// Require that all account addresses are distinct.
394    #[inline(always)]
395    pub const fn all_unique(mut self) -> Self {
396        self.require_all_unique = true;
397        self
398    }
399
400    /// Require that duplicated addresses are never writable aliases.
401    #[inline(always)]
402    pub const fn unique_writable(mut self) -> Self {
403        self.require_unique_writable = true;
404        self
405    }
406
407    /// Require that duplicated addresses are never signer aliases.
408    #[inline(always)]
409    pub const fn unique_signers(mut self) -> Self {
410        self.require_unique_signers = true;
411        self
412    }
413
414    /// Validate against a context.
415    #[inline]
416    pub fn validate(&self, ctx: &ValidationContext) -> ProgramResult {
417        if ctx.accounts.len() < self.min_accounts {
418            return Err(ProgramError::NotEnoughAccountKeys);
419        }
420        if ctx.data.len() < self.min_data_len {
421            return Err(ProgramError::InvalidInstructionData);
422        }
423        if self.require_all_unique {
424            ctx.require_all_unique_accounts()?;
425        }
426        if self.require_unique_writable {
427            ctx.require_unique_writable_accounts()?;
428        }
429        if self.require_unique_signers {
430            ctx.require_unique_signer_accounts()?;
431        }
432        Ok(())
433    }
434}
435
436// -- Named Validation Groups --
437
438/// A named group of validation rules.
439///
440/// Groups bundle related rules under a label for reuse across instructions.
441/// Example: a "token_transfer_preconditions" group that checks signer, writable,
442/// owner, and balance across the relevant accounts.
443///
444/// ```ignore
445/// let mut group = ValidationGroup::<4>::new("transfer_preconditions");
446/// group.add(require_signer_at(0))?;
447/// group.add(require_writable_at(1))?;
448/// group.add(require_owned_at(1))?;
449/// group.run(&ctx)?;
450/// ```
451pub struct ValidationGroup<const N: usize> {
452    name: &'static str,
453    rules: [Option<ValidateFn>; N],
454    count: usize,
455}
456
457impl<const N: usize> ValidationGroup<N> {
458    /// Create a new named validation group.
459    #[inline(always)]
460    pub const fn new(name: &'static str) -> Self {
461        Self {
462            name,
463            rules: [None; N],
464            count: 0,
465        }
466    }
467
468    /// Group name (for diagnostics/logging).
469    #[inline(always)]
470    pub const fn name(&self) -> &'static str {
471        self.name
472    }
473
474    /// Add a rule to the group.
475    #[inline]
476    pub fn add(&mut self, rule: ValidateFn) -> Result<(), ProgramError> {
477        if self.count >= N {
478            return Err(ProgramError::InvalidArgument);
479        }
480        self.rules[self.count] = Some(rule);
481        self.count += 1;
482        Ok(())
483    }
484
485    /// Number of rules in the group.
486    #[inline(always)]
487    pub fn len(&self) -> usize {
488        self.count
489    }
490
491    /// Whether the group has no rules.
492    #[inline(always)]
493    pub fn is_empty(&self) -> bool {
494        self.count == 0
495    }
496
497    /// Run all rules in the group. Fail-fast.
498    #[inline]
499    pub fn run(&self, ctx: &ValidationContext) -> ProgramResult {
500        let mut i = 0;
501        while i < self.count {
502            if let Some(rule) = self.rules[i] {
503                rule(ctx)?;
504            }
505            i += 1;
506        }
507        Ok(())
508    }
509}
510
511// -- Validation Bundle --
512
513/// A bundle that composes multiple `ValidationGroup`s into a single check.
514///
515/// Run all groups in order. If any group fails, the bundle fails.
516/// Useful for instruction handlers that share common preconditions
517/// but add instruction-specific checks on top.
518///
519/// ```ignore
520/// let mut bundle = ValidationBundle::<3>::new();
521/// bundle.add_group(&common_checks)?;
522/// bundle.add_group(&transfer_checks)?;
523/// bundle.run(&ctx)?;
524/// ```
525pub struct ValidationBundle<'a, const N: usize> {
526    groups: [Option<&'a dyn Validatable>; N],
527    count: usize,
528}
529
530/// Trait for validation runnables (groups and graphs).
531pub trait Validatable {
532    /// Run validation against the given context.
533    fn validate(&self, ctx: &ValidationContext) -> ProgramResult;
534}
535
536impl<const M: usize> Validatable for ValidationGraph<M> {
537    #[inline]
538    fn validate(&self, ctx: &ValidationContext) -> ProgramResult {
539        self.run(ctx)
540    }
541}
542
543impl<const M: usize> Validatable for ValidationGroup<M> {
544    #[inline]
545    fn validate(&self, ctx: &ValidationContext) -> ProgramResult {
546        self.run(ctx)
547    }
548}
549
550impl Validatable for TransactionConstraint {
551    #[inline]
552    fn validate(&self, ctx: &ValidationContext) -> ProgramResult {
553        TransactionConstraint::validate(self, ctx)
554    }
555}
556
557impl<'a, const N: usize> ValidationBundle<'a, N> {
558    /// Create an empty bundle.
559    #[inline(always)]
560    pub const fn new() -> Self {
561        Self {
562            groups: [None; N],
563            count: 0,
564        }
565    }
566
567    /// Add a validatable group or graph to the bundle.
568    #[inline]
569    pub fn add(&mut self, v: &'a dyn Validatable) -> Result<(), ProgramError> {
570        if self.count >= N {
571            return Err(ProgramError::InvalidArgument);
572        }
573        self.groups[self.count] = Some(v);
574        self.count += 1;
575        Ok(())
576    }
577
578    /// Run all groups in order. Fail-fast on first error.
579    #[inline]
580    pub fn run(&self, ctx: &ValidationContext) -> ProgramResult {
581        let mut i = 0;
582        while i < self.count {
583            if let Some(v) = self.groups[i] {
584                v.validate(ctx)?;
585            }
586            i += 1;
587        }
588        Ok(())
589    }
590}
591
592// -- Post-Mutation Validator --
593
594/// Signature for a post-mutation check function.
595///
596/// Receives the account slice after mutation. Can inspect state
597/// for invariants that should hold after any write.
598pub type PostMutationFn = fn(accounts: &[AccountView], program_id: &Address) -> ProgramResult;
599
600/// Collects post-mutation checks that run after instruction execution.
601///
602/// Use this to verify invariants that can't be checked upfront:
603/// balance conservation, escrow solvency, authority immutability, etc.
604///
605/// ```ignore
606/// let mut post = PostMutationValidator::<4>::new();
607/// post.add(check_vault_solvent)?;
608/// post.add(check_balance_conserved)?;
609///
610/// // ... execute mutations ...
611///
612/// post.run(accounts, program_id)?;
613/// ```
614pub struct PostMutationValidator<const N: usize> {
615    checks: [Option<PostMutationFn>; N],
616    count: usize,
617}
618
619impl<const N: usize> PostMutationValidator<N> {
620    /// Create an empty post-mutation validator.
621    #[inline(always)]
622    pub const fn new() -> Self {
623        Self {
624            checks: [None; N],
625            count: 0,
626        }
627    }
628
629    /// Add a post-mutation check.
630    #[inline]
631    pub fn add(&mut self, check: PostMutationFn) -> Result<(), ProgramError> {
632        if self.count >= N {
633            return Err(ProgramError::InvalidArgument);
634        }
635        self.checks[self.count] = Some(check);
636        self.count += 1;
637        Ok(())
638    }
639
640    /// Number of checks registered.
641    #[inline(always)]
642    pub fn len(&self) -> usize {
643        self.count
644    }
645
646    /// Whether no checks are registered.
647    #[inline(always)]
648    pub fn is_empty(&self) -> bool {
649        self.count == 0
650    }
651
652    /// Run all post-mutation checks. Fail-fast.
653    #[inline]
654    pub fn run(&self, accounts: &[AccountView], program_id: &Address) -> ProgramResult {
655        let mut i = 0;
656        while i < self.count {
657            if let Some(check) = self.checks[i] {
658                check(accounts, program_id)?;
659            }
660            i += 1;
661        }
662        Ok(())
663    }
664}
665
666// -- Transition-Specific Rule Pack --
667
668/// Instruction dispatch tag for associating validation rules with specific instructions.
669pub type InstructionTag = u8;
670
671/// A rule pack entry: instruction tag + validation function.
672#[derive(Clone, Copy)]
673struct TransitionRuleEntry {
674    tag: InstructionTag,
675    rule: ValidateFn,
676}
677
678/// Associates validation rules with specific instruction tags.
679///
680/// Each instruction in your program can have its own set of checks,
681/// looked up by the dispatch tag. This avoids running irrelevant
682/// checks for instructions that don't need them.
683///
684/// ```ignore
685/// let mut tr = TransitionRulePack::<16>::new();
686/// tr.add(0, validate_init_accounts)?;   // Init
687/// tr.add(1, validate_deposit_accounts)?; // Deposit
688/// tr.add(2, validate_withdraw_accounts)?; // Withdraw
689///
690/// // In handler: run only rules for this instruction
691/// tr.run_for(instruction_tag, &ctx)?;
692/// ```
693pub struct TransitionRulePack<const N: usize> {
694    entries: [Option<TransitionRuleEntry>; N],
695    count: usize,
696}
697
698impl<const N: usize> TransitionRulePack<N> {
699    /// Create an empty rule pack.
700    #[inline(always)]
701    pub const fn new() -> Self {
702        Self {
703            entries: [None; N],
704            count: 0,
705        }
706    }
707
708    /// Register a rule for a specific instruction tag.
709    #[inline]
710    pub fn add(&mut self, tag: InstructionTag, rule: ValidateFn) -> Result<(), ProgramError> {
711        if self.count >= N {
712            return Err(ProgramError::InvalidArgument);
713        }
714        self.entries[self.count] = Some(TransitionRuleEntry { tag, rule });
715        self.count += 1;
716        Ok(())
717    }
718
719    /// Run all rules matching the given instruction tag. Fail-fast.
720    #[inline]
721    pub fn run_for(&self, tag: InstructionTag, ctx: &ValidationContext) -> ProgramResult {
722        let mut i = 0;
723        while i < self.count {
724            if let Some(entry) = &self.entries[i] {
725                if entry.tag == tag {
726                    (entry.rule)(ctx)?;
727                }
728            }
729            i += 1;
730        }
731        Ok(())
732    }
733
734    /// Number of entries.
735    #[inline(always)]
736    pub fn len(&self) -> usize {
737        self.count
738    }
739
740    /// Whether the rule pack has no entries.
741    #[inline(always)]
742    pub fn is_empty(&self) -> bool {
743        self.count == 0
744    }
745}
746
747// -- Default impls --
748
749impl<const N: usize> Default for ValidationGraph<N> {
750    fn default() -> Self {
751        Self::new()
752    }
753}
754
755impl Default for TransactionConstraint {
756    fn default() -> Self {
757        Self::new()
758    }
759}
760
761impl<'a, const N: usize> Default for ValidationBundle<'a, N> {
762    fn default() -> Self {
763        Self::new()
764    }
765}
766
767impl<const N: usize> Default for PostMutationValidator<N> {
768    fn default() -> Self {
769        Self::new()
770    }
771}
772
773impl<const N: usize> Default for TransitionRulePack<N> {
774    fn default() -> Self {
775        Self::new()
776    }
777}