Program Derived Address (PDA)
Program Derived Addresses (PDAs) provide developers on Solana with two main use cases:
- Deterministic Account Addresses: PDAs provide a mechanism to deterministically create an address using a combination of optional "seeds" (predefined inputs) and a specific program ID.
- Enable Program Signing: The Solana runtime enables programs to "sign" for PDAs which are derived from the program's address.
You can think of PDAs as a way to create hashmap-like structures on-chain from a predefined set of inputs (e.g. strings, numbers, and other account addresses).
The benefit of this approach is that it eliminates the need to keep track of an exact address. Instead, you simply need to recall the specific inputs used for its derivation.
Program Derived Address
It's important to understand that simply deriving a Program Derived Address (PDA) doesn't automatically create an on-chain account at that address. Accounts with a PDA as the on-chain address must be explicitly created through the program used to derive the address. You can think of deriving a PDA as finding an address on a map. Just having an address doesn't mean there is anything built at that location.
This section covers the details of deriving PDAs. The section on Cross Program Invocations (CPIs) explains how programs use PDAs for signing.
Key Points
- PDAs are addresses derived deterministically using a combination of predefined seeds, a bump seed, and a program's ID.
- PDAs are addresses that fall off the Ed25519 curve and have no corresponding private key.
- Solana programs can sign on behalf of PDAs derived from its program ID.
- Deriving a PDA doesn't automatically create an on-chain account.
- An account using a PDA as its address must be created through an instruction within a Solana program.
What's a PDA
PDAs are addresses that derive deterministically that look like public keys, but have no private keys. This means it is not possible to generate a valid signature for the address. However, the Solana runtime enables programs to "sign" for PDAs without needing a private key.
For context, Solana Keypairs are points on the Ed25519 curve (elliptic-curve cryptography) with a public key and corresponding private key. Public keys are used as addresses (unique identifier) for on-chain accounts.
On Curve Address
A PDA is a point that's intentionally derived to fall off the Ed25519 curve using a predefined set of inputs. A point that's not on the Ed25519 curve does not have a valid corresponding private key and can't perform cryptographic operations (signing).
A PDA can serve as the address (unique identifier) for an on-chain account, providing a method to easily store, map, and fetch program state.
Off Curve Address
How to derive a PDA
The derivation of a PDA requires three inputs:
- Optional seeds: Predefined inputs (e.g. strings, numbers, other account addresses) for PDA derivation.
- Bump seed: An extra byte appended to the optional seeds to ensure a valid PDA (off curve) is generated. The bump seed starts at 255 and decrements by 1 until a valid PDA is found.
- Program ID: The address of the program from which the PDA is derived. This program can sign on behalf of the PDA.
PDA Derivation
Use the following functions from the respective SDKs to derive a PDA.
SDK | Function |
---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
To derive a PDA, provide the following inputs to the SDK function:
- The predefined optional seeds converted to bytes
- The program ID (address) used for derivation
Once a valid PDA is found, the function returns both the address (PDA) and the bump seed used for derivation.
Examples
The following examples show how to derive a PDA using the respective SDKs.
Click the "Run" button to execute the code.
Derive a PDA with optional string seed
import { Address, getProgramDerivedAddress } from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const seeds = ["helloWorld"];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
You must enable the "Enable Custom URL Param" setting on Solana Explorer.
If not enabled, links will default to localhost:8899 instead of the Mirror.ad RPC URL.
Derive a PDA with optional address seed
import {Address,getAddressEncoder,getProgramDerivedAddress} from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const addressEncoder = getAddressEncoder();const optionalSeedAddress = addressEncoder.encode("B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka" as Address);const seeds = [optionalSeedAddress];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
You must enable the "Enable Custom URL Param" setting on Solana Explorer.
If not enabled, links will default to localhost:8899 instead of the Mirror.ad RPC URL.
Derive a PDA with multiple optional seeds
import {Address,getAddressEncoder,getProgramDerivedAddress} from "@solana/kit";const programAddress = "11111111111111111111111111111111" as Address;const optionalSeedString = "helloWorld";const addressEncoder = getAddressEncoder();const optionalSeedAddress = addressEncoder.encode("B9Lf9z5BfNPT4d5KMeaBFx8x1G4CULZYR1jA2kmxRDka" as Address);const seeds = [optionalSeedString, optionalSeedAddress];const [pda, bump] = await getProgramDerivedAddress({programAddress,seeds});console.log(`PDA: ${pda}`);console.log(`Bump: ${bump}`);
You must enable the "Enable Custom URL Param" setting on Solana Explorer.
If not enabled, links will default to localhost:8899 instead of the Mirror.ad RPC URL.
Canonical Bump
PDA derivation requires a "bump seed", an extra byte appended to the optional seeds. The derivation function iterates through bump values, starting at 255 and decrementing by 1, until a value produces a valid off-curve address. The first bump value that produces a valid off-curve address is the "canonical bump."
The following examples show PDA derivation using all possible bump seeds (255 to 0):
Kit example not included because the createProgramDerivedAddress function isn't exported.
import { PublicKey } from "@solana/web3.js";const programId = new PublicKey("11111111111111111111111111111111");const optionalSeed = "helloWorld";// Loop through all bump seeds (255 down to 0)for (let bump = 255; bump >= 0; bump--) {try {const PDA = PublicKey.createProgramAddressSync([Buffer.from(optionalSeed), Buffer.from([bump])],programId);console.log("bump " + bump + ": " + PDA);} catch (error) {console.log("bump " + bump + ": " + error);}}
You must enable the "Enable Custom URL Param" setting on Solana Explorer.
If not enabled, links will default to localhost:8899 instead of the Mirror.ad RPC URL.
bump 255: Error: Invalid seeds, address must fall off the curvebump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6Xbump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4ybump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHHbump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdPbump 250: Error: Invalid seeds, address must fall off the curve...// remaining bump outputs
The bump seed 255 throws an error and the first bump seed to derive a valid PDA is 254.
Note that bump seeds 253-251 all derive valid PDAs with different addresses.
This means that given the same optional seeds and programId
, a bump seed with
a different value can still derive a valid PDA.
When building Solana programs, always include security checks to ensure a PDA passed to the program is derived from the canonical bump. Failing to include these checks may introduce vulnerabilities that allow unexpected accounts to be used in the program instructions. It is best practice to only use the canonical bump when deriving PDAs.
Create PDA Accounts
The example program below shows how to create an account using a PDA as the address of the new account. The example program uses the Anchor framework.
The program includes a single initialize
instruction to create a new account
using a PDA as the address of the account. The new account stores the address of
the user
and the bump
seed used to derive the PDA.
use anchor_lang::prelude::*;declare_id!("75GJVCJNhaukaa2vCCqhreY31gaphv7XTScBChmr1ueR");#[program]pub mod pda_account {use super::*;pub fn initialize(ctx: Context<Initialize>) -> Result<()> {let account_data = &mut ctx.accounts.pda_account;// store the address of the `user`account_data.user = *ctx.accounts.user.key;// store the canonical bumpaccount_data.bump = ctx.bumps.pda_account;Ok(())}}#[derive(Accounts)]pub struct Initialize<'info> {#[account(mut)]pub user: Signer<'info>,#[account(init,// define the seeds to derive the PDAseeds = [b"data", user.key().as_ref()],// use the canonical bumpbump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,pub system_program: Program<'info, System>,}#[account]#[derive(InitSpace)]pub struct DataAccount {pub user: Pubkey,pub bump: u8,}
In this example, the seeds for PDA derivation include the fixed string data
and the address of the user
account provided in the instruction. The Anchor
framework automatically finds the canonical bump
seed.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
The init
constraint instructs Anchor to invoke the System Program to create a
new account using the PDA as the address. Anchor does this through a
CPI.
#[account(init,seeds = [b"data", user.key().as_ref()],bump,payer = user,space = 8 + DataAccount::INIT_SPACE)]pub pda_account: Account<'info, DataAccount>,
The test file contains the Typescript code to derive the PDA.
const [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);
The transaction in the test file invokes the initialize
instruction to create
a new on-chain account using the PDA as the address. In this example, Anchor can
infer the PDA address in the instruction accounts, so it doesn't need to be
explicitly provided.
it("Is initialized!", async () => {const transactionSignature = await program.methods.initialize().accounts({user: user.publicKey}).rpc();console.log("Transaction Signature:", transactionSignature);});
The test file also shows how fetch the on-chain account created at that address once the transaction is sent.
it("Fetch Account", async () => {const pdaAccount = await program.account.dataAccount.fetch(PDA);console.log(JSON.stringify(pdaAccount, null, 2));});
Note that in this example, if you invoke the initialize
instruction more than
once using the same user
address as a seed, then the transaction fails. This
happens because an account already exists at the derived address.
Is this page helpful?