Skip to main content

hopper_native/
pda.rs

1//! PDA (Program Derived Address) helpers.
2//!
3//! Direct syscall-based PDA creation and derivation. No external dependencies.
4
5use crate::account_view::AccountView;
6use crate::address::{Address, MAX_SEEDS};
7use crate::error::ProgramError;
8
9#[cfg(target_os = "solana")]
10const CURVE25519_EDWARDS: u64 = 0;
11#[cfg(target_os = "solana")]
12const PDA_MARKER_BYTES: &[u8; 21] = crate::address::PDA_MARKER;
13
14/// Create a program-derived address from seeds and a program ID.
15///
16/// Returns `Err(InvalidSeeds)` if the derived address falls on the
17/// ed25519 curve (not a valid PDA).
18#[inline(always)]
19pub fn create_program_address(
20    seeds: &[&[u8]],
21    program_id: &Address,
22) -> Result<Address, ProgramError> {
23    #[cfg(target_os = "solana")]
24    {
25        // Build the seeds array in the format expected by the syscall:
26        // each seed is a (ptr, len) pair packed as two u64 values.
27        let mut seed_buf: [u64; 32] = [0; 32]; // MAX_SEEDS * 2
28        let num_seeds = seeds.len().min(16);
29        let mut i = 0;
30        while i < num_seeds {
31            seed_buf[i * 2] = seeds[i].as_ptr() as u64;
32            seed_buf[i * 2 + 1] = seeds[i].len() as u64;
33            i += 1;
34        }
35
36        let mut result = Address::default();
37        // 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.
38        let rc = unsafe {
39            crate::syscalls::sol_create_program_address(
40                seed_buf.as_ptr() as *const u8,
41                num_seeds as u64,
42                program_id.as_array().as_ptr(),
43                result.0.as_mut_ptr(),
44            )
45        };
46        if rc == 0 {
47            Ok(result)
48        } else {
49            Err(ProgramError::InvalidSeeds)
50        }
51    }
52    #[cfg(not(target_os = "solana"))]
53    {
54        let _ = (seeds, program_id);
55        Err(ProgramError::InvalidSeeds)
56    }
57}
58
59/// Find a program-derived address and its bump seed.
60///
61/// Iterates bump seeds 255..=0 until a valid PDA is found.
62#[inline(always)]
63pub fn find_program_address(seeds: &[&[u8]], program_id: &Address) -> (Address, u8) {
64    #[cfg(target_os = "solana")]
65    {
66        based_try_find_program_address(seeds, program_id).unwrap_or((Address::default(), 0))
67    }
68    #[cfg(not(target_os = "solana"))]
69    {
70        let _ = (seeds, program_id);
71        (Address::default(), 0)
72    }
73}
74
75/// Verify that an expected address matches the PDA hash for the provided seeds.
76///
77/// The seeds slice must already include the bump byte.
78#[inline(always)]
79pub fn verify_program_address(
80    seeds: &[&[u8]],
81    program_id: &Address,
82    expected: &Address,
83) -> Result<(), ProgramError> {
84    if seeds.len() > MAX_SEEDS + 1 {
85        return Err(ProgramError::InvalidSeeds);
86    }
87
88    #[cfg(target_os = "solana")]
89    {
90        let n = seeds.len();
91        let mut slices = core::mem::MaybeUninit::<[&[u8]; MAX_SEEDS + 3]>::uninit();
92        let slice_ptr = slices.as_mut_ptr() as *mut &[u8];
93
94        let mut i = 0;
95        while i < n {
96            // 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.
97            unsafe { slice_ptr.add(i).write(seeds[i]) };
98            i += 1;
99        }
100        // 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.
101        unsafe {
102            slice_ptr.add(n).write(program_id.as_ref());
103            slice_ptr.add(n + 1).write(PDA_MARKER_BYTES.as_slice());
104        }
105
106        // 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.
107        let input = unsafe { core::slice::from_raw_parts(slice_ptr, n + 2) };
108        let mut hash = core::mem::MaybeUninit::<[u8; 32]>::uninit();
109
110        // 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.
111        unsafe {
112            crate::syscalls::sol_sha256(
113                input as *const _ as *const u8,
114                input.len() as u64,
115                hash.as_mut_ptr() as *mut u8,
116            );
117        }
118
119        // 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.
120        let derived = unsafe { &*(hash.as_ptr() as *const Address) };
121        if derived == expected {
122            Ok(())
123        } else {
124            Err(ProgramError::InvalidSeeds)
125        }
126    }
127    #[cfg(not(target_os = "solana"))]
128    {
129        let _ = (seeds, program_id, expected);
130        Err(ProgramError::InvalidSeeds)
131    }
132}
133
134/// Find a valid PDA by hashing seeds directly and checking curve validity.
135///
136/// This avoids the `sol_try_find_program_address` syscall and substantially
137/// reduces the per-attempt CU cost on SBF.
138#[inline(always)]
139pub fn based_try_find_program_address(
140    seeds: &[&[u8]],
141    program_id: &Address,
142) -> Result<(Address, u8), ProgramError> {
143    if seeds.len() > MAX_SEEDS {
144        return Err(ProgramError::InvalidSeeds);
145    }
146
147    #[cfg(target_os = "solana")]
148    {
149        let n = seeds.len();
150        let mut slices = core::mem::MaybeUninit::<[&[u8]; MAX_SEEDS + 3]>::uninit();
151        let slice_ptr = slices.as_mut_ptr() as *mut &[u8];
152
153        let mut i = 0;
154        while i < n {
155            // 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.
156            unsafe { slice_ptr.add(i).write(seeds[i]) };
157            i += 1;
158        }
159        // 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.
160        unsafe {
161            slice_ptr.add(n + 1).write(program_id.as_ref());
162            slice_ptr.add(n + 2).write(PDA_MARKER_BYTES.as_slice());
163        }
164
165        let mut bump_seed = [u8::MAX];
166        let bump_ptr = bump_seed.as_mut_ptr();
167        // 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.
168        unsafe {
169            slice_ptr
170                .add(n)
171                .write(core::slice::from_raw_parts(bump_ptr, 1))
172        };
173
174        // 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.
175        let input = unsafe { core::slice::from_raw_parts(slice_ptr, n + 3) };
176        let mut hash = core::mem::MaybeUninit::<[u8; 32]>::uninit();
177        let mut bump: u64 = u8::MAX as u64;
178
179        loop {
180            // 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.
181            unsafe { bump_ptr.write(bump as u8) };
182
183            unsafe {
184                crate::syscalls::sol_sha256(
185                    input as *const _ as *const u8,
186                    input.len() as u64,
187                    hash.as_mut_ptr() as *mut u8,
188                );
189            }
190
191            // 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.
192            let on_curve = unsafe {
193                crate::syscalls::sol_curve_validate_point(
194                    CURVE25519_EDWARDS,
195                    hash.as_ptr() as *const u8,
196                    core::ptr::null_mut(),
197                )
198            };
199
200            if on_curve != 0 {
201                return Ok((
202                    // 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.
203                    Address::new_from_array(unsafe { hash.assume_init() }),
204                    bump as u8,
205                ));
206            }
207
208            if bump == 0 {
209                break;
210            }
211            bump -= 1;
212        }
213
214        Err(ProgramError::InvalidSeeds)
215    }
216    #[cfg(not(target_os = "solana"))]
217    {
218        let _ = (seeds, program_id);
219        Err(ProgramError::InvalidSeeds)
220    }
221}
222
223/// Verify that an account's address matches a PDA derived from the given seeds.
224///
225/// Returns `Ok(())` if the account address matches the derived PDA,
226/// or `Err(InvalidSeeds)` if it does not.
227#[inline(always)]
228pub fn verify_pda(
229    account: &AccountView,
230    seeds: &[&[u8]],
231    program_id: &Address,
232) -> Result<(), ProgramError> {
233    let expected = create_program_address(seeds, program_id)?;
234    if account.address() == &expected {
235        Ok(())
236    } else {
237        Err(ProgramError::InvalidSeeds)
238    }
239}
240
241/// Verify a PDA with an explicit bump seed appended to the seeds.
242///
243/// Appends `&[bump]` to the end of the seed list before verifying via
244/// SHA-256 (~200 CU). This is substantially cheaper than the syscall-based
245/// `create_program_address` approach (~1500 CU).
246#[inline]
247pub fn verify_pda_with_bump(
248    account: &AccountView,
249    seeds: &[&[u8]],
250    bump: u8,
251    program_id: &Address,
252) -> Result<(), ProgramError> {
253    // Build a seed list with the bump appended.
254    // We use a stack-allocated array since MAX_SEEDS is 16.
255    let mut full_seeds: [&[u8]; 17] = [&[]; 17];
256    let num = seeds.len().min(15);
257    let mut i = 0;
258    while i < num {
259        full_seeds[i] = seeds[i];
260        i += 1;
261    }
262    let bump_bytes = [bump];
263    full_seeds[num] = &bump_bytes;
264
265    verify_program_address(&full_seeds[..num + 1], program_id, account.address())
266}
267
268/// Verify that an address matches a PDA derived from the given seeds.
269///
270/// Unlike `verify_pda` which takes an `AccountView`, this accepts a raw
271/// `Address` reference directly. Useful when validating addresses outside
272/// of the account parsing flow (e.g. instruction data, cross-program reads).
273///
274/// The seeds slice must already include the bump byte (like
275/// `verify_program_address`). Uses SHA-256 verify-only path (~200 CU)
276/// instead of the full `find_program_address` (~1500 CU).
277///
278/// Returns `Ok(())` if the address matches the derived PDA,
279/// or `Err(InvalidSeeds)` if it does not.
280#[inline]
281pub fn verify_pda_strict(
282    expected: &Address,
283    seeds: &[&[u8]],
284    program_id: &Address,
285) -> Result<(), ProgramError> {
286    verify_program_address(seeds, program_id, expected)
287}
288
289/// Find the bump seed for a known PDA address, skipping curve validation.
290///
291/// When you already know the expected address (e.g. from a transaction
292/// account), there is no need to validate the derived hash is off-curve.
293/// If the hash matches `expected` and the account exists on-chain, it
294/// must be a valid PDA. This saves ~90 CU per attempt compared to
295/// `based_try_find_program_address` which calls `sol_curve_validate_point`.
296///
297/// Returns the bump seed, or `Err(InvalidSeeds)` if no bump produces a match.
298#[inline(always)]
299pub fn find_bump_for_address(
300    seeds: &[&[u8]],
301    program_id: &Address,
302    expected: &Address,
303) -> Result<u8, ProgramError> {
304    if seeds.len() > MAX_SEEDS {
305        return Err(ProgramError::InvalidSeeds);
306    }
307
308    #[cfg(target_os = "solana")]
309    {
310        let n = seeds.len();
311        let mut slices = core::mem::MaybeUninit::<[&[u8]; MAX_SEEDS + 3]>::uninit();
312        let slice_ptr = slices.as_mut_ptr() as *mut &[u8];
313
314        let mut i = 0;
315        while i < n {
316            // 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.
317            unsafe { slice_ptr.add(i).write(seeds[i]) };
318            i += 1;
319        }
320        // 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.
321        unsafe {
322            slice_ptr.add(n + 1).write(program_id.as_ref());
323            slice_ptr.add(n + 2).write(PDA_MARKER_BYTES.as_slice());
324        }
325
326        let mut bump_seed = [u8::MAX];
327        let bump_ptr = bump_seed.as_mut_ptr();
328        // 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.
329        unsafe {
330            slice_ptr
331                .add(n)
332                .write(core::slice::from_raw_parts(bump_ptr, 1))
333        };
334
335        // 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.
336        let input = unsafe { core::slice::from_raw_parts(slice_ptr, n + 3) };
337        let mut hash = core::mem::MaybeUninit::<[u8; 32]>::uninit();
338        let mut bump: u64 = u8::MAX as u64;
339
340        loop {
341            // 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.
342            unsafe { bump_ptr.write(bump as u8) };
343
344            unsafe {
345                crate::syscalls::sol_sha256(
346                    input as *const _ as *const u8,
347                    input.len() as u64,
348                    hash.as_mut_ptr() as *mut u8,
349                );
350            }
351
352            // Address-match shortcut: skip curve check entirely.
353            // If the hash matches the expected address and that address
354            // exists on-chain, it is guaranteed to be a valid PDA.
355            // 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.
356            let derived = unsafe { &*(hash.as_ptr() as *const Address) };
357            if derived == expected {
358                return Ok(bump as u8);
359            }
360
361            if bump == 0 {
362                break;
363            }
364            bump -= 1;
365        }
366
367        Err(ProgramError::InvalidSeeds)
368    }
369    #[cfg(not(target_os = "solana"))]
370    {
371        let _ = (seeds, program_id, expected);
372        Err(ProgramError::InvalidSeeds)
373    }
374}
375
376/// Read the bump byte directly from account data at a known offset.
377///
378/// Used with `BUMP_OFFSET` from `hopper_layout!` types to read the stored
379/// bump without any derivation. Combined with `verify_program_address`,
380/// the total PDA verification cost is ~200 CU vs ~1500 CU for
381/// `find_program_address`.
382///
383/// Returns `Err(AccountDataTooSmall)` if the account data is shorter than
384/// `bump_offset + 1`.
385#[inline(always)]
386pub fn read_bump_from_account(
387    account: &AccountView,
388    bump_offset: usize,
389) -> Result<u8, ProgramError> {
390    let data = account.try_borrow()?;
391    if data.len() <= bump_offset {
392        return Err(ProgramError::AccountDataTooSmall);
393    }
394    Ok(data[bump_offset])
395}
396
397/// Verify a PDA using the bump stored in account data (cheapest path).
398///
399/// Reads the bump at `bump_offset`, appends it to seeds, then uses
400/// SHA-256 verify-only. Total cost: ~200 CU vs ~1500 CU.
401///
402/// This is the optimal PDA verification path and should be the default
403/// for Hopper programs that store bumps in their account layout.
404#[inline]
405pub fn verify_pda_from_stored_bump(
406    account: &AccountView,
407    seeds: &[&[u8]],
408    bump_offset: usize,
409    program_id: &Address,
410) -> Result<(), ProgramError> {
411    let bump = read_bump_from_account(account, bump_offset)?;
412
413    let mut full_seeds: [&[u8]; 17] = [&[]; 17];
414    let num = seeds.len().min(15);
415    let mut i = 0;
416    while i < num {
417        full_seeds[i] = seeds[i];
418        i += 1;
419    }
420    let bump_bytes = [bump];
421    full_seeds[num] = &bump_bytes;
422
423    verify_program_address(&full_seeds[..num + 1], program_id, account.address())
424}