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