Skip to main content

hopper_core/state/
mod.rs

1//! State machine validation.
2//!
3//! Enforces valid state transitions for account lifecycle enums.
4//! Prevents invalid state jumps (e.g., Pending -> Closed skipping Active).
5
6use hopper_runtime::error::ProgramError;
7
8/// Check that a state transition is valid.
9///
10/// `valid_transitions` is a list of `(from, to)` pairs.
11/// Returns error if `(current, next)` is not in the list.
12#[inline]
13pub fn check_state_transition(
14    current: u8,
15    next: u8,
16    valid_transitions: &[(u8, u8)],
17) -> Result<(), ProgramError> {
18    for &(from, to) in valid_transitions {
19        if from == current && to == next {
20            return Ok(());
21        }
22    }
23    Err(ProgramError::InvalidAccountData)
24}
25
26/// Check that the state byte at `offset` in `data` matches `expected`.
27#[inline(always)]
28pub fn check_state(data: &[u8], offset: usize, expected: u8) -> Result<(), ProgramError> {
29    if offset >= data.len() {
30        return Err(ProgramError::AccountDataTooSmall);
31    }
32    if data[offset] != expected {
33        return Err(ProgramError::InvalidAccountData);
34    }
35    Ok(())
36}
37
38/// Check that the state byte at `offset` is NOT `rejected`.
39#[inline(always)]
40pub fn check_state_not(data: &[u8], offset: usize, rejected: u8) -> Result<(), ProgramError> {
41    if offset >= data.len() {
42        return Err(ProgramError::AccountDataTooSmall);
43    }
44    if data[offset] == rejected {
45        return Err(ProgramError::InvalidAccountData);
46    }
47    Ok(())
48}
49
50/// Check that the state byte at `offset` is in the `allowed` set.
51#[inline]
52pub fn check_state_in(data: &[u8], offset: usize, allowed: &[u8]) -> Result<(), ProgramError> {
53    if offset >= data.len() {
54        return Err(ProgramError::AccountDataTooSmall);
55    }
56    let state = data[offset];
57    for &a in allowed {
58        if state == a {
59            return Ok(());
60        }
61    }
62    Err(ProgramError::InvalidAccountData)
63}
64
65/// Write a new state byte at `offset`, validating the transition.
66#[inline]
67pub fn transition_state(
68    data: &mut [u8],
69    offset: usize,
70    next: u8,
71    valid_transitions: &[(u8, u8)],
72) -> Result<(), ProgramError> {
73    if offset >= data.len() {
74        return Err(ProgramError::AccountDataTooSmall);
75    }
76    let current = data[offset];
77    check_state_transition(current, next, valid_transitions)?;
78    data[offset] = next;
79    Ok(())
80}