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 AddressProgram 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 AddressOn 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 AddressOff 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 DerivationPDA Derivation

Use the following functions from the respective SDKs to derive a PDA.

SDKFunction
@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}`);

Click 'Run' to see output.
Program Logs in the Output are clickable links to Solana Explorer.
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}`);

Click 'Run' to see output.
Program Logs in the Output are clickable links to Solana Explorer.
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}`);

Click 'Run' to see output.
Program Logs in the Output are clickable links to Solana Explorer.
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);
}
}

Click 'Run' to see output.
Program Logs in the Output are clickable links to Solana Explorer.
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 curve
bump 254: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
bump 253: GBNWBGxKmdcd7JrMnBdZke9Fumj9sir4rpbruwEGmR4y
bump 252: THfBMgduMonjaNsCisKa7Qz2cBoG1VCUYHyso7UXYHH
bump 251: EuRrNqJAofo7y3Jy6MGvF7eZAYegqYTwH2dnLCwDDGdP
bump 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 bump
account_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 PDA
seeds = [b"data", user.key().as_ref()],
// use the canonical bump
bump,
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.

pda_account
#[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.

pda_account
#[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.

Derive 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.

Invoke Initialize Instruction
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.

Fetch Account
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?

جدول المحتويات

تعديل الصفحة