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