Skip to main content

hopper_runtime/
error.rs

1//! Hopper-owned program error type for Solana on-chain programs.
2//!
3//! Each variant maps to a fixed u64 error code returned to the Solana runtime.
4
5/// Errors that a Solana program can return.
6///
7/// This is part of the Hopper runtime type surface. Variant discriminants
8/// match the Solana runtime ABI.
9///
10/// `#[repr(u64)]` with EXPLICIT sequential discriminants is load-bearing
11/// for binary size, exactly as on `hopper_native::error::ProgramError`
12/// (which this type mirrors variant-for-variant): fieldless variant `k`
13/// encodes to the runtime code `(k + 1) << 32`, so the `u64` lowering is
14/// one tag read + one shift instead of a 25-arm match inlined into every
15/// entrypoint, and the native<->runtime glue converts by ROUND-TRIPPING
16/// the u64 code instead of two more 25-arm identity matches. The DWARF
17/// size attribution measured the match forms at 880 bytes, 13% of the
18/// parity vault's `.text`. Append new variants with the next sequential
19/// discriminant, mirrored in the native enum; the exhaustive tests below
20/// refuse to compile otherwise.
21#[derive(Clone, Debug, Eq, PartialEq)]
22#[repr(u64)]
23pub enum ProgramError {
24    /// Custom program error with a u32 code.
25    Custom(u32) = 0,
26    InvalidArgument = 1,
27    InvalidInstructionData = 2,
28    InvalidAccountData = 3,
29    AccountDataTooSmall = 4,
30    InsufficientFunds = 5,
31    IncorrectProgramId = 6,
32    MissingRequiredSignature = 7,
33    AccountAlreadyInitialized = 8,
34    UninitializedAccount = 9,
35    NotEnoughAccountKeys = 10,
36    AccountBorrowFailed = 11,
37    MaxSeedLengthExceeded = 12,
38    InvalidSeeds = 13,
39    BorshIoError = 14,
40    AccountNotRentExempt = 15,
41    UnsupportedSysvar = 16,
42    IllegalOwner = 17,
43    MaxAccountsDataAllocationsExceeded = 18,
44    InvalidRealloc = 19,
45    MaxInstructionTraceLengthExceeded = 20,
46    BuiltinProgramsMustConsumeComputeUnits = 21,
47    InvalidAccountOwner = 22,
48    ArithmeticOverflow = 23,
49    Immutable = 24,
50    IncorrectAuthority = 25,
51}
52
53// ── u64 conversion (Solana runtime ABI) ──────────────────────────────
54
55/// Map a builtin error index to its runtime u64 code.
56const BUILTIN_BIT_SHIFT: usize = 32;
57const CUSTOM_ZERO: u64 = 1_u64 << BUILTIN_BIT_SHIFT;
58
59const BUILTIN_LOW_MASK: u64 = (1_u64 << BUILTIN_BIT_SHIFT) - 1;
60
61/// Reference builtin encoding, kept ONLY as the test oracle: the
62/// shipped conversion reads the enum tag directly, and the golden
63/// tests re-derive every variant's code through this original
64/// arithmetic to pin the two forever equal.
65#[cfg(test)]
66#[inline(always)]
67const fn to_builtin(index: u64) -> u64 {
68    (index + 2) << BUILTIN_BIT_SHIFT
69}
70
71impl From<ProgramError> for u64 {
72    #[inline]
73    fn from(err: ProgramError) -> u64 {
74        match err {
75            ProgramError::Custom(0) => CUSTOM_ZERO,
76            ProgramError::Custom(code) => code as u64,
77            builtin => {
78                // SAFETY: `ProgramError` is `#[repr(u64)]`, which
79                // guarantees the discriminant is stored as a leading
80                // `u64` tag readable through a pointer cast (RFC 2195
81                // primitive-representation layout). `builtin` is one of
82                // the fieldless variants (Custom was matched above), so
83                // its tag is the explicit discriminant `1..=25`.
84                let tag = unsafe { *(&builtin as *const ProgramError as *const u64) };
85                // Variant k encodes to (k + 1) << 32, the old per-arm
86                // to_builtin(k - 1) table without the 25-arm match.
87                (tag + 1) << BUILTIN_BIT_SHIFT
88            }
89        }
90    }
91}
92
93impl From<u64> for ProgramError {
94    fn from(code: u64) -> Self {
95        if code == CUSTOM_ZERO {
96            return ProgramError::Custom(0);
97        }
98        let builtin = code >> BUILTIN_BIT_SHIFT;
99        if code & BUILTIN_LOW_MASK == 0 && builtin >= 2 {
100            match builtin - 2 {
101                0 => return ProgramError::InvalidArgument,
102                1 => return ProgramError::InvalidInstructionData,
103                2 => return ProgramError::InvalidAccountData,
104                3 => return ProgramError::AccountDataTooSmall,
105                4 => return ProgramError::InsufficientFunds,
106                5 => return ProgramError::IncorrectProgramId,
107                6 => return ProgramError::MissingRequiredSignature,
108                7 => return ProgramError::AccountAlreadyInitialized,
109                8 => return ProgramError::UninitializedAccount,
110                9 => return ProgramError::NotEnoughAccountKeys,
111                10 => return ProgramError::AccountBorrowFailed,
112                11 => return ProgramError::MaxSeedLengthExceeded,
113                12 => return ProgramError::InvalidSeeds,
114                13 => return ProgramError::BorshIoError,
115                14 => return ProgramError::AccountNotRentExempt,
116                15 => return ProgramError::UnsupportedSysvar,
117                16 => return ProgramError::IllegalOwner,
118                17 => return ProgramError::MaxAccountsDataAllocationsExceeded,
119                18 => return ProgramError::InvalidRealloc,
120                19 => return ProgramError::MaxInstructionTraceLengthExceeded,
121                20 => return ProgramError::BuiltinProgramsMustConsumeComputeUnits,
122                21 => return ProgramError::InvalidAccountOwner,
123                22 => return ProgramError::ArithmeticOverflow,
124                23 => return ProgramError::Immutable,
125                24 => return ProgramError::IncorrectAuthority,
126                _ => {}
127            }
128        }
129        ProgramError::Custom(code as u32)
130    }
131}
132
133impl core::fmt::Display for ProgramError {
134    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
135        match self {
136            ProgramError::Custom(code) => write!(f, "Custom({code})"),
137            ProgramError::InvalidArgument => write!(f, "InvalidArgument"),
138            ProgramError::InvalidInstructionData => write!(f, "InvalidInstructionData"),
139            ProgramError::InvalidAccountData => write!(f, "InvalidAccountData"),
140            ProgramError::AccountDataTooSmall => write!(f, "AccountDataTooSmall"),
141            ProgramError::InsufficientFunds => write!(f, "InsufficientFunds"),
142            ProgramError::IncorrectProgramId => write!(f, "IncorrectProgramId"),
143            ProgramError::MissingRequiredSignature => write!(f, "MissingRequiredSignature"),
144            ProgramError::AccountAlreadyInitialized => write!(f, "AccountAlreadyInitialized"),
145            ProgramError::UninitializedAccount => write!(f, "UninitializedAccount"),
146            ProgramError::NotEnoughAccountKeys => write!(f, "NotEnoughAccountKeys"),
147            ProgramError::AccountBorrowFailed => write!(f, "AccountBorrowFailed"),
148            ProgramError::MaxSeedLengthExceeded => write!(f, "MaxSeedLengthExceeded"),
149            ProgramError::InvalidSeeds => write!(f, "InvalidSeeds"),
150            ProgramError::BorshIoError => write!(f, "BorshIoError"),
151            ProgramError::AccountNotRentExempt => write!(f, "AccountNotRentExempt"),
152            ProgramError::UnsupportedSysvar => write!(f, "UnsupportedSysvar"),
153            ProgramError::IllegalOwner => write!(f, "IllegalOwner"),
154            ProgramError::MaxAccountsDataAllocationsExceeded => {
155                write!(f, "MaxAccountsDataAllocationsExceeded")
156            }
157            ProgramError::InvalidRealloc => write!(f, "InvalidRealloc"),
158            ProgramError::MaxInstructionTraceLengthExceeded => {
159                write!(f, "MaxInstructionTraceLengthExceeded")
160            }
161            ProgramError::BuiltinProgramsMustConsumeComputeUnits => {
162                write!(f, "BuiltinProgramsMustConsumeComputeUnits")
163            }
164            ProgramError::InvalidAccountOwner => write!(f, "InvalidAccountOwner"),
165            ProgramError::ArithmeticOverflow => write!(f, "ArithmeticOverflow"),
166            ProgramError::Immutable => write!(f, "Immutable"),
167            ProgramError::IncorrectAuthority => write!(f, "IncorrectAuthority"),
168        }
169    }
170}
171
172// ── Backend conversions ──────────────────────────────────────────────
173
174// The two enums are LAYOUT-IDENTICAL twins: both `#[repr(u64)]` with the
175// same variant set, the same explicit discriminants `0..=25`, and the
176// same single `u32` payload on `Custom`. That makes the glue an identity
177// BY LAYOUT, a transmute, zero instructions, where a 25-arm identity
178// match used to sit in every binary (measured: 880 B of the parity
179// vault's debug .text) and a u64 round-trip cost +3..+8 CU on benched
180// rows. The correspondence is pinned three ways: the const asserts
181// below, the exhaustive `glue_roundtrips_identity_for_every_variant`
182// test, and each enum's own exhaustive u64 golden test.
183const _: () = assert!(
184    core::mem::size_of::<ProgramError>()
185        == core::mem::size_of::<hopper_native::error::ProgramError>()
186);
187const _: () = assert!(
188    core::mem::align_of::<ProgramError>()
189        == core::mem::align_of::<hopper_native::error::ProgramError>()
190);
191
192impl From<hopper_native::error::ProgramError> for ProgramError {
193    #[inline(always)]
194    fn from(e: hopper_native::error::ProgramError) -> Self {
195        // SAFETY: both enums are `#[repr(u64)]` with identical variant
196        // sets, identical explicit discriminants (0..=25) and the same
197        // `Custom(u32)` payload, so every valid bit pattern of one is a
198        // valid bit pattern of the other with the same meaning (RFC 2195
199        // layout). Size/align equality is const-asserted above and the
200        // variant-for-variant identity is exhaustively tested.
201        unsafe { core::mem::transmute::<hopper_native::error::ProgramError, ProgramError>(e) }
202    }
203}
204
205impl From<ProgramError> for hopper_native::error::ProgramError {
206    #[inline(always)]
207    fn from(e: ProgramError) -> Self {
208        // SAFETY: mirror of the impl above; same layout-twin argument.
209        unsafe { core::mem::transmute::<ProgramError, hopper_native::error::ProgramError>(e) }
210    }
211}
212
213// ══════════════════════════════════════════════════════════════════════
214//  Cold error constructors
215// ══════════════════════════════════════════════════════════════════════
216//
217// `#[cold]` + `#[inline(never)]` on error return helpers keeps the error path
218// out of the hot-path instruction cache.
219// Call sites become a single branch + call, keeping the inlined fast path tiny.
220
221impl ProgramError {
222    #[inline(always)]
223    pub fn err_data_too_small<T>() -> Result<T, Self> {
224        Err(ProgramError::AccountDataTooSmall)
225    }
226
227    #[cold]
228    #[inline(never)]
229    pub fn err_invalid_data<T>() -> Result<T, Self> {
230        Err(ProgramError::InvalidAccountData)
231    }
232
233    #[cold]
234    #[inline(never)]
235    pub fn err_missing_signer<T>() -> Result<T, Self> {
236        Err(ProgramError::MissingRequiredSignature)
237    }
238
239    #[inline(always)]
240    pub fn err_immutable<T>() -> Result<T, Self> {
241        Err(ProgramError::Immutable)
242    }
243
244    #[cold]
245    #[inline(never)]
246    pub fn err_not_enough_keys<T>() -> Result<T, Self> {
247        Err(ProgramError::NotEnoughAccountKeys)
248    }
249
250    #[cold]
251    #[inline(never)]
252    pub fn err_borrow_failed<T>() -> Result<T, Self> {
253        Err(ProgramError::AccountBorrowFailed)
254    }
255
256    #[cold]
257    #[inline(never)]
258    pub fn err_overflow<T>() -> Result<T, Self> {
259        Err(ProgramError::ArithmeticOverflow)
260    }
261
262    #[cold]
263    #[inline(never)]
264    pub fn err_invalid_argument<T>() -> Result<T, Self> {
265        Err(ProgramError::InvalidArgument)
266    }
267
268    #[inline(always)]
269    pub fn err_incorrect_program<T>() -> Result<T, Self> {
270        Err(ProgramError::IncorrectProgramId)
271    }
272}
273
274#[cfg(test)]
275mod tag_encoding_tests {
276    use super::*;
277
278    fn all_variants() -> [ProgramError; 29] {
279        [
280            ProgramError::Custom(0),
281            ProgramError::Custom(1),
282            ProgramError::Custom(0xD003),
283            ProgramError::Custom(u32::MAX),
284            ProgramError::InvalidArgument,
285            ProgramError::InvalidInstructionData,
286            ProgramError::InvalidAccountData,
287            ProgramError::AccountDataTooSmall,
288            ProgramError::InsufficientFunds,
289            ProgramError::IncorrectProgramId,
290            ProgramError::MissingRequiredSignature,
291            ProgramError::AccountAlreadyInitialized,
292            ProgramError::UninitializedAccount,
293            ProgramError::NotEnoughAccountKeys,
294            ProgramError::AccountBorrowFailed,
295            ProgramError::MaxSeedLengthExceeded,
296            ProgramError::InvalidSeeds,
297            ProgramError::BorshIoError,
298            ProgramError::AccountNotRentExempt,
299            ProgramError::UnsupportedSysvar,
300            ProgramError::IllegalOwner,
301            ProgramError::MaxAccountsDataAllocationsExceeded,
302            ProgramError::InvalidRealloc,
303            ProgramError::MaxInstructionTraceLengthExceeded,
304            ProgramError::BuiltinProgramsMustConsumeComputeUnits,
305            ProgramError::InvalidAccountOwner,
306            ProgramError::ArithmeticOverflow,
307            ProgramError::Immutable,
308            ProgramError::IncorrectAuthority,
309        ]
310    }
311
312    /// Every variant's u64 code, pinned against the pre-tag-read reference
313    /// table (the old 25-arm match, reproduced verbatim). The reference
314    /// match is EXHAUSTIVE on purpose: adding a variant without updating
315    /// this test, and giving it the next sequential discriminant, must
316    /// not compile.
317    #[test]
318    fn u64_conversion_matches_reference_for_every_variant() {
319        fn reference(err: &ProgramError) -> u64 {
320            match err {
321                ProgramError::Custom(0) => CUSTOM_ZERO,
322                ProgramError::Custom(code) => *code as u64,
323                ProgramError::InvalidArgument => to_builtin(0),
324                ProgramError::InvalidInstructionData => to_builtin(1),
325                ProgramError::InvalidAccountData => to_builtin(2),
326                ProgramError::AccountDataTooSmall => to_builtin(3),
327                ProgramError::InsufficientFunds => to_builtin(4),
328                ProgramError::IncorrectProgramId => to_builtin(5),
329                ProgramError::MissingRequiredSignature => to_builtin(6),
330                ProgramError::AccountAlreadyInitialized => to_builtin(7),
331                ProgramError::UninitializedAccount => to_builtin(8),
332                ProgramError::NotEnoughAccountKeys => to_builtin(9),
333                ProgramError::AccountBorrowFailed => to_builtin(10),
334                ProgramError::MaxSeedLengthExceeded => to_builtin(11),
335                ProgramError::InvalidSeeds => to_builtin(12),
336                ProgramError::BorshIoError => to_builtin(13),
337                ProgramError::AccountNotRentExempt => to_builtin(14),
338                ProgramError::UnsupportedSysvar => to_builtin(15),
339                ProgramError::IllegalOwner => to_builtin(16),
340                ProgramError::MaxAccountsDataAllocationsExceeded => to_builtin(17),
341                ProgramError::InvalidRealloc => to_builtin(18),
342                ProgramError::MaxInstructionTraceLengthExceeded => to_builtin(19),
343                ProgramError::BuiltinProgramsMustConsumeComputeUnits => to_builtin(20),
344                ProgramError::InvalidAccountOwner => to_builtin(21),
345                ProgramError::ArithmeticOverflow => to_builtin(22),
346                ProgramError::Immutable => to_builtin(23),
347                ProgramError::IncorrectAuthority => to_builtin(24),
348            }
349        }
350        for err in all_variants() {
351            let want = reference(&err);
352            let got: u64 = err.clone().into();
353            assert_eq!(got, want, "u64 code diverged for {err:?}");
354            assert_eq!(ProgramError::from(want), err, "roundtrip for {err:?}");
355        }
356    }
357
358    /// The native<->runtime glue is an IDENTITY on every variant in both
359    /// directions (it round-trips through the shared u64 wire code).
360    #[test]
361    fn glue_roundtrips_identity_for_every_variant() {
362        for err in all_variants() {
363            let native: hopper_native::error::ProgramError = err.clone().into();
364            assert_eq!(
365                u64::from(native.clone()),
366                u64::from(err.clone()),
367                "wire code diverged crossing into native for {err:?}"
368            );
369            let back: ProgramError = native.into();
370            assert_eq!(back, err, "native->runtime glue not identity for {err:?}");
371        }
372    }
373}