Focus mode

Solana Development I

Ah, the egg. Or as they're officially known: Program Derived Addresses. We've cooked with these before. Let's crack 'em open to see how they work.

PDAs serve two main functions:

  1. Provide a deterministic way to find the address of a program-owned account
  2. Authorize the program from which a PDA was derived to sign on its behalf in the same way a user may sign with their private key

In other words, they're a secure key-value store for storage on the Solana network.

šŸ”Ž Finding PDAs

So far every time we've needed to derive an address we've used a handy function provided to us. What does this function actually do? To find out, we need to learn how all Solana keypairs are made.

Think back to what the point of a keypair is. It's a way to prove that you are who you say you are. We do this using a digital signature system. Solana keypairs are found on what is called the Ed25519 Elliptic Curve (you don't need to know wtf this is).

Since PDAs are controlled by programs, they donā€™t need private keys. So we make PDAs from addresses that are not on the Ed25519 curve. This effectively means they are public keysĀ withoutĀ a corresponding private key.

Thatā€™s it. You donā€™t need to understand Ed25519, or even what a digital signature algorithm is. All you need to know is that PDAs look like regular Solana addresses and are controlled by programs. If you feel like you want to learn this further, check out this cool video on Digital Signatures from Computerphile.

To find a PDA within a Solana program, we'll use theĀ find_program_addressĀ function.

ā€œSeedsā€ are optional inputs used in theĀ find_program_addressĀ function to derive a PDA.For example, seeds can be any combination:

  • instruction data
  • hardcoded values
  • other account public keys

TheĀ find_program_addressĀ function provides an additional seed called a "bump seed" to ensure that the resultĀ is notĀ on the Ed25519 curve

Once a valid PDA is found, the function returns both:

  • the PDA
  • the bump that was used to derive the PDA

let (pda, bump_seed) = Pubkey::find_program_address(&[user.key.as_ref(), user_input.as_bytes().as_ref(), "SEED".as_bytes()], program_id)

šŸ³ Under the hood ofĀ find_program_address

find_program_address is an imposter - it actually passes the inputĀ seedsĀ andĀ program_idĀ to the try_find_program_addressĀ function

pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) {
    Self::try_find_program_address(seeds, program_id)
        .unwrap_or_else(|| panic!("Unable to find a viable program address bump seed"))
}

TheĀ try_find_program_addressĀ function then introduces theĀ bump_seed.

TheĀ bump_seedĀ is aĀ u8Ā variable with a value ranging between 0 to 255, which is appended to the optional input seeds which are then passed to theĀ create_program_addressĀ function:

pub fn try_find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> Option<(Pubkey, u8)> {

    let mut bump_seed = [std::u8::MAX];
    for _ in 0..std::u8::MAX {
        {
            let mut seeds_with_bump = seeds.to_vec();
            seeds_with_bump.push(&bump_seed);
            match Self::create_program_address(&seeds_with_bump, program_id) {
                Ok(address) => return Some((address, bump_seed[0])),
                Err(PubkeyError::InvalidSeeds) => (),
                _ => break,
            }
        }
        bump_seed[0] -= 1;
    }
    None

}

TheĀ create_program_addressĀ function performs a set of hash operations over the seeds andĀ program_id. These operations compute a key, then verify if the computed key lies on the Ed25519 elliptic curve or not.

If a valid PDA is found (i.e. an address that isĀ offĀ the curve), then the PDA is returned. Otherwise, an error is returned.

pub fn create_program_address(
    seeds: &[&[u8]],
    program_id: &Pubkey,
) -> Result<Pubkey, PubkeyError> {

    let mut hasher = crate::hash::Hasher::default();
    for seed in seeds.iter() {
        hasher.hash(seed);
    }
    hasher.hashv(&[program_id.as_ref(), PDA_MARKER]);
    let hash = hasher.result();

    if bytes_are_curve_point(hash) {
        return Err(PubkeyError::InvalidSeeds);
    }

    Ok(Pubkey::new(hash.as_ref()))

}

To recap:

  • TheĀ find_program_addressĀ function passes our input seeds andĀ program_id

Ā to theĀ try_find_program_addressĀ function.

  • TheĀ try_find_program_addressĀ function adds aĀ bump_seed

Ā (starting from 255) to our input seeds, then calls theĀ create_program_addressĀ function until a valid PDA is found.

  • Once found, both the PDA and theĀ bump_seedĀ are returned.

You don't need to remember all of that! The important thing is understanding what's happening when you call the find_program_address function at a high level.

šŸ¤” Things to notes about PDAs

  • For the same input seeds, different valid bumps will generate different valid PDAs.
  • TheĀ bump_seedĀ returned byĀ find_program_addressĀ will always be the first valid PDA found.
  • ThisĀ bump_seedĀ is commonly referred to as the "canonical bump".
  • TheĀ find_program_addressĀ function only returns a Program Derived Address and the bump seed used to derive it.
  • TheĀ find_program_addressĀ function doesĀ notĀ initialize a new account, nor is any PDA returned by the function necessarily associated with an account that stores data.

šŸ—ŗ Map to data stored in PDA accounts

Since programs themselves are stateless, all program state is managed through external accounts. This means we need to do a bunch of mapping to keep things linked.

The mappings between seeds and PDA accounts that you use will be highly dependent on your specific program. While this isn't a lesson on system design or architecture, it's worth calling out a few guidelines:

  • Use seeds that will be known at the time of PDA derivation
  • Be thoughtful about what data is grouped together into a single account
  • Be thoughtful about the data structure used within each account
  • Simpler is usually better

This was a lot! Again, you don't need to remember everything we've covered here. Let's build an on-chain commenting system to see how this all works in practice!

left-disk

Programs to Accelerate Your Progress in a Software Career

Join our 4-8 month intensive Patika+ bootcamps, start with the fundamentals and gain comprehensive knowledge to kickstart your software career!

right-cube

Comments

You need to enroll in the course to be able to comment!