Skip to main content

hopper_native/
entrypoint.rs

1//! Program entrypoint ownership for Hopper Native.
2//!
3//! This file is the only raw program-entry boundary owner in Hopper Native.
4//! Loader input parsing lives in [`crate::raw_input`], while the public macros
5//! below own the raw `entrypoint(input: *mut u8)` boundary and delegate into
6//! Hopper callbacks.
7
8use core::mem::MaybeUninit;
9
10use crate::account_view::AccountView;
11use crate::address::Address;
12use crate::error::ProgramError;
13
14/// Convert a handler's `ProgramError` into the Solana runtime's u64 return code.
15///
16/// Outlined `#[cold] #[inline(never)]` so an entrypoint's success tail lowers to
17/// a bare `return SUCCESS` and the `ProgramError -> u64` mapping (the 25-arm
18/// `From<ProgramError> for u64` match) is never inlined into the hot frame,
19/// where it would add code size and stack traffic that every successful
20/// invocation pays for. This mirrors Pinocchio's cold error outline.
21///
22/// The conversion is exactly `Into::<u64>::into(e)`, byte-for-byte identical to
23/// the previous inline `error.into()`, so the runtime error codes are unchanged.
24#[cold]
25#[inline(never)]
26pub fn err_to_u64(e: ProgramError) -> u64 {
27    e.into()
28}
29
30/// Process the BPF entrypoint input.
31///
32/// This is the function called by the canonical Hopper Native entrypoint macro's
33/// generated entrypoint.
34///
35/// # Safety
36///
37/// `input` must be the raw pointer provided by the Solana runtime.
38#[inline(always)]
39pub unsafe fn process_entrypoint<const MAX: usize>(
40    input: *mut u8,
41    process_instruction: for<'info> fn(
42        &'info Address,
43        &'info [AccountView<'info>],
44        &'info [u8],
45    ) -> crate::ProgramResult,
46) -> u64 {
47    const UNINIT: MaybeUninit<AccountView<'static>> = MaybeUninit::uninit();
48    let mut accounts = [UNINIT; 254]; // MAX_TX_ACCOUNTS
49
50    let (program_id, count, instruction_data) =
51        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
52        unsafe { crate::raw_input::deserialize_accounts::<254>(input, &mut accounts) };
53
54    // Respect MAX: only pass up to MAX accounts to the callback.
55    let effective_count = count.min(MAX);
56    // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
57    let account_slice = unsafe {
58        core::slice::from_raw_parts(accounts.as_ptr() as *const AccountView<'_>, effective_count)
59    };
60
61    match process_instruction(program_id, account_slice, instruction_data) {
62        Ok(()) => crate::SUCCESS,
63        Err(error) => err_to_u64(error),
64    }
65}
66
67/// Declare the canonical Hopper Native program entrypoint.
68///
69/// Generates the `extern "C" fn entrypoint` that the Solana runtime calls.
70/// `program_entrypoint!` remains available as a backward-compatible alias.
71///
72/// # Usage
73///
74/// ```ignore
75/// use hopper_native::hopper_program_entrypoint;
76///
77/// hopper_program_entrypoint!(process_instruction);
78///
79/// pub fn process_instruction(
80///     program_id: &Address,
81///     accounts: &[AccountView],
82///     instruction_data: &[u8],
83/// ) -> ProgramResult {
84///     Ok(())
85/// }
86/// ```
87#[macro_export]
88macro_rules! hopper_program_entrypoint {
89    ( $process_instruction:expr ) => {
90        $crate::hopper_program_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
91    };
92    ( $process_instruction:expr, $maximum:expr ) => {
93        /// # Safety
94        ///
95        /// Called by the Solana runtime; `input` is a valid BPF input buffer.
96        #[no_mangle]
97        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
98            const UNINIT: core::mem::MaybeUninit<$crate::AccountView<'static>> =
99                core::mem::MaybeUninit::<$crate::AccountView<'static>>::uninit();
100            let mut accounts = [UNINIT; $maximum];
101
102            // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
103            let (program_id, count, instruction_data) = unsafe {
104                $crate::raw_input::deserialize_accounts::<$maximum>(input, &mut accounts)
105            };
106
107            match $process_instruction(
108                program_id,
109                // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
110                unsafe {
111                    core::slice::from_raw_parts(
112                        accounts.as_ptr() as *const $crate::AccountView<'_>,
113                        count,
114                    )
115                },
116                instruction_data,
117            ) {
118                Ok(()) => $crate::SUCCESS,
119                Err(error) => $crate::entrypoint::err_to_u64(error),
120            }
121        }
122    };
123}
124
125/// Backward-compatible alias for `hopper_program_entrypoint!`.
126#[macro_export]
127macro_rules! program_entrypoint {
128    ( $process_instruction:expr ) => {
129        $crate::hopper_program_entrypoint!($process_instruction);
130    };
131    ( $process_instruction:expr, $maximum:expr ) => {
132        $crate::hopper_program_entrypoint!($process_instruction, $maximum);
133    };
134}
135
136/// Declare a fast two-argument Hopper Native program entrypoint.
137///
138/// Uses the SVM's second entrypoint register (`r2`), which carries a
139/// direct pointer to instruction data under [SIMD-0321], letting the
140/// entrypoint skip locating the instruction tail. Measured honestly
141/// (2026-07-21, post the 2026-07-07 fused single-pass walk): the fused
142/// scanning entrypoint already hops records by their `data_len` headers
143/// without touching account data, so on programs whose accounts fit the
144/// declared maximum the r2 path is CU-neutral (+/- 2 CU in controlled
145/// A/Bs) and costs ~368 bytes for carrying both paths. The historical
146/// "~30-40 CU" figure described the pre-fusion two-pass scanner. The r2
147/// path earns its keep as the base of the SIMD-0449 O(1) account-pointer
148/// table, and for instructions whose transaction carries many more
149/// accounts than the program materializes.
150///
151/// # Feature gating (`simd-0321`)
152///
153/// SIMD-0321 is **activated on all three public clusters** (feature gate
154/// `5xXZc66h4UdB6Yq7FzdBxBiRAFMMScMLwHxk2QZDaNZL`; mainnet-beta at slot
155/// 410,400,000, 2026-04-01). Current agave sets `r2` unconditionally, so
156/// builds may enable the feature for any cluster target; on a runtime
157/// that ever leaves `r2` zero, the null-check below still falls back to
158/// the scanning parse.
159///
160/// - **Default (feature off):** this macro expands to the standard
161///   scanning entrypoint ([`hopper_program_entrypoint!`]). Identical
162///   semantics, sound on every cluster today, and source-compatible:
163///   when the gate activates, rebuild with the feature to claim the
164///   CU savings.
165/// - **`simd-0321` enabled:** the macro expands to the two-argument
166///   entrypoint. As defense in depth it null-checks `r2` and falls
167///   back to the scanning parse when the register is zero (current
168///   SBPF VMs zero-initialize unused argument registers), so a binary
169///   built with the feature degrades to the slow path instead of
170///   reading garbage if it lands on a cluster without the activation.
171///
172/// `hopper doctor` / `hopper deploy` can check the feature-gate account
173/// on the target cluster before a `simd-0321` build ships.
174///
175/// [SIMD-0321]: https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0321-vm-r2-instruction-data-pointer.md
176///
177/// # Usage
178///
179/// ```ignore
180/// use hopper_native::hopper_fast_entrypoint;
181///
182/// hopper_fast_entrypoint!(process_instruction, 3);
183///
184/// pub fn process_instruction(
185///     program_id: &Address,
186///     accounts: &[AccountView],
187///     instruction_data: &[u8],
188/// ) -> ProgramResult {
189///     Ok(())
190/// }
191/// ```
192#[cfg(feature = "simd-0321")]
193#[macro_export]
194macro_rules! hopper_fast_entrypoint {
195    ( $process_instruction:expr ) => {
196        $crate::hopper_fast_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
197    };
198    ( $process_instruction:expr, $maximum:expr ) => {
199        /// # Safety
200        ///
201        /// Called by the Solana runtime; `input` is a valid BPF input buffer.
202        /// When SIMD-0321 is active, `ix_data` points to the instruction data
203        /// with its u64 length stored at offset -8; when it is not active the
204        /// register is zero and the scanning fallback below is taken.
205        #[no_mangle]
206        pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data: *const u8) -> u64 {
207            const UNINIT: core::mem::MaybeUninit<$crate::AccountView<'static>> =
208                core::mem::MaybeUninit::<$crate::AccountView<'static>>::uninit();
209            let mut accounts = [UNINIT; $maximum];
210
211            let (program_id, count, instruction_data) = if ix_data.is_null() {
212                // SIMD-0321 not active on this cluster: r2 is zero. Fall back
213                // to the full scanning parse so the program stays correct.
214                // SAFETY: `input` is the loader-provided input buffer; the
215                // scanning parser owns all bounds/duplicate-marker checks.
216                unsafe { $crate::raw_input::deserialize_accounts::<$maximum>(input, &mut accounts) }
217            } else {
218                // Instruction data length is the u64 immediately before the
219                // data pointer (per SIMD-0321's serialization contract).
220                // SAFETY: SIMD-0321 ix_data points at instruction-data bytes
221                // with u64 length prefix at `ix_data - 8`.
222                let ix_len =
223                    unsafe { core::ptr::read_unaligned(ix_data.sub(8) as *const u64) as usize };
224                let instruction_data: &'static [u8] =
225                    unsafe { core::slice::from_raw_parts(ix_data, ix_len) };
226
227                // SAFETY: program id trails the instruction data per the
228                // loader serialization layout; `Address` is a transparent
229                // `[u8; 32]`, so a reference into the buffer is valid at any
230                // offset and lives as long as the invocation.
231                let program_id: &'static $crate::Address =
232                    unsafe { &*(ix_data.add(ix_len) as *const $crate::Address) };
233
234                if $crate::raw_input::SIMD_0449_TABLE_ENABLED {
235                    // SIMD-0449 build: consume the runtime's appended
236                    // pre-deduplicated account-pointer table, O(1)
237                    // resolution plus one pointer copy per account. The
238                    // gate is a `const`, so the untaken branch folds
239                    // away entirely.
240                    // SAFETY: the `simd-0449` feature asserts the SIMD
241                    // is active on the target cluster (table present);
242                    // `instruction_data`/`program_id` were derived from
243                    // the SIMD-0321 r2 register above.
244                    unsafe {
245                        $crate::raw_input::deserialize_accounts_0449_into::<$maximum>(
246                            input,
247                            &mut accounts,
248                            instruction_data,
249                            program_id,
250                        )
251                    }
252                } else {
253                    // SAFETY: `input` is the loader input buffer; account-slot
254                    // framing is validated by `deserialize_accounts_fast`.
255                    unsafe {
256                        $crate::raw_input::deserialize_accounts_fast::<$maximum>(
257                            input,
258                            &mut accounts,
259                            instruction_data,
260                            program_id,
261                        )
262                    }
263                }
264            };
265
266            match $process_instruction(
267                program_id,
268                // SAFETY: the first `count` slots were initialized by the
269                // parser above; `AccountView` is repr(C) over the slot data.
270                unsafe {
271                    core::slice::from_raw_parts(
272                        accounts.as_ptr() as *const $crate::AccountView<'_>,
273                        count,
274                    )
275                },
276                instruction_data,
277            ) {
278                Ok(()) => $crate::SUCCESS,
279                Err(error) => $crate::entrypoint::err_to_u64(error),
280            }
281        }
282    };
283}
284
285/// Without the `simd-0321` feature the "fast" entrypoint is an alias for
286/// the standard scanning entrypoint. The SIMD-0321 gate is live on every
287/// public cluster (mainnet-beta 2026-04-01); the r2 form is sound to build
288/// and stays opt-in only because it measured CU-neutral against the fused
289/// scanning walk for ~368 bytes of extra `.text`. The two-argument r2 form
290/// also null-checks the register and falls back to scanning, so it is safe
291/// even where the gate is somehow inactive.
292#[cfg(not(feature = "simd-0321"))]
293#[macro_export]
294macro_rules! hopper_fast_entrypoint {
295    ( $process_instruction:expr ) => {
296        $crate::hopper_program_entrypoint!($process_instruction);
297    };
298    ( $process_instruction:expr, $maximum:expr ) => {
299        $crate::hopper_program_entrypoint!($process_instruction, $maximum);
300    };
301}
302
303/// Backward-compatible alias for `hopper_fast_entrypoint!`.
304#[macro_export]
305macro_rules! fast_entrypoint {
306    ( $process_instruction:expr ) => {
307        $crate::hopper_fast_entrypoint!($process_instruction);
308    };
309    ( $process_instruction:expr, $maximum:expr ) => {
310        $crate::hopper_fast_entrypoint!($process_instruction, $maximum);
311    };
312}
313
314/// Declare the canonical lazy program entrypoint that defers account parsing.
315#[macro_export]
316macro_rules! hopper_lazy_entrypoint {
317    ( $process:expr ) => {
318        /// # Safety
319        ///
320        /// Called by the Solana runtime; `input` is a valid BPF input buffer.
321        #[no_mangle]
322        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
323            // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
324            let mut ctx = unsafe { $crate::lazy::lazy_deserialize(input) };
325            match $process(&mut ctx) {
326                Ok(()) => $crate::SUCCESS,
327                Err(error) => $crate::entrypoint::err_to_u64(error),
328            }
329        }
330    };
331}
332
333/// Backward-compatible alias for `hopper_lazy_entrypoint!`.
334#[macro_export]
335macro_rules! lazy_entrypoint {
336    ( $process:expr ) => {
337        $crate::hopper_lazy_entrypoint!($process);
338    };
339}
340
341/// Set up a no-op global allocator that aborts on allocation.
342///
343/// Useful for `no_std` programs that must not allocate. Any attempt to
344/// allocate will immediately abort the program rather than returning a
345/// null pointer (which violates the `GlobalAlloc` contract).
346#[macro_export]
347macro_rules! no_allocator {
348    () => {
349        #[cfg(target_os = "solana")]
350        mod __hopper_allocator {
351            struct NoAlloc;
352
353            unsafe impl core::alloc::GlobalAlloc for NoAlloc {
354                unsafe fn alloc(&self, _layout: core::alloc::Layout) -> *mut u8 {
355                    // Abort: returning null_mut violates the GlobalAlloc
356                    // contract and causes UB. Abort is the correct response
357                    // for a no-alloc program.
358                    core::arch::asm!("mov r0, 1", "exit", options(noreturn));
359                }
360                unsafe fn dealloc(&self, _ptr: *mut u8, _layout: core::alloc::Layout) {}
361            }
362
363            #[global_allocator]
364            static ALLOCATOR: NoAlloc = NoAlloc;
365        }
366    };
367}
368
369/// Canonical Solana heap region start address (`0x3_0000_0000`).
370pub const HEAP_START_ADDRESS: usize = 0x3_0000_0000;
371
372/// Default Solana heap region length (32 KiB).
373pub const HEAP_LENGTH: usize = 32 * 1024;
374
375/// Bytes of the heap's BOTTOM reserved as Hopper runtime scratch, starting
376/// right after the [`BumpAllocator`] cursor word: the byte range
377/// `[HEAP_START + 8, HEAP_START + 8 + HEAP_RUNTIME_RESERVED)`.
378///
379/// Why this exists: deployed SBF programs cannot carry writable sections,
380/// the loader rejects `.bss`/`.data` outright (`WritableSectionNotSupported`),
381/// so a `static mut` is not merely costly, it makes the program FAIL TO
382/// LOAD. The only writable, per-invocation, zero-initialized memory a
383/// program owns is this VM heap region. Hopper's instruction-scoped
384/// runtime state (today: the lamport gate in
385/// `hopper_runtime::write_policy`) therefore lives at the heap bottom,
386/// which works precisely because the VM zeroes the region on every
387/// invocation and every such structure is valid all-zero.
388///
389/// The [`BumpAllocator`] treats this range as out of bounds (its floor sits
390/// above it), so `alloc` can never hand it out. Programs that install a
391/// custom allocator over the heap must honor the same reservation if they
392/// link any hopper-runtime feature that uses it.
393pub const HEAP_RUNTIME_RESERVED: usize = 20 * 1024;
394
395/// A bump allocator over the SVM heap region.
396///
397/// This is the same single-pass, never-frees design the Solana SDK and
398/// Pinocchio use: the first word of the heap stores the current cursor,
399/// allocations bump it downward from the top of the region, and
400/// `dealloc` is a no-op. It is the right allocator for the cold paths of
401/// a program that wants `alloc` (e.g. a `Vec` while building a CPI) while
402/// keeping the hot path zero-allocation. For programs that must never
403/// allocate, prefer [`no_allocator!`] so any stray allocation traps.
404///
405/// Install it with `default_allocator!`.
406pub struct BumpAllocator {
407    /// Heap region start address.
408    pub start: usize,
409    /// Heap region length in bytes.
410    pub len: usize,
411}
412
413// SAFETY: Solana program execution is single-threaded, so the cursor word
414// at `start` is never accessed concurrently.
415unsafe impl core::alloc::GlobalAlloc for BumpAllocator {
416    #[inline]
417    unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 {
418        // The cursor is stored in the first word of the heap region.
419        let pos_ptr = self.start as *mut usize;
420        // SAFETY: `pos_ptr` is the reserved cursor word; single-threaded.
421        let mut pos = unsafe { *pos_ptr };
422        if pos == 0 {
423            // First allocation: start at the top of the region.
424            pos = self.start + self.len;
425        }
426        pos = pos.saturating_sub(layout.size());
427        pos &= !(layout.align().wrapping_sub(1));
428        // Floor: the cursor word plus the Hopper runtime scratch region
429        // ([`HEAP_RUNTIME_RESERVED`], heap bottom). Bumping into either
430        // would corrupt the allocator state or the instruction-scoped
431        // runtime state (e.g. the lamport gate), so exhaust instead.
432        if pos < self.start + core::mem::size_of::<usize>() + HEAP_RUNTIME_RESERVED {
433            return core::ptr::null_mut();
434        }
435        // SAFETY: `pos_ptr` is the reserved cursor word; single-threaded.
436        unsafe { *pos_ptr = pos };
437        pos as *mut u8
438    }
439
440    #[inline]
441    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: core::alloc::Layout) {
442        // Bump allocator: memory is reclaimed when the instruction ends.
443    }
444}
445
446/// Install the default bump allocator over the SVM heap region.
447///
448/// Opt-in counterpart to [`no_allocator!`]: use this when a program needs
449/// `alloc` (e.g. heap `Vec`/`String` on a cold path) while keeping the
450/// zero-copy hot path allocation-free. Never frees within an instruction;
451/// the whole heap is reclaimed when the instruction returns.
452#[macro_export]
453macro_rules! default_allocator {
454    () => {
455        #[cfg(target_os = "solana")]
456        #[global_allocator]
457        static ALLOCATOR: $crate::BumpAllocator = $crate::BumpAllocator {
458            start: $crate::HEAP_START_ADDRESS,
459            len: $crate::HEAP_LENGTH,
460        };
461    };
462}
463
464/// Default no_std panic handler that aborts immediately.
465///
466/// On BPF, uses inline assembly to return error code 1 (aborts the
467/// program). This is cheaper than `spin_loop()` which would burn CU
468/// until the runtime kills the program.
469#[macro_export]
470macro_rules! nostd_panic_handler {
471    () => {
472        #[cfg(target_os = "solana")]
473        #[panic_handler]
474        fn panic(_info: &core::panic::PanicInfo) -> ! {
475            // Abort immediately, spin_loop() would burn CU indefinitely.
476            // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
477            unsafe { core::arch::asm!("mov r0, 1", "exit", options(noreturn)) };
478        }
479    };
480}
481
482#[cfg(test)]
483mod entrypoint_tail_tests {
484    extern crate std;
485
486    use std::vec;
487    use std::vec::Vec;
488
489    use super::*;
490
491    /// Serialize a zero-account loader frame: `u64` account count (0), then the
492    /// `u64` ix-data length prefix, the ix-data bytes, and the 32-byte program
493    /// id. Returns an 8-aligned `u64` backing (matching `MM_INPUT_START`).
494    fn build_zero_account_frame(ix_data: &[u8], program_id: [u8; 32]) -> Vec<u64> {
495        let mut buf: Vec<u8> = Vec::new();
496        buf.extend_from_slice(&0u64.to_le_bytes()); // account_count = 0
497        buf.extend_from_slice(&(ix_data.len() as u64).to_le_bytes());
498        buf.extend_from_slice(ix_data);
499        buf.extend_from_slice(&program_id);
500        let mut words = vec![0u64; buf.len().div_ceil(8)];
501        // SAFETY: `words` has at least `buf.len()` bytes of capacity and the
502        // regions do not overlap.
503        unsafe {
504            core::ptr::copy_nonoverlapping(buf.as_ptr(), words.as_mut_ptr() as *mut u8, buf.len());
505        }
506        words
507    }
508
509    fn ok_handler<'a>(
510        _: &'a Address,
511        _: &'a [AccountView<'a>],
512        _: &'a [u8],
513    ) -> crate::ProgramResult {
514        Ok(())
515    }
516
517    fn custom_err_handler<'a>(
518        _: &'a Address,
519        _: &'a [AccountView<'a>],
520        _: &'a [u8],
521    ) -> crate::ProgramResult {
522        Err(ProgramError::Custom(4242))
523    }
524
525    fn builtin_err_handler<'a>(
526        _: &'a Address,
527        _: &'a [AccountView<'a>],
528        _: &'a [u8],
529    ) -> crate::ProgramResult {
530        Err(ProgramError::MissingRequiredSignature)
531    }
532
533    #[test]
534    fn ok_returns_bare_success_zero() {
535        let mut frame = build_zero_account_frame(&[1, 2, 3], [7u8; 32]);
536        // SAFETY: `frame` is a well-formed, 8-aligned zero-account loader frame.
537        let code = unsafe { process_entrypoint::<4>(frame.as_mut_ptr() as *mut u8, ok_handler) };
538        assert_eq!(code, 0);
539        assert_eq!(code, crate::SUCCESS);
540    }
541
542    #[test]
543    fn custom_err_maps_through_cold_outline_unchanged() {
544        let mut frame = build_zero_account_frame(&[], [0u8; 32]);
545        // SAFETY: well-formed, 8-aligned zero-account loader frame.
546        let code =
547            unsafe { process_entrypoint::<4>(frame.as_mut_ptr() as *mut u8, custom_err_handler) };
548        // Cold outline must equal the direct `From<ProgramError> for u64` mapping.
549        assert_eq!(code, u64::from(ProgramError::Custom(4242)));
550        assert_eq!(code, err_to_u64(ProgramError::Custom(4242)));
551        assert_eq!(code, 4242);
552    }
553
554    #[test]
555    fn builtin_err_maps_through_cold_outline_unchanged() {
556        let mut frame = build_zero_account_frame(&[], [0u8; 32]);
557        // SAFETY: well-formed, 8-aligned zero-account loader frame.
558        let code =
559            unsafe { process_entrypoint::<4>(frame.as_mut_ptr() as *mut u8, builtin_err_handler) };
560        assert_eq!(code, u64::from(ProgramError::MissingRequiredSignature));
561        assert_eq!(code, err_to_u64(ProgramError::MissingRequiredSignature));
562    }
563
564    /// The cold outline is a byte-for-byte alias of `From<ProgramError> for u64`
565    /// across the full variant space (custom-zero, custom, and builtins).
566    #[test]
567    fn err_to_u64_matches_from_impl_for_all_variants() {
568        let cases = [
569            ProgramError::Custom(0),
570            ProgramError::Custom(1),
571            ProgramError::Custom(u32::MAX),
572            ProgramError::InvalidArgument,
573            ProgramError::MissingRequiredSignature,
574            ProgramError::AccountBorrowFailed,
575            ProgramError::ArithmeticOverflow,
576            ProgramError::IncorrectAuthority,
577        ];
578        for e in cases {
579            assert_eq!(err_to_u64(e.clone()), u64::from(e));
580        }
581    }
582}