hopper_runtime/lib.rs
1//! Hopper Runtime -- canonical semantic runtime surface.
2//!
3//! Hopper Runtime owns the public rules, validation, typed loading, CPI
4//! semantics, and execution context that authored Hopper code targets.
5//! Hopper Native owns the raw execution boundary.
6
7#![no_std]
8#![deny(unsafe_op_in_unsafe_fn)]
9// The backend `AccountView` is `Copy` only under the `copy` feature. The
10// `.clone()` in our manual `Clone` impl is required in the default lane, so we
11// silence `clone_on_copy` only in the lane where the type is actually `Copy`.
12#![cfg_attr(feature = "copy", allow(clippy::clone_on_copy))]
13
14#[cfg(any(test, feature = "thread-local-registry"))]
15extern crate std;
16
17#[doc(hidden)]
18pub mod native_boundary;
19
20pub mod account;
21pub mod account_wrappers;
22pub mod address;
23pub mod audit;
24pub mod behavior;
25pub mod borrow;
26pub(crate) mod borrow_registry;
27pub mod compact;
28#[cfg(any(not(target_os = "solana"), feature = "remaining-compute-units-syscall"))]
29pub mod compute;
30pub mod cpi;
31pub mod cpi_event;
32pub mod crank;
33pub mod crypto;
34pub mod dyn_cpi;
35pub mod error;
36pub mod field_map;
37pub mod foreign;
38pub mod interop;
39pub mod lamports;
40pub mod lazy;
41pub mod log;
42pub mod memory;
43pub mod migrate;
44pub mod pod;
45pub mod policy;
46pub mod proof;
47pub mod ref_only;
48pub mod result;
49pub mod segment;
50pub mod tail;
51pub mod utils;
52pub mod zerocopy;
53// Re-export the sealed marker module at the crate root so macro
54// codegen can address it as `::hopper_runtime::__sealed::...`. It's
55// doc-hidden because it is the sealed enforcement surface,
56// not a normal-user-facing API.
57#[doc(hidden)]
58pub use zerocopy::__sealed;
59pub mod context;
60pub mod instruction;
61pub mod layout;
62pub mod option_byte;
63pub mod pda;
64pub mod remaining;
65pub mod rent;
66pub mod return_data;
67pub mod segment_borrow;
68pub mod segment_lease;
69pub mod syscall;
70pub mod syscalls;
71pub mod system;
72pub mod token;
73pub mod token_2022_ext;
74pub mod token_mint;
75pub mod write_policy;
76
77pub use account::AccountView;
78pub use account_wrappers::{
79 Account, InitAccount, Interface, InterfaceAccount, InterfaceAccountLayout,
80 InterfaceAccountResolve, InterfaceSpec, Program, ProgramId, Signer as HopperSigner,
81 SystemAccount, SystemId, UncheckedAccount,
82};
83pub use address::Address;
84pub use audit::{AccountAudit, DuplicateAccount};
85pub use behavior::{BehaviorChecked, BehaviorWrite, HopperBehavior};
86pub use borrow::{Ref, RefMut};
87pub use compact::{CompactDynamicLayout, CompactLayout, COMPACT_BODY_OFFSET};
88#[cfg(any(not(target_os = "solana"), feature = "remaining-compute-units-syscall"))]
89pub use compute::{check_compute_units, remaining_compute_units, require_compute_units};
90pub use context::{Context, ScopedContext};
91pub use cpi::{invoke, invoke_checked, invoke_signed, invoke_signed_checked};
92#[cfg(feature = "crypto-big-mod-exp")]
93pub use crypto::big_mod_exp;
94#[cfg(feature = "crypto-bn254")]
95pub use crypto::{
96 alt_bn128_add, alt_bn128_g1_addition_be, alt_bn128_g1_compress_be, alt_bn128_g1_decompress_be,
97 alt_bn128_g1_multiplication_be, alt_bn128_g2_compress_be, alt_bn128_g2_decompress_be,
98 alt_bn128_mul, alt_bn128_pairing, alt_bn128_pairing_be,
99};
100pub use crypto::{
101 blake3, blake3_single, keccak256, keccak256_single, recover_ethereum_address,
102 secp256k1_recover, sha256, sha256_single,
103};
104#[cfg(feature = "crypto-curve")]
105pub use crypto::{curve_group_add, curve_group_mul, curve_group_sub, curve_multiscalar_mul};
106#[cfg(feature = "crypto-poseidon")]
107pub use crypto::{poseidon_bn254_x5, poseidon_hash, poseidon_hashv};
108pub use error::ProgramError;
109pub use field_map::{FieldInfo, FieldMap};
110pub use foreign::{
111 ExplainExternal, ExternalAccount, ExternalBytes, ExternalChecked, ExternalExplainSink,
112 ExternalLens, ExternalLensValue, ExternalProof, ExternalResolve, ExternalZeroCopy, ForeignLens,
113 ForeignManifest,
114};
115pub use interop::TransparentAddress;
116pub use lamports::transfer_lamports;
117pub use lazy::LazyContext;
118pub use migrate::{
119 apply_pending_migrations, ensure_fits_with_rent, migrate_layout, migrate_layout_resizing,
120 validate_header_for_epoch_migration, LayoutMigration, MigrationEdge,
121};
122pub use policy::{HopperInstructionPolicy, HopperProgramPolicy, HopperProgramProfile};
123pub use proof::{
124 AccountProof, ExecutableChecked, HasOneChecked, LayoutChecked, OwnerChecked, SeedsChecked,
125 SignerChecked, TokenExtensionsChecked, Unchecked, WritableChecked,
126};
127pub use ref_only::HopperRefOnly;
128pub use remaining::{
129 RemainingAccountViews, RemainingAccounts, RemainingError, RemainingExternalAccounts,
130 RemainingGroup, RemainingLazy, RemainingLazySlot, RemainingMode, RemainingSigners,
131 RemainingTyped, MAX_REMAINING_ACCOUNTS,
132};
133pub use return_data::{get_return_data, set_return_data, try_set_return_data, ReturnData};
134pub use tail::{
135 borrow_address_slice, borrow_bounded_str, read_tail, read_tail_len, seq_capacity_for,
136 seq_region_bytes_for, tail_capacity, tail_payload, write_tail, write_tail_payload,
137 BoundedString, BoundedVec, HopperString, HopperVec, SeqElement, SeqTailRead, SeqTailWrite,
138 TailBytes, TailCodec, TailElement, TailSeq, TailSeqIter, TailSeqMut, TailStr, SEQ_LEN_PREFIX,
139};
140
141/// Compose a layout's `LayoutMigration::MIGRATIONS` chain from a list
142/// of `#[hopper::migrate]`-emitted edge constants.
143///
144/// ```ignore
145/// #[hopper::migrate(from = 1, to = 2)]
146/// pub fn vault_v1_to_v2(body: &mut [u8]) -> ProgramResult { Ok(()) }
147///
148/// hopper::layout_migrations! {
149/// Vault = [VAULT_V1_TO_V2_EDGE, VAULT_V2_TO_V3_EDGE],
150/// }
151/// ```
152///
153/// Emits `impl LayoutMigration for Vault { const MIGRATIONS = .. }`.
154/// Each list entry must evaluate to a
155/// [`MigrationEdge`](crate::migrate::MigrationEdge). typically the
156/// `<UPPER_SNAKE_FN_NAME>_EDGE` constant that
157/// `#[hopper::migrate]` emits alongside each migration function.
158/// Chain continuity (every adjacent pair must satisfy
159/// `a.to_epoch == b.from_epoch`) is enforced at runtime by
160/// [`apply_pending_migrations`].
161#[macro_export]
162macro_rules! layout_migrations {
163 ( $layout:ty = [ $( $edge:expr ),+ $(,)? ] $(,)? ) => {
164 impl $crate::migrate::LayoutMigration for $layout {
165 const MIGRATIONS: &'static [$crate::migrate::MigrationEdge] = &[
166 $( $edge ),+
167 ];
168 }
169 };
170}
171pub use instruction::CpiAccount;
172pub use instruction::{
173 InstructionAccount, InstructionView, Seed, Signer, StoredAccountMeta, StoredInstruction,
174};
175pub use layout::{HopperHeader, LayoutContract, LayoutInfo};
176pub use pod::{read_unaligned_value, Pod, ValuePod, Zeroable};
177pub use result::ProgramResult;
178pub use segment::{
179 FieldCapability, Segment, TypedSegment, FIELD_POLICY_AUTHORITY_GATED,
180 FIELD_POLICY_CHECKED_MATH, FIELD_POLICY_IMMUTABLE_AFTER_INIT, FIELD_ROLE_AUTHORITY,
181 FIELD_ROLE_BALANCE, FIELD_ROLE_DATA, FIELD_ROLE_VERSION,
182};
183pub use segment_borrow::{AccessKind, SegmentBorrow, SegmentBorrowGuard, SegmentBorrowRegistry};
184pub use segment_lease::{SegRef, SegRefMut, SegmentLease, SegmentsMut};
185pub use write_policy::{
186 ParametricWriteRange, WritePolicy, WriteRange, WRITE_POLICY_VIOLATION_PAGE,
187};
188pub use zerocopy::{AccountLayout, WireLayout, ZeroCopy};
189
190pub const MAX_TX_ACCOUNTS: usize = native_boundary::BACKEND_MAX_TX_ACCOUNTS;
191pub const SUCCESS: u64 = native_boundary::BACKEND_SUCCESS;
192
193#[doc(hidden)]
194pub use hopper_native as __hopper_native;
195pub use hopper_native::sha256;
196
197#[doc(hidden)]
198pub use hopper_native::address::decode_base58_32 as __decode_base58_32;
199
200/// Compile-time base58 address literal.
201#[macro_export]
202macro_rules! address {
203 ( $literal:expr ) => {
204 $crate::Address::new_from_array($crate::__decode_base58_32($literal))
205 };
206}
207
208/// Declare a program's on-chain id, mirroring the `declare_id!` convention
209/// every other Solana framework ships (Anchor, Pinocchio, Quasar).
210///
211/// Expands to a `pub const ID: Address` decoded at compile time, a
212/// `pub const fn id() -> Address` accessor, and a `pub fn check_id(&Address)
213/// -> bool` guard. Programs that pin their own id for self-PDA derivation,
214/// CPI-guard checks, or `require_keys_eq!(program.key(), &crate::ID)` use this
215/// instead of hand-rolling an [`address!`] constant.
216///
217/// ```ignore
218/// hopper::declare_id!("D8UGWDX5QRwEkKs2J9Sweabf4zd6hzdLqv7CB11SF91F");
219/// assert!(crate::check_id(&crate::ID));
220/// ```
221#[macro_export]
222macro_rules! declare_id {
223 ( $literal:expr ) => {
224 /// The program's on-chain address, decoded at compile time.
225 pub const ID: $crate::Address = $crate::address!($literal);
226
227 /// Return the program's declared id.
228 #[inline]
229 pub const fn id() -> $crate::Address {
230 ID
231 }
232
233 /// Return true if `other` equals the declared program id.
234 #[inline]
235 pub fn check_id(other: &$crate::Address) -> bool {
236 *other == ID
237 }
238 };
239}
240
241/// A program-derived address evaluated at compile time:
242/// `const_pda!(PROGRAM_ID, [seed, ...], bump)` is
243/// [`pda::const_program_address`] with the seed list spelled inline (each
244/// seed anything that casts to `&[u8]`: a byte-string literal, an
245/// `Address::as_array()`, a `&[u8; N]`). See that function for the bump
246/// contract and the soundness note.
247///
248/// ```ignore
249/// hopper::declare_id!("F4Um7PWsnZfN7y8WFzu1aPYJwqGduJTa4zuCGY9EUqMy");
250/// pub const VAULT: hopper::Address = hopper::const_pda!(ID, [b"vault", ID.as_array()], 255);
251/// ```
252#[macro_export]
253macro_rules! const_pda {
254 ( $program_id:expr, [ $( $seed:expr ),* $(,)? ], $bump:expr ) => {
255 $crate::pda::const_program_address(&$program_id, &[ $( $seed as &[u8] ),* ], $bump)
256 };
257}
258
259/// Early-return with an error if the condition is false.
260#[macro_export]
261macro_rules! require {
262 ( $cond:expr, $err:expr ) => {
263 if !($cond) {
264 return Err($err);
265 }
266 };
267 ( $cond:expr ) => {
268 if !($cond) {
269 return Err($crate::ProgramError::InvalidArgument);
270 }
271 };
272}
273
274/// Assert two values are equal, returning an error on mismatch.
275#[macro_export]
276macro_rules! require_eq {
277 ( $left:expr, $right:expr, $err:expr ) => {
278 if ($left) != ($right) {
279 return Err($err);
280 }
281 };
282 ( $left:expr, $right:expr ) => {
283 if ($left) != ($right) {
284 return Err($crate::ProgramError::InvalidArgument);
285 }
286 };
287}
288
289/// Assert two values are not equal. Early-returns with the supplied
290/// error on match (or `ProgramError::InvalidArgument` in the short
291/// form). Symmetric with [`require_eq!`].
292#[macro_export]
293macro_rules! require_neq {
294 ( $left:expr, $right:expr, $err:expr ) => {
295 if ($left) == ($right) {
296 return Err($err);
297 }
298 };
299 ( $left:expr, $right:expr ) => {
300 if ($left) == ($right) {
301 return Err($crate::ProgramError::InvalidArgument);
302 }
303 };
304}
305
306/// Assert two public keys (or any byte slices convertible via
307/// [`AsRef<[u8; 32]>`]) are equal. Narrower than [`require_eq!`] but
308/// matches the ergonomic spelling ecosystem migrators coming from
309/// Anchor / Jiminy are familiar with.
310///
311/// ```ignore
312/// hopper::require_keys_eq!(
313/// vault.authority,
314/// ctx.signer.address(),
315/// ProgramError::InvalidAccountData,
316/// );
317/// ```
318#[macro_export]
319macro_rules! require_keys_eq {
320 ( $left:expr, $right:expr, $err:expr ) => {
321 if !$crate::address::keys_eq(
322 ::core::convert::AsRef::<[u8; 32]>::as_ref(&$left),
323 ::core::convert::AsRef::<[u8; 32]>::as_ref(&$right),
324 ) {
325 return Err($err);
326 }
327 };
328 ( $left:expr, $right:expr ) => {
329 if !$crate::address::keys_eq(
330 ::core::convert::AsRef::<[u8; 32]>::as_ref(&$left),
331 ::core::convert::AsRef::<[u8; 32]>::as_ref(&$right),
332 ) {
333 return Err($crate::ProgramError::InvalidAccountData);
334 }
335 };
336}
337
338/// Assert two public keys are *not* equal. Used for pinning distinct
339/// accounts (authority != user, source != destination). Same coercion
340/// and error semantics as [`require_keys_eq!`].
341#[macro_export]
342macro_rules! require_keys_neq {
343 ( $left:expr, $right:expr, $err:expr ) => {
344 if $crate::address::keys_eq(
345 ::core::convert::AsRef::<[u8; 32]>::as_ref(&$left),
346 ::core::convert::AsRef::<[u8; 32]>::as_ref(&$right),
347 ) {
348 return Err($err);
349 }
350 };
351 ( $left:expr, $right:expr ) => {
352 if $crate::address::keys_eq(
353 ::core::convert::AsRef::<[u8; 32]>::as_ref(&$left),
354 ::core::convert::AsRef::<[u8; 32]>::as_ref(&$right),
355 ) {
356 return Err($crate::ProgramError::InvalidAccountData);
357 }
358 };
359}
360
361/// Assert `left >= right`, returning the supplied error on underrun.
362/// Useful for lamport / balance checks.
363#[macro_export]
364macro_rules! require_gte {
365 ( $left:expr, $right:expr, $err:expr ) => {
366 if !($left >= $right) {
367 return Err($err);
368 }
369 };
370 ( $left:expr, $right:expr ) => {
371 if !($left >= $right) {
372 return Err($crate::ProgramError::InsufficientFunds);
373 }
374 };
375}
376
377/// Assert `left > right` strictly.
378#[macro_export]
379macro_rules! require_gt {
380 ( $left:expr, $right:expr, $err:expr ) => {
381 if !($left > $right) {
382 return Err($err);
383 }
384 };
385 ( $left:expr, $right:expr ) => {
386 if !($left > $right) {
387 return Err($crate::ProgramError::InvalidArgument);
388 }
389 };
390}
391
392/// Assert `left < right` strictly. Anchor-parity sibling of
393/// [`require_gt!`]. Default error is `ProgramError::InvalidArgument`
394/// because a failed ordering check most often flags a bad user input.
395#[macro_export]
396macro_rules! require_lt {
397 ( $left:expr, $right:expr, $err:expr ) => {
398 if !($left < $right) {
399 return Err($err);
400 }
401 };
402 ( $left:expr, $right:expr ) => {
403 if !($left < $right) {
404 return Err($crate::ProgramError::InvalidArgument);
405 }
406 };
407}
408
409/// Assert `left <= right`. Anchor-parity sibling of [`require_gte!`].
410#[macro_export]
411macro_rules! require_lte {
412 ( $left:expr, $right:expr, $err:expr ) => {
413 if !($left <= $right) {
414 return Err($err);
415 }
416 };
417 ( $left:expr, $right:expr ) => {
418 if !($left <= $right) {
419 return Err($crate::ProgramError::InvalidArgument);
420 }
421 };
422}
423
424/// Return an error immediately. Parallel to Anchor's `err!`.
425///
426/// The macro expands to a bare `return Err(...)`, so the call site
427/// reads like a control-flow keyword rather than an expression. The
428/// argument is evaluated as an expression so either a Hopper-generated
429/// error code or a raw `ProgramError` works.
430///
431/// ```ignore
432/// if amount == 0 {
433/// return err!(VaultError::ZeroDeposit);
434/// }
435/// ```
436#[macro_export]
437macro_rules! err {
438 ( $e:expr ) => {
439 return ::core::result::Result::Err($crate::ProgramError::from($e))
440 };
441}
442
443/// Alias for [`err!`]. Anchor-style spelling for ported code. Functionally
444/// identical.
445#[macro_export]
446macro_rules! error {
447 ( $e:expr ) => {
448 return ::core::result::Result::Err($crate::ProgramError::from($e))
449 };
450}
451
452/// Auditable raw-pointer boundary.
453///
454/// Wraps a block that needs `unsafe` in a named Hopper macro so an
455/// auditor can grep `hopper_unsafe_region!` and find every raw
456/// reinterpretation in the tree with one command. The macro expands
457/// to a plain `unsafe { ... }` block: zero runtime cost, identical
458/// codegen, but the invocation site is nameable and documented.
459///
460/// Usage:
461///
462/// ```ignore
463/// let cleared = hopper::hopper_unsafe_region!("clear rewards via raw ptr", {
464/// let ptr = ctx.as_mut_ptr(0)?;
465/// (ptr.add(24) as *mut u64).write_unaligned(0);
466/// 0u64
467/// });
468/// ```
469///
470/// The label is a compile-time string literal. It is discarded by
471/// the expansion but serves as inline documentation an auditor
472/// reads alongside the `unsafe` body.
473#[macro_export]
474macro_rules! hopper_unsafe_region {
475 ( $label:literal, $body:block ) => {{
476 // The label is a compile-time string literal, captured so it
477 // surfaces in `cargo expand` output and can be grep'd out of
478 // the expanded tree the same way as the macro name.
479 const _HOPPER_UNSAFE_REGION_LABEL: &str = $label;
480 #[allow(unused_unsafe)]
481 // 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.
482 unsafe { $body }
483 }};
484}
485
486/// Backend-neutral logging macro.
487#[macro_export]
488macro_rules! msg {
489 ( $literal:expr ) => {{
490 $crate::log::log($literal);
491 }};
492 ( $fmt:expr, $($arg:tt)* ) => {{
493 #[cfg(target_os = "solana")]
494 {
495 use core::fmt::Write;
496 let mut buf = [0u8; 256];
497 let mut wrapper = $crate::log::StackWriter::new(&mut buf);
498 let _ = write!(wrapper, $fmt, $($arg)*);
499 let len = wrapper.pos();
500 $crate::log::log(
501 // 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.
502 unsafe { core::str::from_utf8_unchecked(&buf[..len]) }
503 );
504 }
505 #[cfg(not(target_os = "solana"))]
506 {
507 let _ = ($fmt, $($arg)*);
508 }
509 }};
510}
511
512/// Emit a Hopper event via self-CPI for reliable indexing, the
513/// manual-wiring form.
514///
515/// Most programs should not call this directly: `#[hopper::context(event_cpi)]`
516/// plus `ctx.emit_event_cpi(&event)` wires the same emission (accounts,
517/// bump, sink) with zero hand-written plumbing. Reach for this macro
518/// only when the context macro is out of the picture (raw handlers,
519/// hand-rolled account handling, a custom sink).
520///
521/// Wraps [`cpi_event::encode_event_cpi`] and a call into the active
522/// backend's `invoke_signed` so indexers see the event as an inner
523/// instruction in the transaction metadata. Log output is size-capped; inner
524/// instructions are retained. Anchor's `emit_cpi!` solves the same problem
525/// with the same trick; Hopper's lives in pure Rust so it works under
526/// `no_std` and any of the three backends, and its wire format is
527/// 3 bytes of overhead (2-byte marker + 1-byte tag) against Anchor's 16
528/// (8-byte instruction tag + 8-byte event discriminator).
529///
530/// ## Required program plumbing (manual form only)
531///
532/// The caller must declare a sentinel handler so the dispatcher routes
533/// the self-CPI somewhere, and should authenticate it rather than
534/// no-op, or forged events become possible:
535///
536/// ```ignore
537/// #[instruction(discriminator = [0xE0, 0x1E])]
538/// fn __hopper_event_sink(ctx: &mut Context<'_>) -> ProgramResult {
539/// hopper_runtime::cpi_event::handle_event_sink(ctx, ctx.instruction_data())
540/// }
541/// ```
542///
543/// And a PDA account seeded with [`cpi_event::EVENT_AUTHORITY_SEED`]
544/// (`b"__hopper_event_authority"`) so the CPI has a signer, plus the
545/// program's own account in the instruction so the runtime can resolve
546/// the self-CPI target.
547///
548/// ## Usage
549///
550/// ```ignore
551/// hopper_emit_cpi!(
552/// ctx.program_id(),
553/// event_authority: &AccountView,
554/// event_authority_bump: u8,
555/// Deposited { amount, depositor }
556/// );
557/// ```
558///
559/// `$event` must be a `#[hopper::event]` type (anything implementing
560/// [`cpi_event::CpiEvent`]). Expands to: build instruction bytes,
561/// invoke_signed with the event_authority PDA as the signer. One CPI,
562/// bounded stack allocation, zero heap.
563#[macro_export]
564macro_rules! hopper_emit_cpi {
565 ( $program_id:expr, $event_authority:expr, $bump:expr, $event:expr ) => {{
566 // Build the wire format into a stack buffer. MAX_EVENT_PAYLOAD
567 // (512) bytes fits every sensibly-sized event; callers with
568 // larger events should grow the buffer at the call site or use
569 // `emit!` with the log-based path.
570 let __ev = $event;
571 let __tag: u8 = $crate::cpi_event::CpiEvent::tag(&__ev);
572 let __payload: &[u8] = $crate::cpi_event::CpiEvent::payload_bytes(&__ev);
573 let mut __buf = [0u8; 2 + 1 + $crate::cpi_event::MAX_EVENT_PAYLOAD];
574 let __n = $crate::cpi_event::encode_event_cpi(__tag, __payload, &mut __buf[..])
575 .ok_or($crate::ProgramError::InvalidInstructionData)?;
576 // Signer seeds for the event-authority PDA. The caller
577 // derived and cached `$bump` so this is a stored-bump CPI.
578 let __bump_byte: [u8; 1] = [$bump];
579 let __seed_slices: [&[u8]; 2] = [$crate::cpi_event::EVENT_AUTHORITY_SEED, &__bump_byte[..]];
580 $crate::cpi_event::invoke_event_cpi(
581 $program_id,
582 $event_authority,
583 &__buf[..__n],
584 &__seed_slices[..],
585 )?;
586 }};
587}
588
589/// Cheap structured logging for hot handlers.
590///
591/// `hopper_log!` is the compute-unit-aware sibling of [`msg!`]. It
592/// dispatches to the backend's native log syscall with no format
593/// machinery, no stack buffer, and no UTF-8 formatting pass. The
594/// tradeoff: fewer ergonomics, predictable CU.
595///
596/// Forms:
597///
598/// - `hopper_log!("static message")` - one `sol_log_` syscall.
599/// - `hopper_log!(my_str_slice)` - same, but for runtime `&str` values.
600/// - `hopper_log!("label:", u64_value)` - one `sol_log_` plus one
601/// `sol_log_64_`. Five `u64` slots (the `sol_log_64_` ABI) are
602/// populated left-to-right and the rest zero.
603/// - `hopper_log!("label:", a, b)` through `hopper_log!("label:", a, b, c, d, e)` -
604/// same pattern; up to five integer values per call.
605///
606/// Reach for `msg!` when you need `{}`-style formatting. Reach for
607/// `hopper_log!` when you are paying for every CU and you already
608/// know the shape of the data.
609#[macro_export]
610macro_rules! hopper_log {
611 // One label + 1..=5 integer values. Each integer is cast to `u64`
612 // at the call site so callers do not need to sprinkle `as u64`.
613 ($label:expr, $a:expr) => {{
614 $crate::log::log($label);
615 $crate::log::log_64($a as u64, 0, 0, 0, 0);
616 }};
617 ($label:expr, $a:expr, $b:expr) => {{
618 $crate::log::log($label);
619 $crate::log::log_64($a as u64, $b as u64, 0, 0, 0);
620 }};
621 ($label:expr, $a:expr, $b:expr, $c:expr) => {{
622 $crate::log::log($label);
623 $crate::log::log_64($a as u64, $b as u64, $c as u64, 0, 0);
624 }};
625 ($label:expr, $a:expr, $b:expr, $c:expr, $d:expr) => {{
626 $crate::log::log($label);
627 $crate::log::log_64($a as u64, $b as u64, $c as u64, $d as u64, 0);
628 }};
629 ($label:expr, $a:expr, $b:expr, $c:expr, $d:expr, $e:expr) => {{
630 $crate::log::log($label);
631 $crate::log::log_64($a as u64, $b as u64, $c as u64, $d as u64, $e as u64);
632 }};
633 // Bare message. Uses the one-argument `log::log` syscall.
634 ($msg:expr) => {{
635 $crate::log::log($msg);
636 }};
637}
638
639/// Declare the explicit Hopper runtime entrypoint bridge.
640///
641/// This is Hopper's direct runtime entrypoint over Solana account memory.
642#[macro_export]
643macro_rules! hopper_entrypoint {
644 ( $process_instruction:expr ) => {
645 $crate::hopper_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
646 };
647 ( $process_instruction:expr, $maximum:expr ) => {
648 /// # Safety
649 ///
650 /// Called by the Solana runtime; `input` is a valid BPF input buffer.
651 #[no_mangle]
652 pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
653 const UNINIT: core::mem::MaybeUninit<$crate::__hopper_native::AccountView<'static>> =
654 core::mem::MaybeUninit::<$crate::__hopper_native::AccountView<'static>>::uninit();
655 let mut accounts = [UNINIT; $maximum];
656
657 // 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.
658 let (program_id, count, instruction_data) = unsafe {
659 $crate::__hopper_native::raw_input::deserialize_accounts::<$maximum>(
660 input,
661 &mut accounts,
662 )
663 };
664
665 // 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.
666 let hopper_program_id = unsafe {
667 &*(program_id as *const $crate::__hopper_native::Address as *const $crate::Address)
668 };
669 // 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.
670 let hopper_accounts = unsafe {
671 core::slice::from_raw_parts(
672 accounts.as_ptr() as *const $crate::AccountView<'_>,
673 count,
674 )
675 };
676
677 match $process_instruction(hopper_program_id, hopper_accounts, instruction_data) {
678 Ok(()) => $crate::__hopper_native::SUCCESS,
679 Err(error) => error.into(),
680 }
681 }
682 };
683}
684
685/// Declare the canonical Hopper program entrypoint.
686#[macro_export]
687macro_rules! program_entrypoint {
688 ( $process_instruction:expr ) => {
689 $crate::hopper_entrypoint!($process_instruction);
690 };
691 ( $process_instruction:expr, $maximum:expr ) => {
692 $crate::hopper_entrypoint!($process_instruction, $maximum);
693 };
694}
695
696/// Declare the fast two-argument Hopper entrypoint.
697///
698/// Uses the SVM's second register (`r2`) to receive instruction data
699/// directly under [SIMD-0321], whose gate is active on all public
700/// clusters (mainnet-beta since 2026-04-01). Post the 2026-07-07 fused
701/// single-pass walk this is CU-neutral (+/- 2, measured 2026-07-21) on
702/// programs whose accounts fit the declared maximum, the fused scanner
703/// already banks the old "~30-40 CU" saving; so the feature stays
704/// opt-in; it is the foundation of the SIMD-0449 table path.
705///
706/// Without the `simd-0321` cargo feature this macro expands to the
707/// standard scanning entrypoint, identical semantics, sound everywhere
708/// today. With the feature it expands to the two-argument form, which
709/// null-checks `r2` and falls back to the scanning parse as defense in
710/// depth.
711///
712/// [SIMD-0321]: https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0321-vm-r2-instruction-data-pointer.md
713#[cfg(feature = "simd-0321")]
714#[macro_export]
715macro_rules! hopper_fast_entrypoint {
716 ( $process_instruction:expr ) => {
717 $crate::hopper_fast_entrypoint!($process_instruction, { $crate::MAX_TX_ACCOUNTS });
718 };
719 ( $process_instruction:expr, $maximum:expr ) => {
720 /// # Safety
721 ///
722 /// Called by the Solana runtime; `input` is a valid BPF input buffer.
723 /// When SIMD-0321 is active, `ix_data` points to the instruction data
724 /// with its u64 length stored at offset -8; when it is not active the
725 /// register is zero and the scanning fallback is taken.
726 #[no_mangle]
727 pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data: *const u8) -> u64 {
728 const UNINIT: core::mem::MaybeUninit<$crate::__hopper_native::AccountView> =
729 core::mem::MaybeUninit::<$crate::__hopper_native::AccountView>::uninit();
730 let mut accounts = [UNINIT; $maximum];
731
732 let (program_id, count, instruction_data) = if ix_data.is_null() {
733 // SIMD-0321 not active on this cluster: r2 is zero. Fall back
734 // to the full scanning parse so the program stays correct.
735 // SAFETY: `input` is the loader-provided input buffer; the
736 // scanning parser owns all bounds/duplicate-marker checks.
737 unsafe {
738 $crate::__hopper_native::raw_input::deserialize_accounts::<$maximum>(
739 input,
740 &mut accounts,
741 )
742 }
743 } else {
744 // SAFETY: SIMD-0321 guarantees `ix_data` points at the
745 // instruction-data bytes, with the u64 length prefix at
746 // `ix_data - 8` and the 32-byte program id after the data.
747 let ix_len =
748 unsafe { core::ptr::read_unaligned(ix_data.sub(8) as *const u64) as usize };
749 let instruction_data: &'static [u8] =
750 unsafe { core::slice::from_raw_parts(ix_data, ix_len) };
751 // SAFETY: program id trails the instruction data per the
752 // loader serialization layout; `Address` is a transparent
753 // `[u8; 32]`, so a reference into the buffer is valid at any
754 // offset and lives as long as the invocation.
755 let program_id: &'static $crate::__hopper_native::Address =
756 unsafe { &*(ix_data.add(ix_len) as *const $crate::__hopper_native::Address) };
757
758 if $crate::__hopper_native::raw_input::SIMD_0449_TABLE_ENABLED {
759 // SIMD-0449 build: consume the runtime's appended
760 // pre-deduplicated account-pointer table, O(1)
761 // resolution plus one pointer copy per account. The gate
762 // is a `const`, so the untaken branch folds away entirely.
763 // Macro programs reach this arm the same way the native
764 // entrypoint does, so `hopper/simd-0449` is not a no-op
765 // one tier up.
766 // SAFETY: the `simd-0449` feature asserts the SIMD is
767 // active on the target cluster (table present);
768 // `instruction_data`/`program_id` were derived from the
769 // SIMD-0321 r2 register above.
770 unsafe {
771 $crate::__hopper_native::raw_input::deserialize_accounts_0449_into::<$maximum>(
772 input,
773 &mut accounts,
774 instruction_data,
775 program_id,
776 )
777 }
778 } else {
779 // SAFETY: `input` is the loader input buffer; account-slot
780 // framing is validated by `deserialize_accounts_fast`.
781 unsafe {
782 $crate::__hopper_native::raw_input::deserialize_accounts_fast::<$maximum>(
783 input,
784 &mut accounts,
785 instruction_data,
786 program_id,
787 )
788 }
789 }
790 };
791
792 // SAFETY: `Address` is a transparent 32-byte wrapper shared by the
793 // native and runtime layers; the reinterpret is layout-identical.
794 let hopper_program_id = unsafe {
795 &*(program_id as *const $crate::__hopper_native::Address as *const $crate::Address)
796 };
797 // SAFETY: the first `count` slots were initialized by the parser;
798 // runtime `AccountView` is repr(transparent) over the native view.
799 let hopper_accounts = unsafe {
800 core::slice::from_raw_parts(accounts.as_ptr() as *const $crate::AccountView, count)
801 };
802
803 match $process_instruction(hopper_program_id, hopper_accounts, instruction_data) {
804 Ok(()) => $crate::__hopper_native::SUCCESS,
805 Err(error) => error.into(),
806 }
807 }
808 };
809}
810
811/// Without the `simd-0321` feature the "fast" entrypoint is an alias for
812/// the standard scanning entrypoint. The SIMD-0321 gate is live on every
813/// public cluster (mainnet-beta 2026-04-01), so the two-argument form is
814/// sound to build; it stays opt-in because the r2 path measured CU-neutral
815/// against the fused scanning walk for ~368 bytes of extra `.text` (see the
816/// `simd-0321` feature note in the workspace `Cargo.toml`). Build with
817/// `--features simd-0321` to select the r2 entrypoint.
818#[cfg(not(feature = "simd-0321"))]
819#[macro_export]
820macro_rules! hopper_fast_entrypoint {
821 ( $process_instruction:expr ) => {
822 $crate::hopper_entrypoint!($process_instruction);
823 };
824 ( $process_instruction:expr, $maximum:expr ) => {
825 $crate::hopper_entrypoint!($process_instruction, $maximum);
826 };
827}
828
829/// Declare the count-exact program entrypoint.
830///
831/// Reads the discriminator from the SIMD-0321 `r2` instruction-data pointer
832/// first, then materializes exactly the matched instruction's declared
833/// account bound before dispatching to the helper `#[program]` generated
834/// for it. `arms` pairs each one-byte discriminator with that bound and
835/// that helper. Accounts past the bound are neither materialized nor
836/// walked, and there is no transaction-sized pointer table: the entry cost
837/// is the declared accounts only. The `Context` (segment borrow registry,
838/// write gate, parametric args) is built exactly as on the scanning path.
839///
840/// The `r2` gate (`5xXZc66h4UdB6Yq7FzdBxBiRAFMMScMLwHxk2QZDaNZL`) is active
841/// on mainnet-beta, devnet, and testnet. A runtime that leaves `r2` zero
842/// gets `ProgramError::InvalidArgument` back instead of a scanning
843/// fallback, which keeps the dual-path code out of the binary; `hopper
844/// feature-gate` reports the gate for a target cluster.
845#[macro_export]
846macro_rules! hopper_exact_entrypoint {
847 ( $( ( $disc:literal, $bound:expr, $helper:path ) ),* $(,)? ) => {
848 /// # Safety
849 ///
850 /// Called by the Solana runtime with the loader input in `input` and,
851 /// under SIMD-0321, the instruction-data pointer in `ix_data` (length
852 /// at `ix_data - 8`, program id after the data).
853 #[no_mangle]
854 pub unsafe extern "C" fn entrypoint(input: *mut u8, ix_data: *const u8) -> u64 {
855 if ix_data.is_null() {
856 return $crate::ProgramError::InvalidArgument.into();
857 }
858 // SAFETY: SIMD-0321 serialization contract, see above.
859 let ix_len =
860 unsafe { core::ptr::read_unaligned(ix_data.sub(8) as *const u64) as usize };
861 // SAFETY: `ix_len` bytes of instruction data start at `ix_data`
862 // and live for the whole invocation.
863 let instruction_data: &'static [u8] =
864 unsafe { core::slice::from_raw_parts(ix_data, ix_len) };
865 // SAFETY: the 32-byte program id trails the data; `Address` is a
866 // transparent `[u8; 32]` with alignment 1.
867 let program_id: &'static $crate::Address =
868 unsafe { &*(ix_data.add(ix_len) as *const $crate::Address) };
869 // The matched arm's bound first, so one walk and one `Context`
870 // serve every instruction (no per-arm copy of either).
871 let bound: usize = match instruction_data.first() {
872 $( ::core::option::Option::Some(&$disc) => $bound, )*
873 _ => return $crate::ProgramError::InvalidInstructionData.into(),
874 };
875 const WIDEST: usize = $crate::max_account_bound(&[ $( $bound ),* ]);
876 const UNINIT: core::mem::MaybeUninit<
877 $crate::__hopper_native::AccountView<'static>,
878 > = core::mem::MaybeUninit::uninit();
879 let mut views = [UNINIT; WIDEST];
880 // SAFETY: `input` is the loader input buffer; the prefix walk
881 // validates its own framing and never exceeds `WIDEST` slots.
882 let count = unsafe {
883 $crate::__hopper_native::raw_input::deserialize_leading_accounts::<WIDEST>(
884 input,
885 &mut views,
886 bound,
887 )
888 };
889 // SAFETY: the first `count` slots were initialized by the walk;
890 // runtime `AccountView` is repr(transparent) over the native view.
891 let accounts = unsafe {
892 core::slice::from_raw_parts(views.as_ptr() as *const $crate::AccountView<'_>, count)
893 };
894 let mut ctx = $crate::Context::new(program_id, accounts, instruction_data);
895 let result: ::core::result::Result<(), $crate::ProgramError> =
896 match instruction_data.first() {
897 $( ::core::option::Option::Some(&$disc) => $helper(&mut ctx, instruction_data), )*
898 _ => ::core::result::Result::Err($crate::ProgramError::InvalidInstructionData),
899 };
900 match result {
901 ::core::result::Result::Ok(()) => $crate::__hopper_native::SUCCESS,
902 ::core::result::Result::Err(error) => error.into(),
903 }
904 }
905 };
906}
907
908/// The widest of a program's per-instruction account bounds; sizes the
909/// scratch the count-exact entrypoint materializes into.
910#[doc(hidden)]
911pub const fn max_account_bound(bounds: &[usize]) -> usize {
912 let mut widest = 0usize;
913 let mut i = 0usize;
914 while i < bounds.len() {
915 if bounds[i] > widest {
916 widest = bounds[i];
917 }
918 i += 1;
919 }
920 widest
921}
922
923/// Backward-compatible alias for the fast Hopper entrypoint macro.
924#[macro_export]
925macro_rules! fast_entrypoint {
926 ( $process_instruction:expr ) => {
927 $crate::hopper_fast_entrypoint!($process_instruction);
928 };
929 ( $process_instruction:expr, $maximum:expr ) => {
930 $crate::hopper_fast_entrypoint!($process_instruction, $maximum);
931 };
932}
933
934/// Declare the Hopper lazy entrypoint, RUNTIME-typed, matching the
935/// eager `hopper_fast_entrypoint!`'s layering.
936///
937/// The handler receives `&mut hopper_runtime::lazy::LazyContext` (also
938/// in the facade prelude) and returns the runtime `ProgramResult`; the
939/// expansion bridges to the substrate lazy parser and maps errors
940/// through the layout-twin glue at the boundary. Substrate authors who
941/// want the native-typed context use the substrate layer's own
942/// `hopper_lazy_entrypoint!` directly, exactly as with the eager pair.
943#[macro_export]
944macro_rules! hopper_lazy_entrypoint {
945 ( $process:expr ) => {
946 $crate::__hopper_native::hopper_lazy_entrypoint!(
947 |__hopper_native_ctx: &mut $crate::__hopper_native::LazyContext|
948 -> ::core::result::Result<(), $crate::__hopper_native::error::ProgramError> {
949 let mut __hopper_ctx =
950 $crate::lazy::LazyContext::from_native(__hopper_native_ctx);
951 match $process(&mut __hopper_ctx) {
952 ::core::result::Result::Ok(()) => ::core::result::Result::Ok(()),
953 ::core::result::Result::Err(e) => {
954 ::core::result::Result::Err(::core::convert::From::from(e))
955 }
956 }
957 }
958 );
959 };
960}
961
962/// Backward-compatible alias for the lazy Hopper entrypoint macro.
963#[macro_export]
964macro_rules! lazy_entrypoint {
965 ( $process:expr ) => {
966 $crate::hopper_lazy_entrypoint!($process);
967 };
968}
969
970/// Refuse allocations immediately using the native SVM abort boundary.
971#[macro_export]
972macro_rules! no_allocator {
973 () => {
974 $crate::__hopper_native::no_allocator!();
975 };
976}
977
978/// Install the default bump allocator over the SVM heap region. Opt-in
979/// counterpart to [`no_allocator!`] for programs that need `alloc` on a
980/// cold path. See [`hopper_native::BumpAllocator`].
981#[macro_export]
982macro_rules! default_allocator {
983 () => {
984 #[cfg(target_os = "solana")]
985 #[global_allocator]
986 static ALLOCATOR: $crate::__hopper_native::BumpAllocator =
987 $crate::__hopper_native::BumpAllocator {
988 start: $crate::__hopper_native::HEAP_START_ADDRESS,
989 len: $crate::__hopper_native::HEAP_LENGTH,
990 };
991 };
992}
993
994/// Abort a panicking no_std program immediately without burning its remaining CU.
995#[macro_export]
996macro_rules! nostd_panic_handler {
997 () => {
998 $crate::__hopper_native::nostd_panic_handler!();
999 };
1000}