Summary
PDAs are derived by hashing seeds + program ID + bump via SHA-256 until the result is off the Ed25519 curve. The canonical bump is the first value that produces an off-curve address. Max 16 seeds, Max 32 bytes per seed.
Background
Solana
Keypair
values are points on the Ed25519 curve. A keypair
consists of a public key (used as the account address) and a secret key (used to
produce signatures). Anyone with the secret key can sign transactions for that
address.
Two accounts with on-curve addresses
A PDA is intentionally derived to fall off the Ed25519 curve. Because it is
not a valid curve point, no secret key exists, and no external party can produce
a signature. Only the deriving program can authorize operations on the PDA
through invoke_signed.
Off Curve Address
PDA vs keypair accounts
| Property | Keypair account | PDA account |
|---|---|---|
| Address type | On Ed25519 curve | Off Ed25519 curve |
| Has private key | Yes | No |
| Can sign transactions | Yes (with private key) | No |
| Can sign during CPI | No (unless signature included in transaction) | Yes (via invoke_signed) |
| Derivation | Generate Ed25519 keypair | Deterministic from seeds + program ID |
| Typical use | User wallets, Program ID | Program-owned data accounts |
Optional Seeds
The optional seeds are user-defined byte strings that serve as inputs to the PDA
derivation. They create unique, deterministic addresses scoped to a program. For
example, using ["user", user_pubkey] as seeds derives a different PDA for each
user.
Seeds must follow these constraints:
- Maximum 16 seeds per derivation (
MAX_SEEDS) - Maximum 32 bytes per seed (
MAX_SEED_LEN)
Bump Seed
The bump seed is a single byte (0-255) appended to the optional seeds during
derivation.
find_program_address
searches from 255 down to 0, calling create_program_address with each value
until the result falls off the Ed25519 curve. The first value that succeeds is
the canonical bump.
Programs should always use the canonical bump to ensure a unique, deterministic mapping from seeds to address.
Always use the canonical bump when deriving PDAs. Using a non-canonical bump creates a second valid address for the same seeds, which can lead to vulnerabilities where an attacker substitutes a different account than expected.
PDA Derivation
Derivation Algorithm
The PDA derivation is implemented in the SDK's
create_program_address
function. The algorithm works as follows:
- Validate that the number of seeds does not exceed
MAX_SEEDS(16) and no individual seed exceedsMAX_SEED_LEN(32 bytes). If either check fails, returnPubkeyError::MaxSeedLengthExceeded. - SHA-256 hash all seeds, the program ID, and the string
"ProgramDerivedAddress"together to produce a 32-byte result. - Check if the result is a valid point on the Ed25519 curve.
- If the result IS on the curve, return
PubkeyError::InvalidSeeds(the address would have a corresponding private key, which violates the PDA security property). - If the result is NOT on the curve, return it as the PDA.
Compute Unit Costs
The
on-chain syscall
for create_program_address charges
1,500 CUs
per call.
The
try_find_program_address syscall
charges 1,500 CUs on entry (before the loop), then an additional 1,500 CUs for
each failed bump attempt within the loop.
Common Seed Patterns
Seeds are application-specific. Common patterns include:
| Pattern | Seeds | Use case |
|---|---|---|
| Global singleton | ["global"] | Single program-wide config account |
| Per-user account | ["user", user_pubkey] | One account per user per program |
| Per-user-per-entity | ["vault", user_pubkey, mint_pubkey] | Token vaults, per-user-per-token |
| Counter / sequential | ["order", user_pubkey, &order_id.to_le_bytes()] | Sequential records per user |
Seeds are concatenated before hashing, so ["ab", "cd"] and ["abcd"]
produce the same PDA. Use fixed-length seeds or a separator to avoid
collisions. For example, ["ab", "-", "cd"] is unambiguous.
Examples: Derive a PDA
Deriving a PDA computes an address only. It does not create an on-chain account
at that address. The account must be explicitly created through a separate
instruction (typically create_account via CPI).
The Solana SDKs provide functions for PDA derivation. Each function takes:
- Program ID: The address of the program 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 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}`);
Iterating All Bumps
The following examples show PDA derivation using all possible bump seeds (255 to
0), illustrating how find_program_address returns the
canonical bump:
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, bump 255 produces an on-curve address and fails. The first valid bump is 254, making it the canonical bump.
Is this page helpful?