Soldev

Program Derived Addresses (PDAs)

Last updated:

A Program Derived Address (PDA) is an address that leads to an account on the Solana blockchain.

The difference between a PDA and a regular address is that the PDA doesn't have a corresponding private key, and exists outside of the Ed25519 eliptical curve. What this means is that external users cannot generate a valid signature for the address, and therefore have no access.

The access is instead completely relegated to the Program that created it.

Ed25519 eliptical curve

Addresses (keypairs) are mapped to an eliptical curve. Normal account public keys are all mapped to this curve and have a corresponding private key to be used to sign for transactions on it.

PDAs exist outside of this curve, and have no associated private key.

Instead, we use a combination of

The optional seeds and bump seed are used in combination with the program ID to find an area outside of the curve where the address will exist.

                \ <- The Ed25519 Curve
                 \
                  \       findProgramAddress(seeds, programId)
                   \                │
                    \               │
              (Public Key) <────────╯
                  │   \
  (PDA) <──+bump──╯    \
                        \

The PDA will iterate over possible bump seeds starting from 255, and going to 0, and pick the first number that yields a value outside of the curve.

The canonical bump is simply the first number, decrementing from 255-0, that yielded a spot off-curve for the PDA to live.

Why do we need to be off-curve?

We make PDAs off-curve so that nothing can sign for it except the controlling Program.

If an address is on curve, it theoretically has a private key, meaning somebody can sign for it.

Creating a Program Derived Address

Solana docs give us a look at first finding a valid PDA:

import { PublicKey } from "@solana/web3.js";

const programId = new PublicKey("11111111111111111111111111111111");

const [PDA, bump] = PublicKey.findProgramAddressSync([], programId);

console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);

We can see them using findProgramAddressSync and handing in an empty array for the optional seeds, and the programId, which will later be used to validate that this program has authority over this account.

It yields the bump and PDA as expected:

PDA: Cu7NwqCXSmsR5vgGA3Vw9uYVViPi3kQvkbKByVQ8nPY9
Bump: 255

But this isn't the actual account, we've just identified a valid address for it to live at.

We would then use Anchor's have to create an account, which is done by conforming to a pda_account (including seeds and bump) as described in the receiving program sketched below:

#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
    #[account(mut)]
    pub signer: Signer<'info>,
    #[account(
        init,
        payer = signer,
        space = 8 + 1,
        seeds = [b"hello_world", signer.key().as_ref()],
        bump,
    )]
    pub pda_account: Account<'info, CustomAccount>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct CustomAccount {
    pub bump_seed: u8,
}

After creating that account, you can use invoke_signed in the Program to act as the PDA and pass in the seeds and bump to validate.

When a program tries to invoke a CPI with PDA, the runtime takes the supplied keywords and bump seeds, uses the caller’s program ID, and repeats the process. If the resulting PDA matches, the account is considered to be signed.

invoke - All necessary signatures are already available before the call. invoke_signed - Calling program needs PDAs to act as signers during the CPI.