Skip to main content

rialo_s_program_entrypoint/
lib.rs

1//! The Rust-based program entrypoint supported by the latest loader.
2#![allow(unsafe_code)]
3#![allow(clippy::ptr_as_ptr)]
4
5extern crate alloc;
6
7/// Not part of the public API. Used by macros.
8#[cfg(target_arch = "riscv64")]
9#[doc(hidden)]
10pub mod __private {
11    pub use polkavm_derive::{self, default_abi, polkavm_export};
12}
13use alloc::vec::Vec;
14use std::{
15    cell::RefCell,
16    mem::{size_of, MaybeUninit},
17    rc::Rc,
18    slice::{from_raw_parts, from_raw_parts_mut},
19};
20
21use rialo_s_account_info::AccountInfo;
22use rialo_s_pubkey::Pubkey;
23// need to re-export msg for custom_heap_default macro
24pub use {
25    crate::allocator::BumpAllocator, rialo_s_account_info::MAX_PERMITTED_DATA_INCREASE,
26    rialo_s_msg::msg as __msg, rialo_s_program_error::ProgramResult,
27};
28
29pub mod allocator;
30
31/// User implemented function to process an instruction
32///
33/// program_id: Program ID of the currently executing program accounts: Accounts
34/// passed as part of the instruction instruction_data: Instruction data
35pub type ProcessInstruction =
36    fn(program_id: &Pubkey, accounts: &[AccountInfo<'_>], instruction_data: &[u8]) -> ProgramResult;
37
38/// Programs indicate success with a return value of 0
39pub const SUCCESS: u64 = 0;
40
41/// Length of the heap memory region used for program heap.
42pub const HEAP_LENGTH: usize = 32 * 1024;
43
44/// Value used to indicate that a serialized account is not a duplicate
45pub const NON_DUP_MARKER: u8 = u8::MAX;
46
47/// Declare the program entrypoint and set up global handlers.
48///
49/// This macro emits the common boilerplate necessary to begin program
50/// execution, calling a provided function to process the program instruction
51/// supplied by the runtime, and reporting its result to the runtime.
52///
53/// It also sets up a [global allocator] and [panic handler], using the
54/// [`custom_heap_default`] and [`custom_panic_default`] macros.
55///
56/// [`custom_heap_default`]: crate::custom_heap_default
57/// [`custom_panic_default`]: crate::custom_panic_default
58///
59/// [global allocator]: https://doc.rust-lang.org/stable/std/alloc/trait.GlobalAlloc.html
60/// [panic handler]: https://doc.rust-lang.org/nomicon/panic-handler.html
61///
62/// The argument is the name of a function with this type signature:
63///
64/// ```ignore
65/// fn process_instruction(
66///     program_id: &Pubkey,      // Public key of the account the program was loaded into
67///     accounts: &[AccountInfo], // All accounts required to process the instruction
68///     instruction_data: &[u8],  // Serialized instruction-specific data
69/// ) -> ProgramResult;
70/// ```
71///
72/// # Cargo features
73///
74/// This macro emits symbols and definitions that may only be defined once
75/// globally. As such, if linked to other Rust crates it will cause compiler
76/// errors. To avoid this, it is common for Solana programs to define an
77/// optional [Cargo feature] called `no-entrypoint`, and use it to conditionally
78/// disable the `entrypoint` macro invocation, as well as the
79/// `process_instruction` function. See a typical pattern for this in the
80/// example below.
81///
82/// [Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
83///
84/// The code emitted by this macro can be customized by adding cargo features
85/// _to your own crate_ (the one that calls this macro) and enabling them:
86///
87/// - If the `custom-heap` feature is defined then the macro will not set up the
88///   global allocator, allowing `entrypoint` to be used with your own
89///   allocator. See documentation for the [`custom_heap_default`] macro for
90///   details of customizing the global allocator.
91///
92/// - If the `custom-panic` feature is defined then the macro will not define a
93///   panic handler, allowing `entrypoint` to be used with your own panic
94///   handler. See documentation for the [`custom_panic_default`] macro for
95///   details of customizing the panic handler.
96///
97/// # Examples
98///
99/// Defining an entrypoint and making it conditional on the `no-entrypoint`
100/// feature. Although the `entrypoint` module is written inline in this example,
101/// it is common to put it into its own file.
102///
103/// ```no_run
104/// #[cfg(not(feature = "no-entrypoint"))]
105/// pub mod entrypoint {
106///
107///     use rialo_s_account_info::AccountInfo;
108///     use rialo_s_program_entrypoint::entrypoint;
109///     use rialo_s_program_entrypoint::ProgramResult;
110///     use rialo_s_msg::msg;
111///     use rialo_s_pubkey::Pubkey;
112///
113///     entrypoint!(process_instruction);
114///
115///     pub fn process_instruction(
116///         program_id: &Pubkey,
117///         accounts: &[AccountInfo],
118///         instruction_data: &[u8],
119///     ) -> ProgramResult {
120///         msg!("Hello world");
121///
122///         Ok(())
123///     }
124///
125/// }
126/// ```
127#[cfg(not(target_arch = "riscv64"))]
128#[macro_export]
129macro_rules! entrypoint {
130    ($process_instruction:ident) => {
131        /// # Safety
132        #[no_mangle]
133        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
134            let (program_id, accounts, instruction_data) = unsafe { $crate::deserialize(input) };
135            match $process_instruction(program_id, &accounts, instruction_data) {
136                Ok(()) => $crate::SUCCESS,
137                Err(error) => error.into(),
138            }
139        }
140        $crate::custom_heap_default!();
141        $crate::custom_panic_default!();
142    };
143}
144
145#[cfg(target_arch = "riscv64")]
146#[macro_export]
147macro_rules! entrypoint {
148    ($process_instruction:expr) => {
149        #[$crate::__private::polkavm_export(abi = $crate::__private::polkavm_derive::default_abi)]
150        extern "C" fn entrypoint(addr: u32, _len: u32, heap_base: u32, heap_size: u32) -> u64 {
151            // Record the loader-supplied heap base and size before the allocator is first used.
152            $crate::allocator::set_heap_base(heap_base);
153            $crate::allocator::set_heap_limit(heap_size);
154            let (program_id, accounts, instruction_data) =
155                unsafe { $crate::deserialize(addr as usize as *mut u8) };
156
157            match $process_instruction(program_id, &accounts, instruction_data) {
158                Ok(()) => $crate::SUCCESS,
159                Err(error) => error.into(),
160            }
161        }
162        $crate::custom_heap_default!();
163        $crate::custom_panic_default!();
164    };
165}
166
167/// Declare the program entrypoint and set up global handlers.
168///
169/// This is similar to the `entrypoint!` macro, except that it does not perform
170/// any dynamic allocations, and instead writes the input accounts into a pre-
171/// allocated array.
172///
173/// This version reduces compute unit usage by 20-30 compute units per unique
174/// account in the instruction. It may become the default option in a future
175/// release.
176///
177/// For more information about how the program entrypoint behaves and what it
178/// does, please see the documentation for [`entrypoint!`].
179///
180/// NOTE: This entrypoint has a hard-coded limit of 64 input accounts.
181#[cfg(not(target_arch = "riscv64"))]
182#[macro_export]
183macro_rules! entrypoint_no_alloc {
184    ($process_instruction:ident) => {
185        /// # Safety
186        #[no_mangle]
187        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
188            use std::mem::MaybeUninit;
189            // Clippy complains about this because a `const` with interior
190            // mutability `RefCell` should use `static` instead to make it
191            // clear that it can change.
192            // In our case, however, we want to create an array of `AccountInfo`s,
193            // and the only way to do it is through a `const` expression, and
194            // we don't expect to mutate the internals of this `const` type.
195            #[allow(clippy::declare_interior_mutable_const)]
196            const UNINIT_ACCOUNT_INFO: MaybeUninit<AccountInfo> =
197                MaybeUninit::<AccountInfo>::uninit();
198            const MAX_ACCOUNT_INFOS: usize = 64;
199            let mut accounts = [UNINIT_ACCOUNT_INFO; MAX_ACCOUNT_INFOS];
200            let (program_id, num_accounts, instruction_data) =
201                unsafe { $crate::deserialize_into(input, &mut accounts) };
202            // Use `slice_assume_init_ref` once it's stabilized
203            let accounts = &*(&accounts[..num_accounts] as *const [MaybeUninit<AccountInfo<'_>>]
204                as *const [AccountInfo<'_>]);
205
206            #[inline(never)]
207            fn call_program(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> u64 {
208                match $process_instruction(program_id, accounts, data) {
209                    Ok(()) => $crate::SUCCESS,
210                    Err(error) => error.into(),
211                }
212            }
213
214            call_program(&program_id, accounts, &instruction_data)
215        }
216        $crate::custom_heap_default!();
217        $crate::custom_panic_default!();
218    };
219}
220
221#[cfg(target_arch = "riscv64")]
222#[macro_export]
223macro_rules! entrypoint_no_alloc {
224    ($process_instruction:expr) => {
225        #[$crate::__private::polkavm_export(abi = $crate::__private::polkavm_derive::default_abi)]
226        pub unsafe extern "C" fn entrypoint(
227            addr: u32,
228            len: u32,
229            heap_base: u32,
230            heap_size: u32,
231        ) -> u64 {
232            use std::mem::MaybeUninit;
233            // Record the loader-supplied heap base and size in case any allocation occurs (e.g.
234            // panic formatting); the no-alloc entrypoint still installs the default heap allocator.
235            $crate::allocator::set_heap_base(heap_base);
236            $crate::allocator::set_heap_limit(heap_size);
237            // Clippy complains about this because a `const` with interior
238            // mutability `RefCell` should use `static` instead to make it
239            // clear that it can change.
240            // In our case, however, we want to create an array of `AccountInfo`s,
241            // and the only way to do it is through a `const` expression, and
242            // we don't expect to mutate the internals of this `const` type.
243            #[allow(clippy::declare_interior_mutable_const)]
244            const UNINIT_ACCOUNT_INFO: MaybeUninit<AccountInfo> =
245                MaybeUninit::<AccountInfo>::uninit();
246            const MAX_ACCOUNT_INFOS: usize = 64;
247            let mut accounts = [UNINIT_ACCOUNT_INFO; MAX_ACCOUNT_INFOS];
248            let input = addr as usize as *mut u8;
249            let (program_id, num_accounts, instruction_data) =
250                unsafe { $crate::deserialize_into(input, &mut accounts) };
251            // Use `slice_assume_init_ref` once it's stabilized
252            let accounts = unsafe {
253                &*(&accounts[..num_accounts] as *const [MaybeUninit<AccountInfo<'_>>]
254                    as *const [AccountInfo<'_>])
255            };
256
257            #[inline(never)]
258            fn call_program(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> u64 {
259                match $process_instruction(program_id, accounts, data) {
260                    Ok(()) => $crate::SUCCESS,
261                    Err(error) => error.into(),
262                }
263            }
264
265            call_program(&program_id, accounts, &instruction_data)
266        }
267        $crate::custom_heap_default!();
268        $crate::custom_panic_default!();
269    };
270}
271
272/// Define the default global allocator.
273///
274/// The default global allocator is enabled only if the calling crate has not
275/// disabled it using [Cargo features] as described below. It is only defined
276/// for on-chain targets.
277///
278/// [Cargo features]: https://doc.rust-lang.org/cargo/reference/features.html
279///
280/// # Cargo features
281///
282/// A crate that calls this macro can provide its own custom heap
283/// implementation, or allow others to provide their own custom heap
284/// implementation, by adding a `custom-heap` feature to its `Cargo.toml`. After
285/// enabling the feature, one may define their own [global allocator] in the
286/// standard way.
287///
288/// [global allocator]: https://doc.rust-lang.org/stable/std/alloc/trait.GlobalAlloc.html
289///
290/// # RISC-V heap base
291///
292/// On the RISC-V target the heap base is supplied by the loader at runtime, not fixed at
293/// compile time. The `entrypoint!` and `entrypoint_no_alloc!` macros record it (via
294/// `allocator::set_heap_base`) before any allocation runs. If you install this allocator
295/// behind a hand-rolled entrypoint instead of those macros, you must call both `set_heap_base`
296/// and `set_heap_limit` at the very start of the entrypoint. Omitting `set_heap_base` leaves the
297/// heap at address 0, so the first allocation hits unmapped memory and traps; omitting
298/// `set_heap_limit` leaves the limit at 0, so the allocator refuses every allocation (null →
299/// `handle_alloc_error` → abort).
300///
301#[macro_export]
302macro_rules! custom_heap_default {
303    () => {
304        #[cfg(all(not(feature = "custom-heap"), target_os = "solana"))]
305        #[global_allocator]
306        static A: $crate::allocator::BumpAllocator<()> =
307            unsafe { $crate::allocator::BumpAllocator::new() };
308    };
309}
310
311/// Define the default global panic handler.
312///
313/// This must be used if the [`entrypoint`] macro is not used, and no other
314/// panic handler has been defined; otherwise compilation will fail with a
315/// missing `custom_panic` symbol.
316///
317/// The default global allocator is enabled only if the calling crate has not
318/// disabled it using [Cargo features] as described below. It is only defined
319/// for on-chain targets.
320///
321/// [Cargo features]: https://doc.rust-lang.org/cargo/reference/features.html
322///
323/// # Cargo features
324///
325/// A crate that calls this macro can provide its own custom panic handler, or
326/// allow others to provide their own custom panic handler, by adding a
327/// `custom-panic` feature to its `Cargo.toml`. After enabling the feature, one
328/// may define their own panic handler.
329///
330/// A good way to reduce the final size of the program is to provide a
331/// `custom_panic` implementation that does nothing. Doing so will cut ~25kb
332/// from a noop program. That number goes down the more the programs pulls in
333/// Rust's standard library for other purposes.
334///
335/// # Defining a panic handler for Solana
336///
337/// _The mechanism for defining a Solana panic handler is different [from most
338/// Rust programs][rpanic]._
339///
340/// [rpanic]: https://doc.rust-lang.org/nomicon/panic-handler.html
341///
342/// To define a panic handler one must define a `custom_panic` function
343/// with the `#[no_mangle]` attribute, as below:
344///
345/// ```ignore
346/// #[cfg(all(feature = "custom-panic", target_os = "solana"))]
347/// #[no_mangle]
348/// fn custom_panic(info: &core::panic::PanicInfo<'_>) {
349///     $crate::msg!("{}", info);
350/// }
351/// ```
352///
353/// The above is how Solana defines the default panic handler.
354#[macro_export]
355macro_rules! custom_panic_default {
356    () => {
357        #[cfg(all(not(feature = "custom-panic"), target_os = "solana"))]
358        #[no_mangle]
359        fn custom_panic(info: &core::panic::PanicInfo<'_>) {
360            // Full panic reporting
361            $crate::__msg!("{}", info);
362        }
363    };
364}
365
366/// `assert_eq(std::mem::align_of::<u128>(), 8)` is true for on-chain targets but not for some host machines
367pub const BPF_ALIGN_OF_U128: usize = 8;
368
369#[allow(clippy::arithmetic_side_effects)]
370#[inline(always)] // this reduces CU usage
371unsafe fn deserialize_instruction_data<'a>(input: *mut u8, mut offset: usize) -> (&'a [u8], usize) {
372    #[allow(clippy::cast_ptr_alignment)]
373    let instruction_data_len = *(input.add(offset) as *const u64) as usize;
374    offset += size_of::<u64>();
375
376    let instruction_data = { from_raw_parts(input.add(offset), instruction_data_len) };
377    offset += instruction_data_len;
378
379    (instruction_data, offset)
380}
381
382#[allow(clippy::arithmetic_side_effects)]
383#[inline(always)] // this reduces CU usage by half!
384unsafe fn deserialize_account_info<'a>(
385    input: *mut u8,
386    mut offset: usize,
387) -> (AccountInfo<'a>, usize) {
388    #[allow(clippy::cast_ptr_alignment)]
389    let is_signer = *(input.add(offset) as *const u8) != 0;
390    offset += size_of::<u8>();
391
392    #[allow(clippy::cast_ptr_alignment)]
393    let is_writable = *(input.add(offset) as *const u8) != 0;
394    offset += size_of::<u8>();
395
396    #[allow(clippy::cast_ptr_alignment)]
397    let executable = *(input.add(offset) as *const u8) != 0;
398    offset += size_of::<u8>();
399
400    // The original data length is stored here because these 4 bytes were
401    // originally only used for padding and served as a good location to
402    // track the original size of the account data in a compatible way.
403    let original_data_len_offset = offset;
404    offset += size_of::<u32>();
405
406    let key: &Pubkey = &*(input.add(offset) as *const Pubkey);
407    offset += size_of::<Pubkey>();
408
409    let owner: &Pubkey = &*(input.add(offset) as *const Pubkey);
410    offset += size_of::<Pubkey>();
411
412    #[allow(clippy::cast_ptr_alignment)]
413    let kelvins = Rc::new(RefCell::new(&mut *(input.add(offset) as *mut u64)));
414    offset += size_of::<u64>();
415
416    #[allow(clippy::cast_ptr_alignment)]
417    let data_len = *(input.add(offset) as *const u64) as usize;
418    offset += size_of::<u64>();
419
420    // Store the original data length for detecting invalid reallocations and
421    // requires that MAX_PERMITTED_DATA_LENGTH fits in a u32
422    *(input.add(original_data_len_offset) as *mut u32) = data_len as u32;
423
424    let data = Rc::new(RefCell::new({
425        from_raw_parts_mut(input.add(offset), data_len)
426    }));
427    offset += data_len + MAX_PERMITTED_DATA_INCREASE;
428    offset += (offset as *const u8).align_offset(BPF_ALIGN_OF_U128); // padding
429
430    #[allow(clippy::cast_ptr_alignment)]
431    let rent_epoch = *(input.add(offset) as *const u64);
432    offset += size_of::<u64>();
433
434    (
435        AccountInfo {
436            key,
437            is_signer,
438            is_writable,
439            kelvins,
440            data,
441            owner,
442            executable,
443            rent_epoch,
444        },
445        offset,
446    )
447}
448
449/// Deserialize the input arguments
450///
451/// The integer arithmetic in this method is safe when called on a buffer that was
452/// serialized by runtime. Use with buffers serialized otherwise is unsupported and
453/// done at one's own risk.
454///
455/// # Safety
456#[allow(clippy::arithmetic_side_effects)]
457pub unsafe fn deserialize<'a>(input: *mut u8) -> (&'a Pubkey, Vec<AccountInfo<'a>>, &'a [u8]) {
458    // Instruction data
459    let (instruction_data, mut offset) = deserialize_instruction_data(input, 0);
460
461    // Program Id
462    let program_id: &Pubkey = &*(input.add(offset) as *const Pubkey);
463    offset += 32;
464
465    // Padding to align `offset` to an 8-byte boundary
466    let padding = (offset as *const u8).align_offset(BPF_ALIGN_OF_U128);
467    offset += padding;
468
469    // Number of accounts present
470    #[allow(clippy::cast_ptr_alignment)]
471    let num_accounts = *(input.add(offset) as *const u64) as usize;
472    offset += size_of::<u64>();
473
474    // Account Infos
475    let mut accounts = Vec::with_capacity(num_accounts);
476    for _ in 0..num_accounts {
477        let dup_info = *(input.add(offset) as *const u8);
478        offset += size_of::<u8>();
479        if dup_info == NON_DUP_MARKER {
480            let (account_info, new_offset) = deserialize_account_info(input, offset);
481            offset = new_offset;
482            accounts.push(account_info);
483        } else {
484            offset += 7; // padding
485
486            // Duplicate account, clone the original
487            accounts.push(accounts[dup_info as usize].clone());
488        }
489    }
490
491    (program_id, accounts, instruction_data)
492}
493
494/// Deserialize the input arguments
495///
496/// Differs from `deserialize` by writing the account infos into an uninitialized
497/// slice, which provides better performance, roughly 30 CUs per unique account
498/// provided to the instruction.
499///
500/// Panics if the input slice is not large enough.
501///
502/// The integer arithmetic in this method is safe when called on a buffer that was
503/// serialized by runtime. Use with buffers serialized otherwise is unsupported and
504/// done at one's own risk.
505///
506/// # Safety
507#[allow(clippy::arithmetic_side_effects)]
508pub unsafe fn deserialize_into<'a>(
509    input: *mut u8,
510    accounts: &mut [MaybeUninit<AccountInfo<'a>>],
511) -> (&'a Pubkey, usize, &'a [u8]) {
512    // Instruction data
513    let (instruction_data, mut offset) = deserialize_instruction_data(input, 0);
514
515    // Program Id
516    let program_id: &Pubkey = &*(input.add(offset) as *const Pubkey);
517    offset += 32;
518
519    // Padding to align `offset` to an 8-byte boundary
520    let padding = (offset as *const u8).align_offset(BPF_ALIGN_OF_U128);
521    offset += padding;
522
523    // Number of accounts present
524    #[allow(clippy::cast_ptr_alignment)]
525    let num_accounts = *(input.add(offset) as *const u64) as usize;
526    offset += size_of::<u64>();
527
528    if num_accounts > accounts.len() {
529        panic!(
530            "{} accounts provided, but only {} are supported",
531            num_accounts,
532            accounts.len()
533        );
534    }
535
536    // Account Infos
537
538    for i in 0..num_accounts {
539        let dup_info = *(input.add(offset) as *const u8);
540        offset += size_of::<u8>();
541        if dup_info == NON_DUP_MARKER {
542            let (account_info, new_offset) = deserialize_account_info(input, offset);
543            offset = new_offset;
544            accounts[i].write(account_info);
545        } else {
546            offset += 7; // padding
547
548            // Duplicate account, clone the original
549            accounts[i].write(accounts[dup_info as usize].assume_init_ref().clone());
550        }
551    }
552
553    (program_id, num_accounts, instruction_data)
554}