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}