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, MAX_SEED_LEN};
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/// The PDA hash of `seeds` (every seed the caller passes, a bump included
15/// if there is one), `program_id`, and the marker. Off chain and in
16/// `const` evaluation; on chain the same bytes go through `sol_sha256`.
17const fn pda_hash(seeds: &[&[u8]], bump: Option<u8>, program_id: &Address) -> [u8; 32] {
18    let mut hasher = crate::sha256::ConstSha256::new();
19    let mut i = 0;
20    while i < seeds.len() {
21        hasher = hasher.update(seeds[i]);
22        i += 1;
23    }
24    if let Some(bump) = bump {
25        hasher = hasher.update(&[bump]);
26    }
27    hasher
28        .update(program_id.as_array())
29        .update(crate::address::PDA_MARKER)
30        .finalize()
31}
32
33/// The canonical program-derived address of `seeds` under `program_id`
34/// and its bump, found at compile time.
35///
36/// This is `find_program_address` as a `const fn`: bumps 255 down to 0,
37/// the first whose hash is not a point of the ed25519 curve. The seeds
38/// may be any `const` expressions (a declared program id, another
39/// constant address), not only literals. A program that knows its seeds
40/// at build time pays nothing on chain for the search and stores no bump:
41///
42/// ```ignore
43/// const CONFIG: (Address, u8) = find_program_address_const(&[b"config"], &crate::ID);
44/// ```
45///
46/// Panics (a compile error in a `const`) on 16 or more seeds, a seed
47/// longer than 32 bytes, or when no bump yields an address off the curve.
48pub const fn find_program_address_const(seeds: &[&[u8]], program_id: &Address) -> (Address, u8) {
49    assert!(
50        seeds.len() < MAX_SEEDS,
51        "a PDA takes at most 15 seeds plus its bump"
52    );
53    let mut i = 0;
54    while i < seeds.len() {
55        assert!(
56            seeds[i].len() <= MAX_SEED_LEN,
57            "a PDA seed is at most 32 bytes"
58        );
59        i += 1;
60    }
61    let mut bump = u8::MAX;
62    loop {
63        let hash = pda_hash(seeds, Some(bump), program_id);
64        if !crate::curve25519::is_on_curve(&hash) {
65            return (Address::new_from_array(hash), bump);
66        }
67        assert!(bump != 0, "no bump yields an address off the curve");
68        bump -= 1;
69    }
70}
71
72/// SHA-based paths must accept exactly the seed domain that the PDA signing
73/// syscall accepts. A helper which appends a bump reserves one of the 16 slots.
74#[inline(always)]
75fn validate_seeds(seeds: &[&[u8]], max_count: usize) -> Result<(), ProgramError> {
76    if seeds.len() > max_count || seeds.iter().any(|seed| seed.len() > MAX_SEED_LEN) {
77        return Err(ProgramError::InvalidSeeds);
78    }
79    Ok(())
80}
81
82/// Create a program-derived address from seeds and a program ID.
83///
84/// Returns `Err(InvalidSeeds)` if the derived address falls on the
85/// ed25519 curve (not a valid PDA), or if more than [`MAX_SEEDS`] seeds
86/// are supplied (matching upstream `Pubkey::create_program_address`
87/// semantics, never silently truncating the seed set).
88#[inline(always)]
89pub fn create_program_address(
90    seeds: &[&[u8]],
91    program_id: &Address,
92) -> Result<Address, ProgramError> {
93    validate_seeds(seeds, MAX_SEEDS)?;
94    #[cfg(target_os = "solana")]
95    {
96        // The syscall reads `seeds.len()` (ptr, len) pairs of 8-byte words,
97        // which is exactly the in-memory shape of a `&[&[u8]]` on the SBF
98        // target (the same layout the Solana SDK and pinocchio hand over).
99        // Passing the slice directly replaces the zero-filled 256-byte
100        // staging buffer and repack loop the wrapper used to run before
101        // every derivation; measured 2026-09-21 on the framework-comparison
102        // counter at ~70 CU per call above the syscall's own charge.
103        const _: () = assert!(core::mem::size_of::<&[u8]>() == 16);
104        // Uninitialized: the syscall writes all 32 bytes on success, and
105        // zero-filling first cost four stores it then overwrote.
106        let mut result = core::mem::MaybeUninit::<Address>::uninit();
107        // SAFETY: `seeds` is a live `&[&[u8]]` whose SBF layout is the
108        // (pointer, length) array the syscall reads (the size is asserted
109        // above); `program_id` is 32 readable bytes and `result` 32 writable
110        // ones.
111        let rc = unsafe {
112            crate::syscalls::sol_create_program_address(
113                seeds.as_ptr() as *const u8,
114                seeds.len() as u64,
115                program_id.as_array().as_ptr(),
116                result.as_mut_ptr() as *mut u8,
117            )
118        };
119        if rc == 0 {
120            // SAFETY: a zero return means the syscall wrote the whole
121            // address, and every byte pattern is a valid `Address`.
122            Ok(unsafe { result.assume_init() })
123        } else {
124            Err(ProgramError::InvalidSeeds)
125        }
126    }
127    #[cfg(not(target_os = "solana"))]
128    {
129        // Off chain: the same hash and the same curve rejection the
130        // syscall applies.
131        let hash = pda_hash(seeds, None, program_id);
132        if crate::curve25519::is_on_curve(&hash) {
133            Err(ProgramError::InvalidSeeds)
134        } else {
135            Ok(Address::new_from_array(hash))
136        }
137    }
138}
139
140/// Find a program-derived address and its bump seed.
141///
142/// Iterates bump seeds 255..=0 until a valid PDA is found.
143///
144/// # Panics
145///
146/// Panics if no viable bump exists, 16 or more base seeds are supplied,
147/// or a seed exceeds 32 bytes. The bump occupies the final seed slot,
148/// matching upstream `Pubkey::find_program_address` semantics.
149/// Silently returning a placeholder here would hand callers the all-zero
150/// address, the System Program, as if it were their PDA. Use
151/// [`based_try_find_program_address`] for the fallible variant.
152///
153/// `#[inline(always)]` is deliberate: outlining the sibling
154/// `verify_pda_sha256_loop` was measured on 2026-07-09 at only −88 bytes
155/// of release `.text` for +44..+73 CU on every benched vault row, the
156/// call boundary defeats LLVM's per-call-site specialization of the seed
157/// staging and bump loop. The size answer to PDA duplication is
158/// `bump = stored` (one hash, no search), not outlining the search.
159#[inline(always)]
160pub fn find_program_address(seeds: &[&[u8]], program_id: &Address) -> (Address, u8) {
161    match based_try_find_program_address(seeds, program_id) {
162        Ok(found) => found,
163        Err(_) => panic!("hopper: unable to find a viable program address bump seed"),
164    }
165}
166
167/// The program-derived address for `seeds` and `bump` under `program_id`,
168/// computed with the const SHA-256 so it can be evaluated at compile time.
169///
170/// This is the hash half of `create_program_address` (seeds, bump, program
171/// id, the `ProgramDerivedAddress` marker) without the curve rejection, so
172/// callers must pass the canonical bump their client obtained from
173/// `find_program_address`; a bump the runtime would skip because its hash
174/// lands on the ed25519 curve is not detected here. Static seeds hashed
175/// once at compile time turn a runtime PDA check into a 32-byte compare.
176/// Panics on 16 or more base seeds or a seed longer than 32 bytes. The bump
177/// occupies one of the runtime's 16 seed slots. In a const expression the
178/// refusal is a compilation error.
179pub const fn program_address_const(seeds: &[&[u8]], bump: u8, program_id: &Address) -> Address {
180    assert!(
181        seeds.len() < MAX_SEEDS,
182        "a PDA takes at most 15 seeds plus its bump"
183    );
184    let mut hasher = crate::sha256::ConstSha256::new();
185    let mut i = 0;
186    while i < seeds.len() {
187        assert!(
188            seeds[i].len() <= crate::address::MAX_SEED_LEN,
189            "a PDA seed is at most 32 bytes"
190        );
191        hasher = hasher.update(seeds[i]);
192        i += 1;
193    }
194    let hash = hasher
195        .update(&[bump])
196        .update(program_id.as_array())
197        .update(crate::address::PDA_MARKER)
198        .finalize();
199    Address::new_from_array(hash)
200}
201
202/// Verify that an expected address matches the PDA hash for the provided seeds.
203///
204/// The seeds slice must already include the bump byte.
205/// This checks hash equality only. It does not establish curve membership,
206/// canonicality, account ownership, or an account's initialization history.
207#[inline(always)]
208pub fn verify_program_address(
209    seeds: &[&[u8]],
210    program_id: &Address,
211    expected: &Address,
212) -> Result<(), ProgramError> {
213    validate_seeds(seeds, MAX_SEEDS)?;
214
215    #[cfg(target_os = "solana")]
216    {
217        let n = seeds.len();
218        let mut slices = core::mem::MaybeUninit::<[&[u8]; MAX_SEEDS + 2]>::uninit();
219        let slice_ptr = slices.as_mut_ptr() as *mut &[u8];
220
221        let mut i = 0;
222        while i < n {
223            // SAFETY: `validate_seeds` bounded `n`, so every index written is
224            // inside the `MAX_SEEDS + 2` descriptor array; `write`
225            // initializes a slot without reading the uninitialized memory it
226            // replaces.
227            unsafe { slice_ptr.add(i).write(seeds[i]) };
228            i += 1;
229        }
230        // SAFETY: `validate_seeds` bounded `n`, so every index written is
231        // inside the `MAX_SEEDS + 2` descriptor array; `write` initializes a
232        // slot without reading the uninitialized memory it replaces.
233        unsafe {
234            slice_ptr.add(n).write(program_id.as_ref());
235            slice_ptr.add(n + 1).write(PDA_MARKER_BYTES.as_slice());
236        }
237
238        // SAFETY: The first `n + 2` descriptors were initialized above, and
239        // only that prefix is exposed.
240        let input = unsafe { core::slice::from_raw_parts(slice_ptr, n + 2) };
241        let mut hash = core::mem::MaybeUninit::<[u8; 32]>::uninit();
242
243        // SAFETY: `input` is `n + 2` initialized descriptors whose slices are
244        // all live; `hash` has room for the 32 bytes the syscall writes.
245        unsafe {
246            crate::syscalls::sol_sha256(
247                input as *const _ as *const u8,
248                input.len() as u64,
249                hash.as_mut_ptr() as *mut u8,
250            );
251        }
252
253        // SAFETY: `sol_sha256` wrote all 32 bytes of `hash` (it aborts the
254        // transaction otherwise), and `Address` is `#[repr(transparent)]`
255        // over `[u8; 32]`.
256        let derived = unsafe { &*(hash.as_ptr() as *const Address) };
257        if derived == expected {
258            Ok(())
259        } else {
260            Err(ProgramError::InvalidSeeds)
261        }
262    }
263    #[cfg(not(target_os = "solana"))]
264    {
265        if pda_hash(seeds, None, program_id) == *expected.as_array() {
266            Ok(())
267        } else {
268            Err(ProgramError::InvalidSeeds)
269        }
270    }
271}
272
273/// Find a valid PDA by hashing seeds directly and checking curve validity.
274///
275/// This avoids the `sol_try_find_program_address` syscall and substantially
276/// reduces the per-attempt CU cost on SBF.
277#[inline(always)]
278pub fn based_try_find_program_address(
279    seeds: &[&[u8]],
280    program_id: &Address,
281) -> Result<(Address, u8), ProgramError> {
282    validate_seeds(seeds, MAX_SEEDS - 1)?;
283
284    #[cfg(target_os = "solana")]
285    {
286        let n = seeds.len();
287        let mut slices = core::mem::MaybeUninit::<[&[u8]; MAX_SEEDS + 2]>::uninit();
288        let slice_ptr = slices.as_mut_ptr() as *mut &[u8];
289
290        let mut i = 0;
291        while i < n {
292            // SAFETY: `validate_seeds` bounded `n`, so every index written is
293            // inside the `MAX_SEEDS + 2` descriptor array; `write`
294            // initializes a slot without reading the uninitialized memory it
295            // replaces.
296            unsafe { slice_ptr.add(i).write(seeds[i]) };
297            i += 1;
298        }
299        // SAFETY: `validate_seeds` bounded `n`, so every index written is
300        // inside the `MAX_SEEDS + 2` descriptor array; `write` initializes a
301        // slot without reading the uninitialized memory it replaces.
302        unsafe {
303            slice_ptr.add(n + 1).write(program_id.as_ref());
304            slice_ptr.add(n + 2).write(PDA_MARKER_BYTES.as_slice());
305        }
306
307        let mut hash = core::mem::MaybeUninit::<[u8; 32]>::uninit();
308        let mut bump: u64 = u8::MAX as u64;
309
310        loop {
311            let bump_seed = [bump as u8];
312            // SAFETY: n <= MAX_SEEDS - 1. Install this iteration's immutable
313            // seed before constructing the initialized prefix. The syscall
314            // consumes it synchronously; no shared seed reference is reused
315            // after the next iteration rewrites the descriptor array.
316            unsafe {
317                slice_ptr
318                    .add(n)
319                    .write(core::slice::from_raw_parts(bump_seed.as_ptr(), 1))
320            };
321            let input = unsafe { core::slice::from_raw_parts(slice_ptr, n + 3) };
322
323            // SAFETY: All n + 3 descriptors are initialized, every referenced
324            // byte is live for the call, and hash has space for its 32-byte output.
325            unsafe {
326                crate::syscalls::sol_sha256(
327                    input as *const _ as *const u8,
328                    input.len() as u64,
329                    hash.as_mut_ptr() as *mut u8,
330                );
331            }
332
333            // SAFETY: `hash` was fully written by sol_sha256 above; the
334            // syscall only reads 32 bytes from it.
335            // Return code semantics: 0 = the point IS on the ed25519 curve
336            // (not a valid PDA), nonzero = off-curve (valid PDA).
337            let curve_rc = unsafe {
338                crate::syscalls::sol_curve_validate_point(
339                    CURVE25519_EDWARDS,
340                    hash.as_ptr() as *const u8,
341                    core::ptr::null_mut(),
342                )
343            };
344
345            if curve_rc != 0 {
346                return Ok((
347                    // SAFETY: `sol_sha256` wrote all 32 bytes of `hash` in
348                    // this iteration before the curve check read them.
349                    Address::new_from_array(unsafe { hash.assume_init() }),
350                    bump as u8,
351                ));
352            }
353
354            if bump == 0 {
355                break;
356            }
357            bump -= 1;
358        }
359
360        Err(ProgramError::InvalidSeeds)
361    }
362    #[cfg(not(target_os = "solana"))]
363    {
364        let mut bump = u8::MAX;
365        loop {
366            let hash = pda_hash(seeds, Some(bump), program_id);
367            if !crate::curve25519::is_on_curve(&hash) {
368                return Ok((Address::new_from_array(hash), bump));
369            }
370            if bump == 0 {
371                return Err(ProgramError::InvalidSeeds);
372            }
373            bump -= 1;
374        }
375    }
376}
377
378/// Verify that an account's address matches a PDA derived from the given seeds.
379///
380/// Returns `Ok(())` if the account address matches the derived PDA,
381/// or `Err(InvalidSeeds)` if it does not.
382#[inline(always)]
383pub fn verify_pda(
384    account: &AccountView<'_>,
385    seeds: &[&[u8]],
386    program_id: &Address,
387) -> Result<(), ProgramError> {
388    let expected = create_program_address(seeds, program_id)?;
389    if account.address() == &expected {
390        Ok(())
391    } else {
392        Err(ProgramError::InvalidSeeds)
393    }
394}
395
396/// Verify a PDA with an explicit bump seed appended to the seeds.
397///
398/// Appends `&[bump]` to the end of the seed list before verifying via
399/// SHA-256 (~200 CU). This is substantially cheaper than the syscall-based
400/// `create_program_address` approach (~1500 CU).
401///
402/// Returns `Err(InvalidSeeds)` for 16 or more base seeds or a seed longer
403/// than 32 bytes. The bump counts toward the 16-seed runtime limit.
404///
405/// # Bump canonicalization
406///
407/// `bump` must be the **canonical** bump for these seeds, the one
408/// `find_program_address` returns and the program stored at init. Passing
409/// an attacker-supplied bump (e.g. straight from instruction data) lets
410/// multiple addresses verify for the same logical seed set, the classic
411/// bump-canonicalization vulnerability. Prefer
412/// [`verify_pda_from_stored_bump`], which reads the bump the account
413/// itself recorded.
414#[inline]
415pub fn verify_pda_with_bump(
416    account: &AccountView<'_>,
417    seeds: &[&[u8]],
418    bump: u8,
419    program_id: &Address,
420) -> Result<(), ProgramError> {
421    validate_seeds(seeds, MAX_SEEDS - 1)?;
422    // Build a seed list with the bump appended.
423    // Stack-allocated: at most 15 base seeds plus the bump.
424    let mut full_seeds: [&[u8]; MAX_SEEDS] = [&[]; MAX_SEEDS];
425    let num = seeds.len();
426    let mut i = 0;
427    while i < num {
428        full_seeds[i] = seeds[i];
429        i += 1;
430    }
431    let bump_bytes = [bump];
432    full_seeds[num] = &bump_bytes;
433
434    verify_program_address(&full_seeds[..num + 1], program_id, account.address())
435}
436
437/// Verify that an address matches a PDA derived from the given seeds.
438///
439/// Unlike `verify_pda` which takes an `AccountView`, this accepts a raw
440/// `Address` reference directly. Useful when validating addresses outside
441/// of the account parsing flow (e.g. instruction data, cross-program reads).
442///
443/// The seeds slice must already include the bump byte (like
444/// `verify_program_address`). Uses SHA-256 verify-only path (~200 CU)
445/// instead of the full `find_program_address` (~1500 CU).
446///
447/// The included bump must be the **canonical** one for the seed set (see
448/// [`verify_pda_with_bump`]'s bump-canonicalization note): verifying with
449/// an attacker-supplied bump lets multiple addresses pass for the same
450/// logical seeds.
451///
452/// Returns `Ok(())` if the address matches the derived PDA,
453/// or `Err(InvalidSeeds)` if it does not.
454#[inline]
455pub fn verify_pda_strict(
456    expected: &Address,
457    seeds: &[&[u8]],
458    program_id: &Address,
459) -> Result<(), ProgramError> {
460    verify_program_address(seeds, program_id, expected)
461}
462
463/// Find the bump seed for a known PDA address, skipping curve validation.
464///
465/// This is a hash-match search, not canonical derivation or an off-curve
466/// proof. Mere presence in a transaction or existence on chain does not
467/// establish either property. The caller must establish the required PDA
468/// provenance separately. Use [`based_try_find_program_address`] and compare
469/// its result when a canonical address is required.
470///
471/// Returns the bump seed, or `Err(InvalidSeeds)` if no bump produces a match.
472#[inline(always)]
473pub fn find_bump_for_address(
474    seeds: &[&[u8]],
475    program_id: &Address,
476    expected: &Address,
477) -> Result<u8, ProgramError> {
478    validate_seeds(seeds, MAX_SEEDS - 1)?;
479
480    #[cfg(target_os = "solana")]
481    {
482        let n = seeds.len();
483        let mut slices = core::mem::MaybeUninit::<[&[u8]; MAX_SEEDS + 2]>::uninit();
484        let slice_ptr = slices.as_mut_ptr() as *mut &[u8];
485
486        let mut i = 0;
487        while i < n {
488            // SAFETY: `validate_seeds` bounded `n`, so every index written is
489            // inside the `MAX_SEEDS + 2` descriptor array; `write`
490            // initializes a slot without reading the uninitialized memory it
491            // replaces.
492            unsafe { slice_ptr.add(i).write(seeds[i]) };
493            i += 1;
494        }
495        // SAFETY: `validate_seeds` bounded `n`, so every index written is
496        // inside the `MAX_SEEDS + 2` descriptor array; `write` initializes a
497        // slot without reading the uninitialized memory it replaces.
498        unsafe {
499            slice_ptr.add(n + 1).write(program_id.as_ref());
500            slice_ptr.add(n + 2).write(PDA_MARKER_BYTES.as_slice());
501        }
502
503        let mut hash = core::mem::MaybeUninit::<[u8; 32]>::uninit();
504        let mut bump: u64 = u8::MAX as u64;
505
506        loop {
507            let bump_seed = [bump as u8];
508            // SAFETY: n <= MAX_SEEDS - 1. Install this iteration's immutable
509            // seed before constructing the initialized prefix. No shared
510            // seed reference is reused after its backing byte changes.
511            unsafe {
512                slice_ptr
513                    .add(n)
514                    .write(core::slice::from_raw_parts(bump_seed.as_ptr(), 1))
515            };
516            let input = unsafe { core::slice::from_raw_parts(slice_ptr, n + 3) };
517
518            // SAFETY: All descriptors and input bytes remain live through the
519            // synchronous call, which fills the 32-byte hash output.
520            unsafe {
521                crate::syscalls::sol_sha256(
522                    input as *const _ as *const u8,
523                    input.len() as u64,
524                    hash.as_mut_ptr() as *mut u8,
525                );
526            }
527
528            // Address-match shortcut: skip curve check entirely.
529            // Matching this address does not establish canonicality or
530            // curve membership; those are separate caller obligations.
531            // SAFETY: `sol_sha256` wrote all 32 bytes of `hash` in this
532            // iteration, and `Address` is `#[repr(transparent)]` over `[u8;
533            // 32]`.
534            let derived = unsafe { &*(hash.as_ptr() as *const Address) };
535            if derived == expected {
536                return Ok(bump as u8);
537            }
538
539            if bump == 0 {
540                break;
541            }
542            bump -= 1;
543        }
544
545        Err(ProgramError::InvalidSeeds)
546    }
547    #[cfg(not(target_os = "solana"))]
548    {
549        // The same hash-match search as on chain: no curve check.
550        let mut bump = u8::MAX;
551        loop {
552            if pda_hash(seeds, Some(bump), program_id) == *expected.as_array() {
553                return Ok(bump);
554            }
555            if bump == 0 {
556                return Err(ProgramError::InvalidSeeds);
557            }
558            bump -= 1;
559        }
560    }
561}
562
563/// Read the bump byte directly from account data at a known offset.
564///
565/// Used with `BUMP_OFFSET` from `hopper_layout!` types to read the stored
566/// bump without any derivation. Combined with `verify_program_address`,
567/// the total PDA verification cost is ~200 CU vs ~1500 CU for
568/// `find_program_address`.
569///
570/// Returns `Err(AccountDataTooSmall)` if the account data is shorter than
571/// `bump_offset + 1`.
572#[inline(always)]
573pub fn read_bump_from_account(
574    account: &AccountView<'_>,
575    bump_offset: usize,
576) -> Result<u8, ProgramError> {
577    let data = account.try_borrow()?;
578    if data.len() <= bump_offset {
579        return Err(ProgramError::AccountDataTooSmall);
580    }
581    Ok(data[bump_offset])
582}
583
584/// Verify a PDA using the bump stored in account data (cheapest path).
585///
586/// Reads the bump at `bump_offset`, appends it to seeds, then uses
587/// SHA-256 verify-only. Total cost: ~200 CU vs ~1500 CU.
588///
589/// This is the optimal PDA verification path and should be the default
590/// for Hopper programs that store bumps in their account layout.
591#[inline]
592pub fn verify_pda_from_stored_bump(
593    account: &AccountView<'_>,
594    seeds: &[&[u8]],
595    bump_offset: usize,
596    program_id: &Address,
597) -> Result<(), ProgramError> {
598    validate_seeds(seeds, MAX_SEEDS - 1)?;
599    let bump = read_bump_from_account(account, bump_offset)?;
600
601    let mut full_seeds: [&[u8]; MAX_SEEDS] = [&[]; MAX_SEEDS];
602    let num = seeds.len();
603    let mut i = 0;
604    while i < num {
605        full_seeds[i] = seeds[i];
606        i += 1;
607    }
608    let bump_bytes = [bump];
609    full_seeds[num] = &bump_bytes;
610
611    verify_program_address(&full_seeds[..num + 1], program_id, account.address())
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617
618    #[test]
619    fn const_pda_accepts_fifteen_base_seeds() {
620        let id = Address::new_from_array([91; 32]);
621        let fifteen: [&[u8]; 15] = [&[]; 15];
622        assert_eq!(
623            program_address_const(&fifteen, 255, &id),
624            program_address_const(&[], 255, &id)
625        );
626    }
627
628    #[test]
629    #[should_panic(expected = "at most 15 seeds plus its bump")]
630    fn const_pda_reserves_the_bump_slot() {
631        let seeds: [&[u8]; 16] = [&[]; 16];
632        program_address_const(&seeds, 255, &Address::new_from_array([91; 32]));
633    }
634
635    #[test]
636    #[should_panic(expected = "at most 32 bytes")]
637    fn const_pda_rejects_oversized_seeds() {
638        program_address_const(&[&[0; 33]], 255, &Address::new_from_array([91; 32]));
639    }
640}