Skip to main content

hopper_native/
introspect.rs

1//! Instruction introspection -- stack height and sibling instruction access.
2//!
3//! These wrappers support security patterns based on transaction and call-stack
4//! introspection:
5//!
6//! - **CPI guard**: Detect if the current instruction is running inside a CPI
7//!   call (stack height > 1). Prevents unauthorized composition -- e.g., a
8//!   governance instruction that must be top-level only.
9//!
10//! - **Ed25519 signature verification**: Check that a previous instruction in
11//!   the same transaction was to the Ed25519 precompile with specific data.
12//!   This is how on-chain programs verify off-chain signatures without heavy
13//!   crypto libraries.
14//!
15//! - **Secp256k1 recovery**: Same pattern for Ethereum-compatible signatures.
16//!
17//! Hopper wraps these syscalls behind small typed helpers so programs do not
18//! need to repeat raw unsafe glue at every call site.
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 reviewed 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 reviewed 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/// Well-known precompile address for Secp256r1 (P-256) signature
171/// verification (SIMD-0075). This is the precompile that backs passkey /
172/// WebAuthn signature checks on Solana.
173pub const SECP256R1_PROGRAM_ID: Address =
174    crate::address!("Secp256r1SigVerify1111111111111111111111111");
175
176/// Check that a previous sibling instruction was to the Ed25519 precompile.
177///
178/// Returns the instruction data from the Ed25519 precompile instruction.
179/// The caller should parse this data to extract the verified message,
180/// public key, and signature.
181///
182/// `sibling_index` is 0 for the most recent sibling, 1 for the one before, etc.
183#[inline]
184pub fn require_ed25519_instruction(
185    sibling_index: u64,
186) -> Result<ProcessedInstruction, ProgramError> {
187    let ix = get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
188
189    if !crate::address::address_eq(&ix.program_id, &ED25519_PROGRAM_ID) {
190        return Err(ProgramError::IncorrectProgramId);
191    }
192
193    Ok(ix)
194}
195
196/// Check that a previous sibling instruction was to the Secp256k1 precompile.
197#[inline]
198pub fn require_secp256k1_instruction(
199    sibling_index: u64,
200) -> Result<ProcessedInstruction, ProgramError> {
201    let ix = get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
202
203    if !crate::address::address_eq(&ix.program_id, &SECP256K1_PROGRAM_ID) {
204        return Err(ProgramError::IncorrectProgramId);
205    }
206
207    Ok(ix)
208}
209
210/// Check that a previous sibling instruction was to the Secp256r1
211/// (P-256) precompile, the verification path for passkeys / WebAuthn.
212///
213/// Returns the precompile instruction data, which the caller parses to
214/// bind the verified `(public_key, message, signature)` to the
215/// program's own authorization logic. Pairing this introspection check
216/// with the precompile is how a program proves a passkey signed a
217/// specific payload without trusting the client.
218///
219/// `sibling_index` is 0 for the most recent sibling, 1 for the one
220/// before, etc.
221#[inline]
222pub fn require_secp256r1_instruction(
223    sibling_index: u64,
224) -> Result<ProcessedInstruction, ProgramError> {
225    let ix = get_processed_instruction(sibling_index).ok_or(ProgramError::InvalidArgument)?;
226
227    if !crate::address::address_eq(&ix.program_id, &SECP256R1_PROGRAM_ID) {
228        return Err(ProgramError::IncorrectProgramId);
229    }
230
231    Ok(ix)
232}
233
234// ---- Internal types for syscall FFI ----------------------------------
235
236#[repr(C)]
237#[allow(dead_code)]
238struct ProcessedInstructionMeta {
239    data_len: u64,
240    accounts_len: u64,
241}