Program-Derived Address
A Solana account address points to the account's location on the blockchain. Many account addresses are the public key of a keypair, in which case the corresponding private key is used to sign transactions involving the account.
A useful alternative to a public key address is a program-derived address (PDA). PDAs provide an easy method to store, map, and fetch program state. A PDA is an address that is created deterministically using a program ID and a combination of optional predefined inputs. PDAs look similar to public key addresses, but do not have a corresponding private key.
The Solana runtime enables programs to sign for PDAs without needing a private key. Using a PDA eliminates the need to keep track of the account's address. Instead, you can recall the specific inputs used for the PDA's derivation. (To learn how programs use PDAs for signing, see the Cross Program Invocations section.)
Background
Solana keypairs are points on the Ed25519 curve (elliptic-curve cryptography). They consist of a public key and a private key. The public key becomes the account address, and the private key is used to generate valid signature for the account.
Two accounts with on-curve addresses
A PDA is intentionally derived to fall off the Ed25519 curve. This means it does not have a valid corresponding private key and can't perform cryptographic operations. (Such as providing a signature.) However, Solana enables programs to sign for PDAs without needing a private key.
Off Curve Address
You can think of PDAs as a way to create hashmap-like structures on-chain using a predefined set of inputs. (For example, strings, numbers, and other account addresses.)
Program Derived Address
Derive a PDA
Before creating an account with a PDA, you must first derive the address. Deriving a PDA does not automatically create an on-chain account at that address— the account must be explicitly created through the program used to derive the PDA. You can think of a PDA like an address on a map: just because an address exists doesn't mean there is anything built there.
The Solana SDKs support PDA creation with the functions shown in the table below. Each function receives the following input:
- Program ID: The address of the program being used to derive the PDA. This program can sign on behalf of the PDA.
- Optional seeds: Predefined inputs, such as strings, numbers or other account addresses.
| SDK | Function |
|---|---|
@solana/kit (Typescript) | getProgramDerivedAddress |
@solana/web3.js (Typescript) | findProgramAddressSync |
solana_sdk (Rust) | find_program_address |
The function uses the program ID and optional seeds, then iterates through bump values to attempt to create a valid program address. The iteration of bump values starts at 255 and decrements by 1 until a valid PDA is found. After a valid PDA is found, the function returns the PDA and the bump seed.
The bump seed is an extra byte appended to the optional seeds to ensure a valid off-curve address is generated.
PDA Derivation
Canonical bump
A bump seed is 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 value that produces a valid off-curve address is called the "canonical bump."
The following examples show PDA derivation using all possible bump seeds (255 to 0):
Kit example is 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);}}
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
In this example, the first bump seed throws an error. The first bump seed to derive a valid PDA is 254. Bump seeds 253-251 also derive unique, valid PDAs.
This means that given the same optional seeds and programId, a bump seed with a different value can still derive a valid PDA.
Always include security checks to ensure a PDA passed to the program is derived from the canonical bump. Failure to do so may introduce vulnerabilities that allow unexpected accounts to be used in the program's instructions. It is best practice to only use the canonical bump when deriving PDAs.
Examples
The examples below derive a PDA using the Solana SDKs. Click ▷ Run to execute the code.
Derive a PDA with a string seed
The example below derives a PDA using a program ID and an 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}`);
Derive a PDA with an address seed
The example below derives a PDA using a program ID and an 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}`);
Derive a PDA with multiple seeds
The example below derives a PDA using a program ID and 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}`);
Create a PDA account
The example below uses the Anchor framework to create a new account with a program-derived address. The program includes a single initialize instruction to create the new account, which will store the user address and 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 bumpdaccount_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,}
The init constraint tells Anchor to invoke the System Program to create a new account using the PDA as the address. The seeds used to create the PDA are:
- The address of the user account provided in the instruction
- The fixed string: "data"
- The canonical bump seed
In this example, the bump constraint is not assigned a value, so Anchor will use find_program_address to derive the PDA and find the bump.
#[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 below contains a transaction that invokes the initialize instruction to create a new account with a program-derived address. The file contains code to derive the PDA.
The example also shows how to fetch the new account that will be created.
import * as anchor from "@coral-xyz/anchor";import { Program } from "@coral-xyz/anchor";import { PdaAccount } from "../target/types/pda_account";import { PublicKey } from "@solana/web3.js";describe("pda-account", () => {const provider = anchor.AnchorProvider.env();anchor.setProvider(provider);const program = anchor.workspace.PdaAccount as Program<PdaAccount>;const user = provider.wallet as anchor.Wallet;// Derive the PDA address using the seeds specified on the programconst [PDA] = PublicKey.findProgramAddressSync([Buffer.from("data"), user.publicKey.toBuffer()],program.programId);it("Is initialized!", async () => {const transactionSignature = await program.methods.initialize().accounts({user: user.publicKey}).rpc();console.log("Transaction Signature:", transactionSignature);});it("Fetch Account", async () => {const pdaAccount = await program.account.dataAccount.fetch(PDA);console.log(JSON.stringify(pdaAccount, null, 2));});});
If you invoke the initialize instruction again with the same user address seed, the transaction will fail. This happens because an account already exists at the derived address.
Is this page helpful?