Skip to main content

hopper_runtime/
pda.rs

1//! Hopper-owned PDA ergonomics on top of the active backend substrate.
2
3use crate::address::Address;
4use crate::error::ProgramError;
5use crate::AccountView;
6
7/// Create a program-derived address from seeds and a program ID.
8///
9/// Returns `Err(InvalidSeeds)` if the derived address falls on the
10/// ed25519 curve (not a valid PDA).
11#[inline]
12pub fn create_program_address(
13    seeds: &[&[u8]],
14    program_id: &Address,
15) -> Result<Address, ProgramError> {
16    crate::compat::create_program_address(seeds, program_id)
17}
18
19/// Find a program-derived address and its bump seed.
20///
21/// Iterates bump seeds 255..=0 until a valid PDA is found.
22#[inline]
23pub fn find_program_address(seeds: &[&[u8]], program_id: &Address) -> (Address, u8) {
24    #[cfg(target_os = "solana")]
25    {
26        crate::compat::find_program_address(seeds, program_id)
27    }
28    #[cfg(not(target_os = "solana"))]
29    {
30        let _ = (seeds, program_id);
31        (Address::default(), 0)
32    }
33}
34
35/// Hopper-facing alias for PDA derivation.
36#[inline(always)]
37pub fn derive(seeds: &[&[u8]], program_id: &Address) -> (Address, u8) {
38    find_program_address(seeds, program_id)
39}
40
41/// Verify that an account's address matches a PDA derived from the given seeds.
42#[inline]
43pub fn verify_pda(
44    account: &AccountView,
45    seeds: &[&[u8]],
46    program_id: &Address,
47) -> Result<(), ProgramError> {
48    #[cfg(all(target_os = "solana", feature = "hopper-native-backend"))]
49    {
50        hopper_native::pda::verify_pda(
51            account.as_backend(),
52            seeds,
53            crate::compat::as_backend_address(program_id),
54        )
55        .map_err(ProgramError::from)
56    }
57
58    #[cfg(not(all(target_os = "solana", feature = "hopper-native-backend")))]
59    {
60        let expected = create_program_address(seeds, program_id)?;
61        if crate::address::address_eq(account.address(), &expected) {
62            Ok(())
63        } else {
64            Err(ProgramError::InvalidSeeds)
65        }
66    }
67}
68
69/// Verify a PDA with an explicit bump seed appended to the seeds.
70#[inline]
71pub fn verify_pda_with_bump(
72    account: &AccountView,
73    seeds: &[&[u8]],
74    bump: u8,
75    program_id: &Address,
76) -> Result<(), ProgramError> {
77    #[cfg(all(target_os = "solana", feature = "hopper-native-backend"))]
78    {
79        hopper_native::pda::verify_pda_with_bump(
80            account.as_backend(),
81            seeds,
82            bump,
83            crate::compat::as_backend_address(program_id),
84        )
85        .map_err(ProgramError::from)
86    }
87
88    #[cfg(not(all(target_os = "solana", feature = "hopper-native-backend")))]
89    {
90        let mut full_seeds: [&[u8]; 17] = [&[]; 17];
91        let num = seeds.len().min(15);
92        let mut i = 0;
93        while i < num {
94            full_seeds[i] = seeds[i];
95            i += 1;
96        }
97        let bump_bytes = [bump];
98        full_seeds[num] = &bump_bytes;
99
100        let expected = create_program_address(&full_seeds[..num + 1], program_id)?;
101        if crate::address::address_eq(account.address(), &expected) {
102            Ok(())
103        } else {
104            Err(ProgramError::InvalidSeeds)
105        }
106    }
107}
108
109/// Verify that an account matches a PDA derived from the given seeds.
110///
111/// **Verify-only approach**: iterates bumps 255→0 using `sol_sha256` only -
112/// no `sol_curve_validate_point` needed because we compare each hash directly
113/// against the known PDA address. This saves ~159 CU per attempt compared to
114/// the standard `find_program_address` approach (sha256+curve_validate).
115///
116/// Average cost: ~200 CU for bump=255, ~400 CU for bump=254, etc.
117/// Standard find_program_address: ~544 CU per attempt.
118///
119/// Returns the bump seed on success.
120#[inline]
121pub fn find_and_verify_pda(
122    account: &AccountView,
123    seeds: &[&[u8]],
124    program_id: &Address,
125) -> Result<u8, ProgramError> {
126    #[cfg(all(target_os = "solana", feature = "hopper-native-backend"))]
127    {
128        let expected_addr = account.as_backend().address();
129        let backend_expected =
130            // 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.
131            unsafe { &*(expected_addr as *const hopper_native::address::Address) };
132        verify_pda_sha256_loop(backend_expected, seeds, program_id)
133    }
134
135    #[cfg(not(all(target_os = "solana", feature = "hopper-native-backend")))]
136    {
137        let (expected, bump) = find_program_address(seeds, program_id);
138        if crate::address::address_eq(account.address(), &expected) {
139            Ok(bump)
140        } else {
141            Err(ProgramError::InvalidSeeds)
142        }
143    }
144}
145
146/// Verify that a raw address matches a PDA derived from the given seeds.
147///
148/// Uses the same verify-only sha256 loop as `find_and_verify_pda`.
149#[inline]
150pub fn verify_pda_strict(
151    expected: &Address,
152    seeds: &[&[u8]],
153    program_id: &Address,
154) -> Result<(), ProgramError> {
155    #[cfg(all(target_os = "solana", feature = "hopper-native-backend"))]
156    {
157        let backend_expected =
158            // 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.
159            unsafe { &*(expected as *const Address as *const hopper_native::address::Address) };
160        verify_pda_sha256_loop(backend_expected, seeds, program_id).map(|_| ())
161    }
162
163    #[cfg(not(all(target_os = "solana", feature = "hopper-native-backend")))]
164    {
165        let (derived, _) = find_program_address(seeds, program_id);
166        if crate::address::address_eq(&derived, expected) {
167            Ok(())
168        } else {
169            Err(ProgramError::InvalidSeeds)
170        }
171    }
172}
173
174/// Shared sha256-only PDA verify loop used by both `find_and_verify_pda`
175/// and `verify_pda_strict`.
176///
177/// Iterates bumps 255→0, hashing seeds + bump + program_id + PDA_MARKER.
178/// Returns the matching bump on success.
179#[cfg(all(target_os = "solana", feature = "hopper-native-backend"))]
180#[inline(always)]
181fn verify_pda_sha256_loop(
182    expected: &hopper_native::address::Address,
183    seeds: &[&[u8]],
184    program_id: &Address,
185) -> Result<u8, ProgramError> {
186    let backend_pid = crate::compat::as_backend_address(program_id);
187    let n = seeds.len().min(16);
188    let mut slices = core::mem::MaybeUninit::<[&[u8]; 19]>::uninit();
189    let sptr = slices.as_mut_ptr() as *mut &[u8];
190    let mut i = 0;
191    while i < n {
192        // 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.
193        unsafe { sptr.add(i).write(seeds[i]) };
194        i += 1;
195    }
196    let mut bump_byte = [255u8];
197    // 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.
198    unsafe {
199        sptr.add(n).write(&bump_byte as &[u8]);
200        sptr.add(n + 1).write(backend_pid.as_ref());
201        sptr.add(n + 2)
202            .write(hopper_native::address::PDA_MARKER.as_slice());
203    }
204    // 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.
205    let input = unsafe { core::slice::from_raw_parts(sptr as *const &[u8], n + 3) };
206
207    let mut bump: u16 = 256;
208    while bump > 0 {
209        bump -= 1;
210        bump_byte[0] = bump as u8;
211
212        let mut hash = core::mem::MaybeUninit::<[u8; 32]>::uninit();
213        // 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.
214        unsafe {
215            hopper_native::syscalls::sol_sha256(
216                input as *const _ as *const u8,
217                input.len() as u64,
218                hash.as_mut_ptr() as *mut u8,
219            );
220        }
221        let derived =
222            // 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.
223            unsafe { &*(hash.as_ptr() as *const hopper_native::address::Address) };
224        if hopper_native::address::address_eq(derived, expected) {
225            return Ok(bump as u8);
226        }
227    }
228
229    Err(ProgramError::InvalidSeeds)
230}
231
232/// Verify a PDA using the bump stored in account data (cheapest path).
233///
234/// Reads the bump byte at `bump_offset` in account data, appends it to seeds,
235/// then hashes with SHA-256 and compares to the account address. ~200 CU total.
236#[inline]
237pub fn verify_pda_from_stored_bump(
238    account: &AccountView,
239    seeds: &[&[u8]],
240    bump_offset: usize,
241    program_id: &Address,
242) -> Result<(), ProgramError> {
243    #[cfg(all(target_os = "solana", feature = "hopper-native-backend"))]
244    {
245        hopper_native::verify_pda_from_stored_bump(
246            account.as_backend(),
247            seeds,
248            bump_offset,
249            crate::compat::as_backend_address(program_id),
250        )
251        .map_err(ProgramError::from)
252    }
253
254    #[cfg(not(all(target_os = "solana", feature = "hopper-native-backend")))]
255    {
256        // Off-chain fallback: read bump, append to seeds, derive + compare.
257        let data = account.try_borrow()?;
258        if bump_offset >= data.len() {
259            return Err(ProgramError::AccountDataTooSmall);
260        }
261        let bump = data[bump_offset];
262        let mut full_seeds: [&[u8]; 17] = [&[]; 17];
263        let num = seeds.len().min(15);
264        let mut i = 0;
265        while i < num {
266            full_seeds[i] = seeds[i];
267            i += 1;
268        }
269        let bump_bytes = [bump];
270        full_seeds[num] = &bump_bytes;
271
272        let expected = create_program_address(&full_seeds[..num + 1], program_id)?;
273        if crate::address::address_eq(account.address(), &expected) {
274            Ok(())
275        } else {
276            Err(ProgramError::InvalidSeeds)
277        }
278    }
279}