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}