Skip to main content

hopper_native/
instruction.rs

1//! CPI instruction types: InstructionView, InstructionAccount, Seed, Signer.
2//!
3//! These types match the Solana runtime's C ABI for cross-program invocation.
4//! Matching the C descriptor ABI does not make Rust types interchangeable with
5//! types defined in other crates. Construct Hopper descriptors explicitly.
6
7use crate::account_view::AccountView;
8use crate::address::Address;
9use crate::error::ProgramError;
10use crate::raw_account::RuntimeAccount;
11use crate::{ProgramResult, NOT_BORROWED};
12use core::marker::PhantomData;
13
14// ── InstructionAccount ───────────────────────────────────────────────
15
16/// Metadata for an account referenced in a CPI instruction.
17#[repr(C)]
18#[derive(Debug, Clone)]
19pub struct InstructionAccount<'a> {
20    /// Public key of the account.
21    pub address: &'a Address,
22    /// Whether the account should be writable.
23    pub is_writable: bool,
24    /// Whether the account should sign.
25    pub is_signer: bool,
26}
27
28impl<'a> InstructionAccount<'a> {
29    /// Construct with explicit flags.
30    #[inline(always)]
31    pub const fn new(address: &'a Address, is_writable: bool, is_signer: bool) -> Self {
32        Self {
33            address,
34            is_writable,
35            is_signer,
36        }
37    }
38
39    /// Read-only, non-signer.
40    #[inline(always)]
41    pub const fn readonly(address: &'a Address) -> Self {
42        Self {
43            address,
44            is_writable: false,
45            is_signer: false,
46        }
47    }
48
49    /// Writable, non-signer.
50    #[inline(always)]
51    pub const fn writable(address: &'a Address) -> Self {
52        Self {
53            address,
54            is_writable: true,
55            is_signer: false,
56        }
57    }
58
59    /// Read-only signer.
60    #[inline(always)]
61    pub const fn readonly_signer(address: &'a Address) -> Self {
62        Self {
63            address,
64            is_writable: false,
65            is_signer: true,
66        }
67    }
68
69    /// Writable signer.
70    #[inline(always)]
71    pub const fn writable_signer(address: &'a Address) -> Self {
72        Self {
73            address,
74            is_writable: true,
75            is_signer: true,
76        }
77    }
78}
79
80impl<'a> From<&'a AccountView<'a>> for InstructionAccount<'a> {
81    #[inline(always)]
82    fn from(view: &'a AccountView<'a>) -> Self {
83        Self {
84            address: view.address(),
85            is_writable: view.is_writable(),
86            is_signer: view.is_signer(),
87        }
88    }
89}
90
91// ── InstructionView ──────────────────────────────────────────────────
92
93/// A cross-program instruction to invoke.
94#[derive(Debug, Clone)]
95pub struct InstructionView<'a, 'b, 'c, 'd>
96where
97    'a: 'b,
98{
99    /// Program to call.
100    pub program_id: &'c Address,
101    /// Instruction data.
102    pub data: &'d [u8],
103    /// Account metadata.
104    pub accounts: &'b [InstructionAccount<'a>],
105}
106
107// ── CpiAccount ───────────────────────────────────────────────────────
108
109/// C-ABI account info passed to `sol_invoke_signed_c`.
110///
111/// This matches the Solana runtime's expected layout for CPI account infos.
112#[repr(C)]
113#[derive(Clone, Copy, Debug)]
114pub struct CpiAccount<'a> {
115    address: *const Address,
116    lamports: *const u64,
117    data_len: u64,
118    data: *const u8,
119    owner: *const Address,
120    rent_epoch: u64,
121    is_signer: bool,
122    is_writable: bool,
123    executable: bool,
124    _account_view: PhantomData<&'a AccountView<'a>>,
125}
126
127// The flag bytes are copied as one word (see `From<&AccountView>` below):
128// the three `bool` fields must be adjacent, in header order, with a
129// padding byte after them for the fourth byte of the word.
130const _: () = {
131    use core::mem::{offset_of, size_of};
132    let signer = offset_of!(CpiAccount<'static>, is_signer);
133    assert!(offset_of!(CpiAccount<'static>, is_writable) == signer + 1);
134    assert!(offset_of!(CpiAccount<'static>, executable) == signer + 2);
135    assert!(signer + 4 <= size_of::<CpiAccount<'static>>());
136};
137
138impl<'a> From<&'a AccountView<'a>> for CpiAccount<'a> {
139    #[inline(always)]
140    fn from(view: &'a AccountView<'a>) -> Self {
141        let raw = view.account_ptr();
142        let mut out = core::mem::MaybeUninit::<Self>::uninit();
143        let slot = out.as_mut_ptr();
144        // The loader writes `is_signer`, `is_writable` and `executable` as 0
145        // or 1 into header bytes 1..4, which are the byte values of `bool`,
146        // and the three fields sit together here in the same order, so one
147        // 4-byte copy moves all three (the fourth byte lands in this
148        // struct's padding). Turning each flag into a `bool` separately
149        // costs a branch per flag on SBF, which has no set-on-condition
150        // instruction.
151        // SAFETY: `raw` is the view's live header; `addr_of!` takes field
152        // addresses without forming references. Every field of `slot` is
153        // written before `assume_init`: the five pointers and two integers
154        // one by one, the three flags by the word copy, whose bytes are 0 or
155        // 1 as the loader wrote them, valid `bool`s.
156        unsafe {
157            core::ptr::addr_of_mut!((*slot).address).write(core::ptr::addr_of!((*raw).address));
158            core::ptr::addr_of_mut!((*slot).lamports).write(core::ptr::addr_of!((*raw).lamports));
159            core::ptr::addr_of_mut!((*slot).data_len).write(view.data_len() as u64);
160            core::ptr::addr_of_mut!((*slot).data).write(view.data_ptr_unchecked());
161            core::ptr::addr_of_mut!((*slot).owner).write(core::ptr::addr_of!((*raw).owner));
162            core::ptr::addr_of_mut!((*slot).rent_epoch).write(0);
163            let flags = core::ptr::read_unaligned((raw as *const u8).add(1) as *const u32);
164            (core::ptr::addr_of_mut!((*slot).is_signer) as *mut u32).write_unaligned(flags);
165            out.assume_init()
166        }
167    }
168}
169
170impl<'a> CpiAccount<'a> {
171    /// Rebuild one instruction meta from protocol-declared flags.
172    ///
173    /// The flags deliberately do not come from the outer account view: a PDA
174    /// may be a signer only for this CPI, and an outer-writable account may be
175    /// intentionally read-only to the callee.
176    #[inline(always)]
177    pub(crate) fn instruction_account(
178        &self,
179        is_writable: bool,
180        is_signer: bool,
181    ) -> InstructionAccount<'a> {
182        // SAFETY: `CpiAccount::from` captured this pointer from an
183        // `AccountView<'a>` and the private fields prevent safe fabrication.
184        let address = unsafe { &*self.address };
185        InstructionAccount::new(address, is_writable, is_signer)
186    }
187}
188
189/// Validate the borrow state and writable privilege encoded by specialized
190/// CPI builders before entering a syscall.
191///
192/// `writable_mask` describes the callee instruction metas, not the outer
193/// transaction privileges: bit `i` is set when account `i` will be writable
194/// in the CPI. Read-only metas need shared-borrow compatibility; writable
195/// metas need both outer writable privilege and exclusive-borrow compatibility.
196#[inline(always)]
197pub(crate) fn preflight_cpi_accounts(
198    accounts: &[CpiAccount<'_>],
199    writable_mask: usize,
200    signer_mask: usize,
201    has_pda_signers: bool,
202) -> ProgramResult {
203    let mut index = 0usize;
204    while index < accounts.len() {
205        let account = &accounts[index];
206        // With no PDA signer seeds, a missing outer signature cannot be
207        // satisfied by the runtime. Match the generic checked invoke path.
208        // Nonempty seeds are NOT proof of authority: the SVM derives and
209        // authenticates the instruction's PDA signers at the syscall boundary.
210        if signer_mask & (1usize << index) != 0 && !account.is_signer && !has_pda_signers {
211            return Err(ProgramError::MissingRequiredSignature);
212        }
213        let is_writable_meta = writable_mask & (1usize << index) != 0;
214        if is_writable_meta && !account.is_writable {
215            return Err(ProgramError::Immutable);
216        }
217
218        // `CpiAccount::from` always derives `data` from the byte immediately
219        // after its RuntimeAccount header. The fields are private, so safe
220        // callers cannot synthesize a CpiAccount with a different relation.
221        let raw = unsafe { account.data.sub(RuntimeAccount::SIZE) as *const RuntimeAccount };
222        // SAFETY: `raw` was recovered from the invariant above and remains
223        // valid for the `CpiAccount` lifetime.
224        let borrow_state = unsafe { (*raw).borrow_state };
225        let compatible = if is_writable_meta {
226            borrow_state == NOT_BORROWED
227        } else {
228            borrow_state != 0
229        };
230        if !compatible {
231            return Err(ProgramError::AccountBorrowFailed);
232        }
233
234        index += 1;
235    }
236    Ok(())
237}
238
239// Pin the two C structures handed to `sol_invoke_signed_c`. Rust `bool` is one
240// byte, matching the syscall ABI's byte flags; the tail padding rounds each
241// record to pointer alignment.
242const _: () = {
243    assert!(core::mem::size_of::<InstructionAccount<'static>>() == 16);
244    assert!(core::mem::align_of::<InstructionAccount<'static>>() == 8);
245    assert!(core::mem::offset_of!(InstructionAccount<'static>, address) == 0);
246    assert!(core::mem::offset_of!(InstructionAccount<'static>, is_writable) == 8);
247    assert!(core::mem::offset_of!(InstructionAccount<'static>, is_signer) == 9);
248
249    assert!(core::mem::size_of::<CpiAccount<'static>>() == 56);
250    assert!(core::mem::align_of::<CpiAccount<'static>>() == 8);
251    assert!(core::mem::offset_of!(CpiAccount<'static>, address) == 0);
252    assert!(core::mem::offset_of!(CpiAccount<'static>, lamports) == 8);
253    assert!(core::mem::offset_of!(CpiAccount<'static>, data_len) == 16);
254    assert!(core::mem::offset_of!(CpiAccount<'static>, data) == 24);
255    assert!(core::mem::offset_of!(CpiAccount<'static>, owner) == 32);
256    assert!(core::mem::offset_of!(CpiAccount<'static>, rent_epoch) == 40);
257    assert!(core::mem::offset_of!(CpiAccount<'static>, is_signer) == 48);
258    assert!(core::mem::offset_of!(CpiAccount<'static>, is_writable) == 49);
259    assert!(core::mem::offset_of!(CpiAccount<'static>, executable) == 50);
260};
261
262// ── Seed ─────────────────────────────────────────────────────────────
263
264/// A single PDA seed for CPI signing.
265#[repr(C)]
266#[derive(Debug, Clone)]
267pub struct Seed<'a> {
268    pub(crate) seed: *const u8,
269    pub(crate) len: u64,
270    _bytes: PhantomData<&'a [u8]>,
271}
272
273impl<'a> From<&'a [u8]> for Seed<'a> {
274    #[inline(always)]
275    fn from(bytes: &'a [u8]) -> Self {
276        Self {
277            seed: bytes.as_ptr(),
278            len: bytes.len() as u64,
279            _bytes: PhantomData,
280        }
281    }
282}
283
284impl<'a, const N: usize> From<&'a [u8; N]> for Seed<'a> {
285    #[inline(always)]
286    fn from(bytes: &'a [u8; N]) -> Self {
287        Self {
288            seed: bytes.as_ptr(),
289            len: N as u64,
290            _bytes: PhantomData,
291        }
292    }
293}
294
295impl core::ops::Deref for Seed<'_> {
296    type Target = [u8];
297
298    #[inline(always)]
299    fn deref(&self) -> &[u8] {
300        // SAFETY: `seed` and `len` were taken from one `&'a [u8]` in the
301        // constructor, and the `PhantomData` ties `self` to that borrow.
302        unsafe { core::slice::from_raw_parts(self.seed, self.len as usize) }
303    }
304}
305
306// ── Signer ───────────────────────────────────────────────────────────
307
308/// A PDA signer: a set of seeds that derive the signing PDA.
309#[repr(C)]
310#[derive(Debug, Clone)]
311pub struct Signer<'a, 'b> {
312    pub(crate) seeds: *const Seed<'a>,
313    pub(crate) len: u64,
314    _seeds: PhantomData<&'b [Seed<'a>]>,
315}
316
317impl<'a, 'b> From<&'b [Seed<'a>]> for Signer<'a, 'b> {
318    #[inline(always)]
319    fn from(seeds: &'b [Seed<'a>]) -> Self {
320        Self {
321            seeds: seeds.as_ptr(),
322            len: seeds.len() as u64,
323            _seeds: PhantomData,
324        }
325    }
326}
327
328impl<'a, 'b, const N: usize> From<&'b [Seed<'a>; N]> for Signer<'a, 'b> {
329    #[inline(always)]
330    fn from(seeds: &'b [Seed<'a>; N]) -> Self {
331        Self {
332            seeds: seeds.as_ptr(),
333            len: N as u64,
334            _seeds: PhantomData,
335        }
336    }
337}
338
339/// Convenience macro for building an array of `Seed` from expressions.
340///
341/// Usage: `let seeds = seeds!(b"vault", mint_key.as_ref(), &[bump]);`
342#[macro_export]
343macro_rules! seeds {
344    ( $($seed:expr),* $(,)? ) => {
345        [$(
346            $crate::instruction::Seed::from($seed),
347        )*]
348    };
349}