Skip to main content

hopper_native/
introspect.rs

1//! Instruction introspection -- stack height and sibling instruction access.
2//!
3//! These syscalls are critical for security patterns that no framework wraps:
4//!
5//! - **CPI guard**: Detect if the current instruction is running inside a CPI
6//!   call (stack height > 1). Prevents unauthorized composition -- e.g., a
7//!   governance instruction that must be top-level only.
8//!
9//! - **Ed25519 signature verification**: Check that a previous instruction in
10//!   the same transaction was to the Ed25519 precompile with specific data.
11//!   This is how on-chain programs verify off-chain signatures without heavy
12//!   crypto libraries.
13//!
14//! - **Secp256k1 recovery**: Same pattern for Ethereum-compatible signatures.
15//!
16//! No existing framework (pinocchio, Anchor, Steel, Quasar) wraps these
17//! syscalls with ergonomic APIs. Programs that need them write raw unsafe
18//! glue every time.
19
20use crate::address::Address;
21use crate::error::ProgramError;
22
23/// Get the current instruction stack height.
24///
25/// Returns 1 for top-level instructions invoked by the runtime.
26/// Returns 2+ for instructions running inside a CPI call.
27///
28/// Use this to implement CPI guards that prevent unauthorized composition.
29#[inline(always)]
30pub fn get_stack_height() -> u64 {
31    #[cfg(target_os = "solana")]
32    {
33        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
34        unsafe { crate::syscalls::sol_get_stack_height() }
35    }
36    #[cfg(not(target_os = "solana"))]
37    {
38        1 // Off-chain: simulate top-level.
39    }
40}
41
42/// Returns true if the current instruction is at the top level
43/// (not running inside a CPI).
44#[inline(always)]
45pub fn is_top_level() -> bool {
46    get_stack_height() <= 1
47}
48
49/// Returns true if the current instruction is running inside a CPI.
50#[inline(always)]
51pub fn is_cpi() -> bool {
52    get_stack_height() > 1
53}
54
55/// Require that the current instruction is NOT a CPI call.
56///
57/// Programs that should never be composed via CPI (governance, admin
58/// instructions, emergency controls) should call this at the top of
59/// their handler. Returns `Err` if the instruction is inside a CPI.
60#[inline(always)]
61pub fn require_top_level() -> Result<(), ProgramError> {
62    if is_top_level() {
63        Ok(())
64    } else {
65        Err(ProgramError::InvalidArgument)
66    }
67}
68
69/// Require that the current instruction IS inside a CPI.
70///
71/// Some instructions are designed to be called only via CPI (callback
72/// patterns, module-internal helpers). This enforces that contract.
73#[inline(always)]
74pub fn require_cpi() -> Result<(), ProgramError> {
75    if is_cpi() {
76        Ok(())
77    } else {
78        Err(ProgramError::InvalidArgument)
79    }
80}
81
82// ---- Processed sibling instructions ----------------------------------
83
84/// Metadata about a previously processed sibling instruction.
85#[derive(Clone, Debug)]
86pub struct ProcessedInstruction {
87    /// Program ID that executed the instruction.
88    pub program_id: Address,
89    /// Instruction data.
90    pub data: [u8; 1232],
91    /// Actual length of instruction data.
92    pub data_len: usize,
93    /// Number of accounts involved.
94    pub accounts_len: usize,
95}
96
97/// Retrieve a previously processed sibling instruction from the current
98/// transaction.
99///
100/// `index` is 0-based: 0 = most recently processed instruction before
101/// the current one, 1 = the one before that, etc.
102///
103/// Returns `None` if no instruction exists at that index.
104///
105/// # Use case: Ed25519 signature verification
106///
107/// To verify an Ed25519 signature on-chain:
108/// 1. The transaction includes an instruction to the Ed25519 precompile
109///    with the message, signature, and public key.
110/// 2. Your program calls `get_processed_instruction(0)` to read that
111///    instruction.
112/// 3. Verify the program_id is the Ed25519 precompile address.
113/// 4. Parse the instruction data to extract the verified message.
114#[inline]
115pub fn get_processed_instruction(index: u64) -> Option<ProcessedInstruction> {
116    #[cfg(target_os = "solana")]
117    {
118        let mut meta = ProcessedInstructionMeta {
119            data_len: 0,
120            accounts_len: 0,
121        };
122        let mut program_id = Address::default();
123        let mut data = [0u8; 1232];
124        // SolAccountMeta is 34 bytes each (32-byte pubkey + 2 bools).
125        // Max accounts per instruction is ~64, so 64 * 34 = 2176 bytes.
126        let mut accounts_buf = [0u8; 2176];
127
128        // The syscall populates meta first to indicate required buffer sizes,
129        // then fills the buffers.
130        meta.data_len = data.len() as u64;
131        meta.accounts_len = (accounts_buf.len() / 34) as u64;
132
133        // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
134        let rc = unsafe {
135            crate::syscalls::sol_get_processed_sibling_instruction(
136                index,
137                &mut meta as *mut ProcessedInstructionMeta as *mut u8,
138                program_id.0.as_mut_ptr(),
139                data.as_mut_ptr(),
140                accounts_buf.as_mut_ptr(),
141            )
142        };
143
144        if rc != 0 {
145            return None;
146        }
147
148        Some(ProcessedInstruction {
149            program_id,
150            data,
151            data_len: meta.data_len as usize,
152            accounts_len: meta.accounts_len as usize,
153        })
154    }
155    #[cfg(not(target_os = "solana"))]
156    {
157        let _ = index;
158        None
159    }
160}
161
162/// Well-known precompile address for Ed25519 signature verification.
163pub const ED25519_PROGRAM_ID: Address =
164    crate::address!("Ed25519SigVerify111111111111111111111111111");
165
166/// Well-known precompile address for Secp256k1 signature recovery.
167pub const SECP256K1_PROGRAM_ID: Address =
168    crate::address!("KeccakSecp256k11111111111111111111111111111");
169
170/// Check that a previous sibling instruction was to the Ed25519 precompile.
171///
172/// Returns the instruction data from the Ed25519 precompile instruction.
173/// The caller should parse this data to extract the verified message,
174/// public key, and signature.
175///
176/// `sibling_index` is 0 for the most recent sibling, 1 for the one before, etc.
177#[inline]
178pub fn require_ed25519_instruction(
179    sibling_index: u64,
180) -> Result<ProcessedInstruction, ProgramError> {
181    let ix = get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
182
183    if !crate::address::address_eq(&ix.program_id, &ED25519_PROGRAM_ID) {
184        return Err(ProgramError::IncorrectProgramId);
185    }
186
187    Ok(ix)
188}
189
190/// Check that a previous sibling instruction was to the Secp256k1 precompile.
191#[inline]
192pub fn require_secp256k1_instruction(
193    sibling_index: u64,
194) -> Result<ProcessedInstruction, ProgramError> {
195    let ix = get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
196
197    if !crate::address::address_eq(&ix.program_id, &SECP256K1_PROGRAM_ID) {
198        return Err(ProgramError::IncorrectProgramId);
199    }
200
201    Ok(ix)
202}
203
204// ---- Internal types for syscall FFI ----------------------------------
205
206#[repr(C)]
207#[allow(dead_code)]
208struct ProcessedInstructionMeta {
209    data_len: u64,
210    accounts_len: u64,
211}