use core::mem::MaybeUninit;
use crate::account_view::AccountView;
use crate::address::Address;
use crate::error::ProgramError;
#[cold]
#[inline(never)]
pub fn err_to_u64(e: ProgramError) -> u64 {
e.into()
}
#[inline(always)]
pub unsafe fn process_entrypoint<const MAX: usize>(
input: *mut u8,
process_instruction: for<'info> fn(
&'info Address,
&'info [AccountView<'info>],
&'info [u8],
) -> crate::ProgramResult,
) -> u64 {
const UNINIT: MaybeUninit<AccountView<'static>> = MaybeUninit::uninit();
let mut accounts = [UNINIT; 254];
let (program_id, count, instruction_data) =
unsafe { crate::raw_input::deserialize_accounts::<254>(input, &mut accounts) };
let effective_count = count.min(MAX);
let account_slice = unsafe {
core::slice::from_raw_parts(accounts.as_ptr() as *const AccountView<'_>, effective_count)
};
match process_instruction(program_id, account_slice, instruction_data) {
Ok(()) => crate::SUCCESS,
Err(error) => err_to_u64(error),
}
}
#[macro_export]
macro_rules! hopper_program_entrypoint {
( $process_instruction:expr ) => {
$crate::hopper_program_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
};
( $process_instruction:expr, $maximum:expr ) => {
#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
const UNINIT: core::mem::MaybeUninit<$crate::AccountView<'static>> =
core::mem::MaybeUninit::<$crate::AccountView<'static>>::uninit();
let mut accounts = [UNINIT; $maximum];
let (program_id, count, instruction_data) = unsafe {
$crate::raw_input::deserialize_accounts::<$maximum>(input, &mut accounts)
};
match $process_instruction(
program_id,
unsafe {
core::slice::from_raw_parts(
accounts.as_ptr() as *const $crate::AccountView<'_>,
count,
)
},
instruction_data,
) {
Ok(()) => $crate::SUCCESS,
Err(error) => $crate::entrypoint::err_to_u64(error),
}
}
};
}
#[macro_export]
macro_rules! program_entrypoint {
( $process_instruction:expr ) => {
$crate::hopper_program_entrypoint!($process_instruction);
};
( $process_instruction:expr, $maximum:expr ) => {
$crate::hopper_program_entrypoint!($process_instruction, $maximum);
};
}
#[cfg(feature = "simd-0321")]
#[macro_export]
macro_rules! hopper_fast_entrypoint {
( $process_instruction:expr ) => {
$crate::hopper_fast_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
};
( $process_instruction:expr, $maximum:expr ) => {
#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data: *const u8) -> u64 {
const UNINIT: core::mem::MaybeUninit<$crate::AccountView<'static>> =
core::mem::MaybeUninit::<$crate::AccountView<'static>>::uninit();
let mut accounts = [UNINIT; $maximum];
let (program_id, count, instruction_data) = if ix_data.is_null() {
unsafe { $crate::raw_input::deserialize_accounts::<$maximum>(input, &mut accounts) }
} else {
let ix_len =
unsafe { core::ptr::read_unaligned(ix_data.sub(8) as *const u64) as usize };
let instruction_data: &'static [u8] =
unsafe { core::slice::from_raw_parts(ix_data, ix_len) };
let program_id: &'static $crate::Address =
unsafe { &*(ix_data.add(ix_len) as *const $crate::Address) };
if $crate::raw_input::SIMD_0449_TABLE_ENABLED {
unsafe {
$crate::raw_input::deserialize_accounts_0449_into::<$maximum>(
input,
&mut accounts,
instruction_data,
program_id,
)
}
} else {
unsafe {
$crate::raw_input::deserialize_accounts_fast::<$maximum>(
input,
&mut accounts,
instruction_data,
program_id,
)
}
}
};
match $process_instruction(
program_id,
unsafe {
core::slice::from_raw_parts(
accounts.as_ptr() as *const $crate::AccountView<'_>,
count,
)
},
instruction_data,
) {
Ok(()) => $crate::SUCCESS,
Err(error) => $crate::entrypoint::err_to_u64(error),
}
}
};
}
#[cfg(not(feature = "simd-0321"))]
#[macro_export]
macro_rules! hopper_fast_entrypoint {
( $process_instruction:expr ) => {
$crate::hopper_program_entrypoint!($process_instruction);
};
( $process_instruction:expr, $maximum:expr ) => {
$crate::hopper_program_entrypoint!($process_instruction, $maximum);
};
}
#[macro_export]
macro_rules! fast_entrypoint {
( $process_instruction:expr ) => {
$crate::hopper_fast_entrypoint!($process_instruction);
};
( $process_instruction:expr, $maximum:expr ) => {
$crate::hopper_fast_entrypoint!($process_instruction, $maximum);
};
}
#[macro_export]
macro_rules! hopper_lazy_entrypoint {
( $process:expr ) => {
#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
let mut ctx = unsafe { $crate::lazy::lazy_deserialize(input) };
match $process(&mut ctx) {
Ok(()) => $crate::SUCCESS,
Err(error) => $crate::entrypoint::err_to_u64(error),
}
}
};
}
#[macro_export]
macro_rules! lazy_entrypoint {
( $process:expr ) => {
$crate::hopper_lazy_entrypoint!($process);
};
}
#[macro_export]
macro_rules! no_allocator {
() => {
#[cfg(target_os = "solana")]
mod __hopper_allocator {
struct NoAlloc;
unsafe impl core::alloc::GlobalAlloc for NoAlloc {
unsafe fn alloc(&self, _layout: core::alloc::Layout) -> *mut u8 {
unsafe { $crate::syscalls::abort() }
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: core::alloc::Layout) {}
}
#[global_allocator]
static ALLOCATOR: NoAlloc = NoAlloc;
}
};
}
pub const HEAP_START_ADDRESS: usize = 0x3_0000_0000;
pub const HEAP_LENGTH: usize = 32 * 1024;
pub const HEAP_RUNTIME_RESERVED: usize = 20 * 1024;
pub const RENT_CACHE_BYTES: usize = 32;
pub const RENT_CACHE_HEAP_OFFSET: usize = HEAP_RUNTIME_RESERVED - RENT_CACHE_BYTES;
pub const MAX_HEAP_LENGTH: usize = 256 * 1024;
pub struct BumpAllocator {
pub start: usize,
pub len: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HeapMark(usize);
impl HeapMark {
#[cfg(not(target_os = "solana"))]
pub(crate) const fn host() -> Self {
Self(0)
}
}
impl BumpAllocator {
pub const fn new(len: usize) -> Self {
assert!(
len >= HEAP_LENGTH && len <= MAX_HEAP_LENGTH,
"the heap is between 32 KiB and 256 KiB"
);
assert!(
len.is_multiple_of(1024),
"a heap frame is a multiple of 1 KiB"
);
Self {
start: HEAP_START_ADDRESS,
len,
}
}
#[inline(always)]
const fn floor(&self) -> usize {
self.start + core::mem::size_of::<usize>() + HEAP_RUNTIME_RESERVED
}
#[inline(always)]
fn cursor(&self) -> usize {
let pos = unsafe { *(self.start as *const usize) };
if pos == 0 {
self.floor()
} else {
pos
}
}
#[inline(always)]
fn set_cursor(&self, pos: usize) {
unsafe { *(self.start as *mut usize) = pos };
}
#[inline]
pub fn used(&self) -> usize {
self.cursor() - self.floor()
}
#[inline]
pub fn remaining(&self) -> usize {
(self.start + self.len).saturating_sub(self.cursor())
}
#[inline]
pub fn mark(&self) -> HeapMark {
HeapMark(self.cursor())
}
#[inline]
pub unsafe fn release_to(&self, mark: HeapMark) {
let pos = mark.0;
if pos >= self.floor() && pos <= self.cursor() {
self.set_cursor(pos);
}
}
}
unsafe impl core::alloc::GlobalAlloc for BumpAllocator {
#[inline]
unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 {
let mask = layout.align() - 1;
let start = match self.cursor().checked_add(mask) {
Some(bumped) => bumped & !mask,
None => return core::ptr::null_mut(),
};
let end = match start.checked_add(layout.size()) {
Some(end) => end,
None => return core::ptr::null_mut(),
};
if end > self.start + self.len {
return core::ptr::null_mut();
}
self.set_cursor(end);
start as *mut u8
}
#[inline]
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: core::alloc::Layout) {}
#[inline]
unsafe fn realloc(
&self,
ptr: *mut u8,
layout: core::alloc::Layout,
new_size: usize,
) -> *mut u8 {
let block = ptr as usize;
if block.checked_add(layout.size()) == Some(self.cursor()) {
return match block.checked_add(new_size) {
Some(end) if end <= self.start + self.len => {
self.set_cursor(end);
ptr
}
_ => core::ptr::null_mut(),
};
}
if new_size <= layout.size() {
return ptr;
}
let new_layout =
unsafe { core::alloc::Layout::from_size_align_unchecked(new_size, layout.align()) };
let new_ptr = unsafe { self.alloc(new_layout) };
if !new_ptr.is_null() {
unsafe { core::ptr::copy_nonoverlapping(ptr, new_ptr, layout.size()) };
}
new_ptr
}
}
#[macro_export]
macro_rules! default_allocator {
() => {
$crate::default_allocator!(heap = $crate::HEAP_LENGTH);
};
(heap = $len:expr) => {
#[cfg(target_os = "solana")]
#[global_allocator]
static ALLOCATOR: $crate::BumpAllocator = $crate::BumpAllocator::new($len);
};
}
pub const PANIC_REPORTS_LOCATION: bool = cfg!(feature = "panic-location");
pub const PANIC_REPORTS_MESSAGE: bool = cfg!(feature = "panic-message");
#[cfg(target_os = "solana")]
#[inline(always)]
pub fn report_panic(info: &core::panic::PanicInfo<'_>) -> ! {
#[cfg(feature = "panic-message")]
{
use core::fmt::Write;
let mut buf = [0u8; 256];
let mut writer = crate::log::StackWriter::new(&mut buf);
let _ = write!(writer, "{}", info.message());
crate::log::log(writer.as_str());
}
#[cfg(feature = "panic-location")]
if let Some(location) = info.location() {
let file = location.file();
unsafe {
crate::syscalls::sol_panic_(
file.as_ptr(),
file.len() as u64,
location.line() as u64,
location.column() as u64,
)
}
}
let _ = info;
unsafe { crate::syscalls::abort() }
}
#[macro_export]
macro_rules! nostd_panic_handler {
() => {
#[cfg(target_os = "solana")]
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
$crate::entrypoint::report_panic(info)
}
};
}
#[cfg(test)]
mod entrypoint_tail_tests {
extern crate std;
use std::vec;
use std::vec::Vec;
use super::*;
fn build_zero_account_frame(ix_data: &[u8], program_id: [u8; 32]) -> Vec<u64> {
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&0u64.to_le_bytes()); buf.extend_from_slice(&(ix_data.len() as u64).to_le_bytes());
buf.extend_from_slice(ix_data);
buf.extend_from_slice(&program_id);
let mut words = vec![0u64; buf.len().div_ceil(8)];
unsafe {
core::ptr::copy_nonoverlapping(buf.as_ptr(), words.as_mut_ptr() as *mut u8, buf.len());
}
words
}
fn ok_handler<'a>(
_: &'a Address,
_: &'a [AccountView<'a>],
_: &'a [u8],
) -> crate::ProgramResult {
Ok(())
}
fn custom_err_handler<'a>(
_: &'a Address,
_: &'a [AccountView<'a>],
_: &'a [u8],
) -> crate::ProgramResult {
Err(ProgramError::Custom(4242))
}
fn builtin_err_handler<'a>(
_: &'a Address,
_: &'a [AccountView<'a>],
_: &'a [u8],
) -> crate::ProgramResult {
Err(ProgramError::MissingRequiredSignature)
}
#[test]
fn ok_returns_bare_success_zero() {
let mut frame = build_zero_account_frame(&[1, 2, 3], [7u8; 32]);
let code = unsafe { process_entrypoint::<4>(frame.as_mut_ptr() as *mut u8, ok_handler) };
assert_eq!(code, 0);
assert_eq!(code, crate::SUCCESS);
}
#[test]
fn custom_err_maps_through_cold_outline_unchanged() {
let mut frame = build_zero_account_frame(&[], [0u8; 32]);
let code =
unsafe { process_entrypoint::<4>(frame.as_mut_ptr() as *mut u8, custom_err_handler) };
assert_eq!(code, u64::from(ProgramError::Custom(4242)));
assert_eq!(code, err_to_u64(ProgramError::Custom(4242)));
assert_eq!(code, 4242);
}
#[test]
fn builtin_err_maps_through_cold_outline_unchanged() {
let mut frame = build_zero_account_frame(&[], [0u8; 32]);
let code =
unsafe { process_entrypoint::<4>(frame.as_mut_ptr() as *mut u8, builtin_err_handler) };
assert_eq!(code, u64::from(ProgramError::MissingRequiredSignature));
assert_eq!(code, err_to_u64(ProgramError::MissingRequiredSignature));
}
#[test]
fn err_to_u64_matches_from_impl_for_all_variants() {
let cases = [
ProgramError::Custom(0),
ProgramError::Custom(1),
ProgramError::Custom(u32::MAX),
ProgramError::InvalidArgument,
ProgramError::MissingRequiredSignature,
ProgramError::AccountBorrowFailed,
ProgramError::ArithmeticOverflow,
ProgramError::IncorrectAuthority,
];
for e in cases {
assert_eq!(err_to_u64(e.clone()), u64::from(e));
}
}
}
#[cfg(test)]
mod allocator_tests {
extern crate std;
use super::*;
use core::alloc::{GlobalAlloc, Layout};
use std::vec;
use std::vec::Vec;
fn test_heap(len: usize) -> (Vec<u64>, BumpAllocator) {
let mut backing = vec![0u64; len / 8];
let allocator = BumpAllocator {
start: backing.as_mut_ptr() as usize,
len,
};
(backing, allocator)
}
const SCRATCH_END: usize = 8 + HEAP_RUNTIME_RESERVED;
fn layout(size: usize, align: usize) -> Layout {
Layout::from_size_align(size, align).unwrap()
}
#[test]
fn dealloc_gives_nothing_back_and_touches_nothing() {
let (backing, heap) = test_heap(HEAP_LENGTH);
let a = unsafe { heap.alloc(layout(64, 8)) };
let used = heap.used();
unsafe { heap.dealloc(a, layout(64, 8)) };
assert_eq!(heap.used(), used);
let b = unsafe { heap.alloc(layout(64, 8)) };
assert_eq!(b as usize, a as usize + 64, "the freed block is not reused");
assert!(backing[1..].iter().all(|word| *word == 0));
}
#[test]
fn allocations_start_above_the_scratch_and_grow_upward() {
let (backing, heap) = test_heap(HEAP_LENGTH);
assert_eq!(heap.used(), 0);
assert_eq!(heap.remaining(), HEAP_LENGTH - SCRATCH_END);
let a = unsafe { heap.alloc(layout(10, 1)) } as usize;
let b = unsafe { heap.alloc(layout(8, 8)) } as usize;
let c = unsafe { heap.alloc(layout(1, 1)) } as usize;
assert_eq!(a, heap.start + SCRATCH_END);
assert_eq!(
b,
heap.start + SCRATCH_END + 16,
"aligned up past the 10 bytes"
);
assert_eq!(c, b + 8);
assert_eq!(heap.used(), 25);
assert!(backing[1..SCRATCH_END / 8].iter().all(|w| *w == 0));
}
#[test]
fn every_alignment_is_honoured() {
let (_backing, heap) = test_heap(HEAP_LENGTH);
for shift in 0..8 {
let align = 1usize << shift;
let odd = unsafe { heap.alloc(layout(3, 1)) };
assert!(!odd.is_null());
let ptr = unsafe { heap.alloc(layout(5, align)) } as usize;
assert_eq!(ptr % align, 0, "alignment {align}");
}
}
#[test]
fn the_heap_is_exhausted_at_its_declared_end_and_not_before() {
let (_backing, heap) = test_heap(HEAP_LENGTH);
let room = HEAP_LENGTH - SCRATCH_END;
let all = unsafe { heap.alloc(layout(room, 1)) };
assert!(!all.is_null());
assert_eq!(heap.remaining(), 0);
assert!(unsafe { heap.alloc(layout(1, 1)) }.is_null());
assert_eq!(heap.used(), room);
let (_backing, heap) = test_heap(HEAP_LENGTH);
assert!(unsafe { heap.alloc(layout(room + 1, 1)) }.is_null());
assert_eq!(heap.used(), 0);
assert!(unsafe { heap.alloc(layout(isize::MAX as usize - 64, 1)) }.is_null());
}
#[test]
fn a_requested_heap_frame_is_usable_to_its_end() {
let (_backing, heap) = test_heap(MAX_HEAP_LENGTH);
let big = unsafe { heap.alloc(layout(200 * 1024, 8)) };
assert!(!big.is_null());
unsafe { core::ptr::write_bytes(big, 0xA5, 200 * 1024) };
assert_eq!(heap.remaining(), MAX_HEAP_LENGTH - SCRATCH_END - 200 * 1024);
let (_backing, heap) = test_heap(MAX_HEAP_LENGTH);
let small = unsafe { heap.alloc(layout(64, 8)) } as usize;
assert!(small + 64 <= heap.start + HEAP_LENGTH);
}
#[test]
fn the_last_allocation_resizes_in_place() {
let (_backing, heap) = test_heap(HEAP_LENGTH);
let first = layout(16, 8);
unsafe {
let ptr = heap.alloc(first);
core::ptr::write_bytes(ptr, 7, 16);
let grown = heap.realloc(ptr, first, 64);
assert_eq!(grown, ptr, "grown where it is");
assert_eq!(heap.used(), 64);
assert_eq!(*grown.add(15), 7);
let shrunk = heap.realloc(grown, layout(64, 8), 8);
assert_eq!(shrunk, ptr);
assert_eq!(heap.used(), 8, "the tail was given back");
let refused = heap.realloc(shrunk, layout(8, 8), HEAP_LENGTH);
assert!(refused.is_null());
assert_eq!(heap.used(), 8);
}
}
#[test]
fn an_older_allocation_moves_when_it_grows() {
let (_backing, heap) = test_heap(HEAP_LENGTH);
let old = layout(8, 8);
unsafe {
let a = heap.alloc(old);
core::ptr::write_bytes(a, 0x11, 8);
let b = heap.alloc(layout(8, 8));
core::ptr::write_bytes(b, 0x22, 8);
let moved = heap.realloc(a, old, 24);
assert!(
moved as usize > b as usize,
"a new block above the newer one"
);
assert_eq!(core::slice::from_raw_parts(moved, 8), &[0x11; 8]);
assert_eq!(
core::slice::from_raw_parts(b, 8),
&[0x22; 8],
"the neighbour is intact"
);
assert_eq!(heap.realloc(b, layout(8, 8), 4), b);
}
}
#[test]
fn a_checkpoint_gives_the_heap_back() {
let (_backing, heap) = test_heap(HEAP_LENGTH);
unsafe {
let kept = heap.alloc(layout(32, 8));
let mark = heap.mark();
let mut previous: *mut u8 = core::ptr::null_mut();
for _ in 0..1_000 {
let scratch = heap.alloc(layout(4096, 8));
assert!(!scratch.is_null(), "the loop never runs out");
if !previous.is_null() {
assert_eq!(scratch, previous, "the same bytes every time");
}
previous = scratch;
heap.release_to(mark);
}
assert_eq!(heap.used(), 32);
assert_eq!(heap.mark(), mark);
assert!(!kept.is_null());
let after = heap.alloc(layout(8, 8));
let high = heap.mark();
heap.release_to(mark);
heap.release_to(high);
assert_eq!(heap.used(), 32, "a stale mark cannot move the cursor up");
assert!(!after.is_null());
}
}
#[test]
fn the_declared_heap_is_checked() {
assert_eq!(BumpAllocator::new(HEAP_LENGTH).len, HEAP_LENGTH);
assert_eq!(
BumpAllocator::new(MAX_HEAP_LENGTH).start,
HEAP_START_ADDRESS
);
for bad in [
0,
HEAP_LENGTH - 1024,
HEAP_LENGTH + 1,
MAX_HEAP_LENGTH + 1024,
] {
assert!(
std::panic::catch_unwind(|| BumpAllocator::new(bad)).is_err(),
"{bad}"
);
}
}
}