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        unsafe { crate::syscalls::sol_get_stack_height() }
34    }
35    #[cfg(not(target_os = "solana"))]
36    {
37        1 // Off-chain: simulate top-level.
38    }
39}
40
41/// Returns true if the current instruction is at the top level
42/// (not running inside a CPI).
43#[inline(always)]
44pub fn is_top_level() -> bool {
45    get_stack_height() <= 1
46}
47
48/// Returns true if the current instruction is running inside a CPI.
49#[inline(always)]
50pub fn is_cpi() -> bool {
51    get_stack_height() > 1
52}
53
54/// Require that the current instruction is NOT a CPI call.
55///
56/// Programs that should never be composed via CPI (governance, admin
57/// instructions, emergency controls) should call this at the top of
58/// their handler. Returns `Err` if the instruction is inside a CPI.
59#[inline(always)]
60pub fn require_top_level() -> Result<(), ProgramError> {
61    if is_top_level() {
62        Ok(())
63    } else {
64        Err(ProgramError::InvalidArgument)
65    }
66}
67
68/// Require that the current instruction IS inside a CPI.
69///
70/// Some instructions are designed to be called only via CPI (callback
71/// patterns, module-internal helpers). This enforces that contract.
72#[inline(always)]
73pub fn require_cpi() -> Result<(), ProgramError> {
74    if is_cpi() {
75        Ok(())
76    } else {
77        Err(ProgramError::InvalidArgument)
78    }
79}
80
81// ---- Processed sibling instructions ----------------------------------
82
83/// Metadata about a previously processed sibling instruction.
84#[derive(Clone, Debug)]
85pub struct ProcessedInstruction {
86    /// Program ID that executed the instruction.
87    pub program_id: Address,
88    /// Instruction data.
89    pub data: [u8; 1232],
90    /// Actual length of instruction data.
91    pub data_len: usize,
92    /// Number of accounts involved.
93    pub accounts_len: usize,
94}
95
96/// Retrieve a previously processed sibling instruction from the current
97/// transaction.
98///
99/// `index` is 0-based: 0 = most recently processed instruction before
100/// the current one, 1 = the one before that, etc.
101///
102/// Returns `None` if no instruction exists at that index.
103///
104/// # Use case: Ed25519 signature verification
105///
106/// To verify an Ed25519 signature on-chain:
107/// 1. The transaction includes an instruction to the Ed25519 precompile
108///    with the message, signature, and public key.
109/// 2. Your program calls `get_processed_instruction(0)` to read that
110///    instruction.
111/// 3. Verify the program_id is the Ed25519 precompile address.
112/// 4. Parse the instruction data to extract the verified message.
113#[inline]
114pub fn get_processed_instruction(index: u64) -> Option<ProcessedInstruction> {
115    #[cfg(target_os = "solana")]
116    {
117        let mut meta = ProcessedInstructionMeta {
118            data_len: 0,
119            accounts_len: 0,
120        };
121        let mut program_id = Address::default();
122        let mut data = [0u8; 1232];
123        // SolAccountMeta is 34 bytes each (32-byte pubkey + 2 bools).
124        // Max accounts per instruction is ~64, so 64 * 34 = 2176 bytes.
125        let mut accounts_buf = [0u8; 2176];
126
127        // The syscall populates meta first to indicate required buffer sizes,
128        // then fills the buffers.
129        meta.data_len = data.len() as u64;
130        meta.accounts_len = (accounts_buf.len() / 34) as u64;
131
132        let rc = unsafe {
133            crate::syscalls::sol_get_processed_sibling_instruction(
134                index,
135                &mut meta as *mut ProcessedInstructionMeta as *mut u8,
136                program_id.0.as_mut_ptr(),
137                data.as_mut_ptr(),
138                accounts_buf.as_mut_ptr(),
139            )
140        };
141
142        if rc != 0 {
143            return None;
144        }
145
146        Some(ProcessedInstruction {
147            program_id,
148            data,
149            data_len: meta.data_len as usize,
150            accounts_len: meta.accounts_len as usize,
151        })
152    }
153    #[cfg(not(target_os = "solana"))]
154    {
155        let _ = index;
156        None
157    }
158}
159
160/// Well-known precompile address for Ed25519 signature verification.
161pub const ED25519_PROGRAM_ID: Address =
162    crate::address!("Ed25519SigVerify111111111111111111111111111");
163
164/// Well-known precompile address for Secp256k1 signature recovery.
165pub const SECP256K1_PROGRAM_ID: Address =
166    crate::address!("KeccakSecp256k11111111111111111111111111111");
167
168/// Check that a previous sibling instruction was to the Ed25519 precompile.
169///
170/// Returns the instruction data from the Ed25519 precompile instruction.
171/// The caller should parse this data to extract the verified message,
172/// public key, and signature.
173///
174/// `sibling_index` is 0 for the most recent sibling, 1 for the one before, etc.
175#[inline]
176pub fn require_ed25519_instruction(
177    sibling_index: u64,
178) -> Result<ProcessedInstruction, ProgramError> {
179    let ix = get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
180
181    if !crate::address::address_eq(&ix.program_id, &ED25519_PROGRAM_ID) {
182        return Err(ProgramError::IncorrectProgramId);
183    }
184
185    Ok(ix)
186}
187
188/// Check that a previous sibling instruction was to the Secp256k1 precompile.
189#[inline]
190pub fn require_secp256k1_instruction(
191    sibling_index: u64,
192) -> Result<ProcessedInstruction, ProgramError> {
193    let ix = get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
194
195    if !crate::address::address_eq(&ix.program_id, &SECP256K1_PROGRAM_ID) {
196        return Err(ProgramError::IncorrectProgramId);
197    }
198
199    Ok(ix)
200}
201
202// ---- Internal types for syscall FFI ----------------------------------
203
204#[repr(C)]
205#[allow(dead_code)]
206struct ProcessedInstructionMeta {
207    data_len: u64,
208    accounts_len: u64,
209}