hopper_native/return_data.rs
1//! CPI return data retrieval and typed deserialization.
2//!
3//! The Solana runtime supports return data from CPI calls (up to 1024 bytes).
4//! This module combines invocation, program-id validation, and typed return-data
5//! decoding.
6
7use crate::address::Address;
8use crate::error::ProgramError;
9use crate::project::Projectable;
10use core::mem::MaybeUninit;
11
12#[cfg(feature = "cpi")]
13use crate::instruction::{InstructionView, Signer};
14
15/// Maximum return data size (1 KiB), matching Solana runtime limit.
16pub const MAX_RETURN_DATA: usize = 1024;
17
18/// Return data from a previous CPI call.
19///
20/// The buffer is deliberately left uninitialized until the
21/// `sol_get_return_data` syscall fills it; only the syscall-initialized
22/// prefix (`len` bytes) is ever exposed to callers.
23pub struct ReturnData {
24 /// Buffer holding the return data (stack-allocated; only the first
25 /// `len` bytes are initialized).
26 buf: [MaybeUninit<u8>; MAX_RETURN_DATA],
27 /// Actual length of the return data.
28 len: usize,
29 /// Program ID that set the return data.
30 program_id: Address,
31}
32
33impl ReturnData {
34 /// Get the return data bytes.
35 #[inline(always)]
36 pub fn data(&self) -> &[u8] {
37 // Fail-closed backstop for the invariant the SAFETY comment relies
38 // on: `len` can never exceed the buffer capacity.
39 debug_assert!(self.len <= MAX_RETURN_DATA);
40 // SAFETY: `sol_get_return_data` initializes exactly
41 // `min(actual_len, MAX_RETURN_DATA)` bytes of the buffer it was
42 // handed, and `get_return_data` sets `len` to that same value (the
43 // test constructor likewise writes `len` bytes before setting it), so
44 // the first `len` bytes are always initialized `u8`s.
45 unsafe { core::slice::from_raw_parts(self.buf.as_ptr() as *const u8, self.len) }
46 }
47
48 /// Get the program that set the return data.
49 #[inline(always)]
50 pub fn program_id(&self) -> &Address {
51 &self.program_id
52 }
53
54 /// Length of the return data.
55 #[inline(always)]
56 pub fn len(&self) -> usize {
57 self.len
58 }
59
60 /// Whether the return data is empty.
61 #[inline(always)]
62 pub fn is_empty(&self) -> bool {
63 self.len == 0
64 }
65
66 /// Interpret the return data as a `Projectable` type.
67 ///
68 /// Returns `Err(AccountDataTooSmall)` if the return data is smaller
69 /// than `size_of::<T>()`.
70 #[inline]
71 pub fn as_type<T: Projectable>(&self) -> Result<&T, ProgramError> {
72 let size = core::mem::size_of::<T>();
73 if self.len < size {
74 return Err(ProgramError::AccountDataTooSmall);
75 }
76
77 let data = self.data();
78 let align = core::mem::align_of::<T>();
79 let ptr = data.as_ptr();
80 if !(ptr as usize).is_multiple_of(align) {
81 return Err(ProgramError::InvalidAccountData);
82 }
83
84 // SAFETY: `data` is the initialized `len`-byte prefix of the buffer,
85 // the length check above guarantees `len >= size_of::<T>()`, the
86 // alignment check guarantees `ptr` is aligned for `T`, and
87 // `T: Projectable` is valid for any initialized bit pattern.
88 Ok(unsafe { &*(ptr as *const T) })
89 }
90
91 /// Read a u64 from the first 8 bytes of return data.
92 #[inline]
93 pub fn as_u64(&self) -> Result<u64, ProgramError> {
94 if self.len < 8 {
95 return Err(ProgramError::AccountDataTooSmall);
96 }
97 let mut bytes = [0u8; 8];
98 bytes.copy_from_slice(&self.data()[..8]);
99 Ok(u64::from_le_bytes(bytes))
100 }
101
102 /// Read a u32 from the first 4 bytes of return data.
103 #[inline]
104 pub fn as_u32(&self) -> Result<u32, ProgramError> {
105 if self.len < 4 {
106 return Err(ProgramError::AccountDataTooSmall);
107 }
108 let mut bytes = [0u8; 4];
109 bytes.copy_from_slice(&self.data()[..4]);
110 Ok(u32::from_le_bytes(bytes))
111 }
112}
113
114/// Retrieve return data from the most recent CPI call.
115///
116/// Returns `None` if no return data was set (length == 0).
117///
118/// The 1 KiB buffer is *not* zero-filled before the syscall, the syscall
119/// initializes exactly the reported prefix, and `None` is returned before any
120/// read when the length is 0. This is the bug class behind Quasar #238/#234
121/// (an `assume_init` over a buffer the syscall never wrote, exposing
122/// uninitialized stack bytes as return data); Hopper's shape is immune
123/// because uninitialized bytes can never escape: empty return data (including
124/// the off-chain path, where `len` stays 0) short-circuits to `None`, and
125/// every accessor reads only the syscall-initialized `len`-byte prefix.
126#[inline]
127pub fn get_return_data() -> Option<ReturnData> {
128 #[allow(unused_mut)]
129 let mut rd = ReturnData {
130 buf: [const { MaybeUninit::uninit() }; MAX_RETURN_DATA],
131 len: 0,
132 program_id: Address::default(),
133 };
134
135 #[cfg(target_os = "solana")]
136 {
137 // SAFETY: The buffer and program-id pointers are stack-allocated with
138 // the exact capacities advertised to the runtime syscall; the buffer
139 // may be uninitialized because the syscall only writes (never reads)
140 // it.
141 let actual_len = unsafe {
142 crate::syscalls::sol_get_return_data(
143 rd.buf.as_mut_ptr() as *mut u8,
144 MAX_RETURN_DATA as u64,
145 rd.program_id.0.as_mut_ptr(),
146 )
147 };
148 rd.len = (actual_len as usize).min(MAX_RETURN_DATA);
149 }
150
151 #[cfg(not(target_os = "solana"))]
152 {
153 // Off-chain: no return data available; `len` stays 0 so the
154 // uninitialized buffer is discarded below without being read.
155 }
156
157 if rd.len == 0 {
158 None
159 } else {
160 Some(rd)
161 }
162}
163
164/// Invoke a CPI and immediately read back typed return data.
165///
166/// Combines `invoke_signed` + `get_return_data` + `as_type::<T>()` into
167/// a single operation. This is the cleanest way to call a program that
168/// returns structured data.
169///
170/// # Example
171///
172/// ```ignore
173/// let oracle_price: &PriceData = invoke_and_read::<PriceData, 2>(
174/// &instruction,
175/// &[&oracle_program, &price_feed],
176/// &[],
177/// )?;
178/// ```
179#[cfg(feature = "cpi")]
180#[inline]
181pub fn invoke_and_read<T: Projectable, const ACCOUNTS: usize>(
182 instruction: &InstructionView<'_, '_, '_, '_>,
183 account_views: &[&crate::account_view::AccountView<'_>; ACCOUNTS],
184 signers_seeds: &[Signer<'_, '_>],
185) -> Result<ReturnData, ProgramError> {
186 crate::cpi::invoke_signed::<ACCOUNTS>(instruction, account_views, signers_seeds)?;
187
188 get_return_data().ok_or(ProgramError::InvalidAccountData)
189}
190
191#[cfg(test)]
192impl ReturnData {
193 /// Test-only constructor: builds a snapshot whose buffer prefix is fully
194 /// initialized from `bytes`, mirroring what the syscall produces on-chain.
195 fn test_snapshot(bytes: &[u8], program_id: Address) -> Self {
196 assert!(bytes.len() <= MAX_RETURN_DATA);
197 let mut buf = [const { MaybeUninit::uninit() }; MAX_RETURN_DATA];
198 for (dst, src) in buf.iter_mut().zip(bytes) {
199 dst.write(*src);
200 }
201 ReturnData {
202 buf,
203 len: bytes.len(),
204 program_id,
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn offchain_get_return_data_is_none() {
215 assert!(get_return_data().is_none());
216 }
217
218 #[test]
219 fn data_exposes_exactly_the_written_prefix() {
220 let payload = [0xAB, 0xCD, 0xEF];
221 let rd = ReturnData::test_snapshot(&payload, Address::default());
222 assert_eq!(rd.data(), &payload);
223 assert_eq!(rd.len(), payload.len());
224 assert!(!rd.is_empty());
225 }
226
227 #[test]
228 fn as_u64_and_as_u32_never_read_past_the_prefix() {
229 let short = ReturnData::test_snapshot(&[1, 2, 3], Address::default());
230 assert!(short.as_u64().is_err());
231 assert!(short.as_u32().is_err());
232
233 let rd = ReturnData::test_snapshot(&7u64.to_le_bytes(), Address::default());
234 assert_eq!(rd.as_u64().unwrap(), 7);
235 assert_eq!(rd.as_u32().unwrap(), 7);
236 }
237
238 #[test]
239 fn as_type_length_checks_against_the_prefix() {
240 let rd = ReturnData::test_snapshot(&[5u8], Address::default());
241 assert!(rd.as_type::<u64>().is_err());
242 assert_eq!(*rd.as_type::<u8>().unwrap(), 5);
243 }
244}