Program Derived Address
Program Derived Addresses (PDAs) provide developers on Solana with two main use cases:
- Deterministic Account Addresses: PDAs provide a mechanism to deterministically derive 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 its program ID.
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.
It’s important to understand that simply deriving a Program Derived Address (PDA) does not 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 does not mean there is anything built at that location.
This section will cover the details of deriving PDAs. The details on how programs use PDAs for signing will be addressed in the section on Cross Program Invocations (CPIs) as it requires context for both concepts.
What is a PDA
PDAs are addresses that are deterministically derived and look like standard public keys, but have no associated private keys. This means that no external user can generate a valid signature for the address. However, the Solana runtime enables programs to programmatically “sign” for PDAs without needing a private key.
For context, Solana Keypairs are points on the Ed25519 curve (elliptic-curve cryptography) which have a public key and corresponding private key. We often use public keys as the unique IDs for new on-chain accounts and private keys for signing.
A PDA is a point that is intentionally derived to fall off the Ed25519 curve using a predefined set of inputs. A point that is not on the Ed25519 curve does not have a valid corresponding private key and cannot be used for cryptographic operations (signing).
A PDA can then be used as the address (unique identifier) for an on-chain account, providing a method to easily store, map, and fetch program state.
How to derive a PDA
The derivation of a PDA requires 3 inputs.
- Optional seeds: Predefined inputs (e.g. string, number, other account addresses) used to derive a PDA. These inputs are converted to a buffer of bytes.
- Bump seed: An additional input (with a value between 255-0) that is used to guarantee that a valid PDA (off curve) is generated. This bump seed (starting with 255) is appended to the optional seeds when generating a PDA to “bump” the point off the Ed25519 curve. The bump seed is sometimes referred to as a “nonce”.
- Program ID: The address of the program the PDA is derived from. This is also the program that can “sign” on behalf of the PDA
The examples below include links to Solana Playground, where you can run the examples in an in-browser editor.
FindProgramAddress
To derive a PDA, we can use the
findProgramAddressSync
method from @solana/web3.js
.
There are equivalents of this function in other programming languages (e.g.
Rust),
but in this section, we will walk through examples using Javascript.
When using the findProgramAddressSync
method, we pass in:
- the predefined optional seeds converted to a buffer of bytes, and
- the program ID (address) used to derive the PDA
Once a valid PDA is found, findProgramAddressSync
returns both the address
(PDA) and bump seed used to derive the PDA.
No Optional Seeds
The example below derives a PDA without providing any optional seeds.
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}`);
Run the example on Solana Playground.
run
The PDA and bump seed output will always be the same:
Output
Running client...
client.ts:
PDA: Cu7NwqCXSmsR5vgGA3Vw9uYVViPi3kQvkbKByVQ8nPY9
Bump: 255
Add Optional Seeds
The next example below adds an optional seed “helloWorld”.
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
const [PDA, bump] = PublicKey.findProgramAddressSync(
[Buffer.from(string)],
programId,
);
console.log(`PDA: ${PDA}`);
console.log(`Bump: ${bump}`);
Run the example on Solana Playground. Run the
run
The PDA and bump seed output will always be the same:
Output
Running client...
client.ts:
PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
Bump: 254
Note that the bump seed is 254. This means that 255 derived a point on the Ed25519 curve, and is not a valid PDA.
The bump seed returned by findProgramAddressSync
is the first value (between
255-0) for the given combination of optional seeds and program ID that derives a
valid PDA.
This first valid bump seed is referred to as the “canonical bump”. For program security, it is recommended to only use the canonical bump when working with PDAs.
Source Code
static findProgramAddressSync(
seeds: Array<Buffer | Uint8Array>,
programId: PublicKey,
): [PublicKey, number] {
let nonce = 255;
let address;
while (nonce != 0) {
try {
const seedsWithNonce = seeds.concat(Buffer.from([nonce]));
address = this.createProgramAddressSync(seedsWithNonce, programId);
} catch (err) {
if (err instanceof TypeError) {
throw err;
}
nonce--;
continue;
}
return [address, nonce];
}
throw new Error(`Unable to find a viable program address nonce`);
}
CreateProgramAddress
Under the hood, findProgramAddressSync
will iteratively append an additional
bump seed (nonce) to the seeds buffer and call the
createProgramAddressSync
method. The bump seed starts with a value of 255 and is decreased by 1 until a
valid PDA (off curve) is found.
You can replicate the previous example by using createProgramAddressSync
and
explicitly passing in the bump seed of 254.
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
const bump = 254;
const PDA = PublicKey.createProgramAddressSync(
[Buffer.from(string), Buffer.from([bump])],
programId,
);
console.log(`PDA: ${PDA}`);
Run the example on Solana Playground.
run
Given the same seeds and program ID, the PDA output will match the previous one:
Output
Running client...
client.ts:
PDA: 46GZzzetjCURsdFPb7rcnspbEMnCBXe9kpjrsZAkKb6X
Source Code
static createProgramAddressSync(
seeds: Array<Buffer | Uint8Array>,
programId: PublicKey,
): PublicKey {
let buffer = Buffer.alloc(0);
seeds.forEach(function (seed) {
if (seed.length > MAX_SEED_LENGTH) {
throw new TypeError(`Max seed length exceeded`);
}
buffer = Buffer.concat([buffer, toBuffer(seed)]);
});
buffer = Buffer.concat([
buffer,
programId.toBuffer(),
Buffer.from('ProgramDerivedAddress'),
]);
const publicKeyBytes = sha256(buffer);
if (isOnCurve(publicKeyBytes)) {
throw new Error(`Invalid seeds, address must fall off the curve`);
}
return new PublicKey(publicKeyBytes);
}
Canonical Bump
The “canonical bump” refers to the first bump seed (starting from 255 and decrementing by 1) that derives a valid PDA. For program security, it is recommended to only use PDAs derived from a canonical bump.
Using the previous example as a reference, the example below attempts to derive a PDA using every bump seed from 255-0.
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("11111111111111111111111111111111");
const string = "helloWorld";
// Loop through all bump seeds for demonstration
for (let bump = 255; bump >= 0; bump--) {
try {
const PDA = PublicKey.createProgramAddressSync(
[Buffer.from(string), Buffer.from([bump])],
programId,
);
console.log("bump " + bump + ": " + PDA);
} catch (error) {
console.log("bump " + bump + ": " + error);
}
}
Run the example on Solana Playground.
run
You should see the following output:
Output
Running client...
client.ts:
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
bump 2: 67GsQvejZnzvXv3CcFu3F83cHyYc9qPwA21QAoD5ci9o
bump 1: Error: Invalid seeds, address must fall off the curve
bump 0: 9QEQyq7aBV4jw7BmmN6nbYmzaaw6Dtxz15VrD3ffqNRW
As expected, the bump seed 255 throws an error and the first bump seed to derive a valid PDA is 254.
However, 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, it is recommended to include security checks that validate a PDA passed to the program is derived using the canonical bump. Failing to do so may introduce vulnerabilities that allow for unexpected accounts to be provided to a program.
PDA with Anchor
This example program on Solana Playground demonstrates how to create an account using a PDA as the address of the new account. The example program is written using the Anchor framework.
Program
In the lib.rs
file, you will find the following program which includes a
single 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,
// set 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,
}
The seeds used to derive the PDA include the hardcoded string data
and the
address of the user
account provided in the instruction. The Anchor framework
automatically derives 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. Under the hood, this is done 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>,
Client
In the test file (pda-account.test.ts
) located within the Solana Playground
link provided above, you will find the Javascript equivalent to derive the PDA.
const [PDA] = PublicKey.findProgramAddressSync(
[Buffer.from("data"), user.publicKey.toBuffer()],
program.programId,
);
A transaction is then sent to invoke the initialize
instruction to create a
new on-chain account using the PDA as the address. Once the transaction is sent,
the PDA is used to fetch the on-chain account that was created at the address.
it("Is initialized!", async () => {
const transactionSignature = await program.methods
.initialize()
.accounts({
user: user.publicKey,
pdaAccount: PDA,
})
.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));
});
Note that if you invoke the initialize
instruction more than once using the
same user
address as a seed, then the transaction will fail. This is because
an account will already exist at the derived address.
How to create PDA Accounts with Anchor
- playground on 0.29, code on 0.30, causing error when running test due to client/ idl changes
start https://beta.solpg.io/https://github.com/solana-developers/anchor-starter/tree/main/01-counter
end https://beta.solpg.io/https://github.com/solana-developers/anchor-starter/tree/main/02-pda-counter
Source Code - Counter Program
use anchor_lang::prelude::*;
declare_id!("HQM9iY5WRbWXFidD6p2LCHV2VuDcmcmXXuAw6qGZYWtk");
#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.bump = ctx.bumps.counter; // store bump seed in `Counter` account
msg!("Counter account created! Current count: {}", counter.count);
msg!("Counter bump: {}", counter.bump);
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
msg!("Previous counter: {}", counter.count);
counter.count = counter.count.checked_add(1).unwrap();
msg!("Counter incremented! Current count: {}", counter.count);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub user: Signer<'info>,
// Create and initialize `Counter` account using a PDA as the address
#[account(
init,
seeds = [b"counter"], // optional seeds for pda
bump, // bump seed for pda
payer = user,
space = 8 + Counter::INIT_SPACE
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
// The address of the `Counter` account must be a PDA derived with the specified `seeds`
#[account(
mut,
seeds = [b"counter"], // optional seeds for pda
bump = counter.bump, // bump seed for pda stored in `Counter` account
)]
pub counter: Account<'info, Counter>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64, // 8 bytes
pub bump: u8, // 1 byte
}
Starter Code
- open solana playground
- starter code
Code
use anchor_lang::prelude::*;
// Specify the program address
declare_id!("C93fyDjEmyAfr9nwDeWMVCeWVVx8fjySxnshSA9VY4KG");
// Instructions defined in program module
#[program]
pub mod counter {
use super::*;
// Instruction to initialize a new counter account
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// Reference to the counter account from the Initialize struct
let counter = &ctx.accounts.counter;
msg!("Counter account created! Current count: {}", counter.count);
Ok(())
}
// Instruction to increment a counter account
pub fn increment(ctx: Context<Increment>) -> Result<()> {
// Mutable reference to the counter account from the Increment struct
let counter = &mut ctx.accounts.counter;
msg!("Previous counter: {}", counter.count);
// Increment the count value stored on the counter account by 1
counter.count = counter.count + 1;
msg!("Counter incremented! Current count: {}", counter.count);
Ok(())
}
}
// Accounts required by the initialize instruction
#[derive(Accounts)]
pub struct Initialize<'info> {
// The account paying to create the counter account
#[account(mut)]
pub user: Signer<'info>, // specify account must be signer on the transaction
// The counter account being created and initialized in the instruction
#[account(
init, // specifies we are creating this account
payer = user, // specifies account paying for the creation of the account
space = 8 + 8 // space allocated to the new account (8 byte discriminator + 8 byte for u64)
)]
pub counter: Account<'info, Counter>, // specify account is 'Counter' type
pub system_program: Program<'info, System>, // specify account must be System Program
}
// Account required by the increment instruction
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)] // specify account is mutable because we are updating its data
pub counter: Account<'info, Counter>, // specify account is 'Counter' type
}
// Define structure of `Counter` account
#[account]
pub struct Counter {
pub count: u64, // define count value type as u64
}
- build program
build
Output
$ build
Building...
Build successful. Completed in 3.82s.
- deploy program, airdrop if needed
deploy
Output
$ deploy
Deploying... This could take a while depending on the program size and network conditions.
Deployment successful. Completed in 17s.
- run test
test
Output
Running tests...
anchor.test.ts:
counter
Transaction Signature: 3pVEPm3SEzr64eLqkauMQBLWHQQ5aiZUoNDnwUWfSe1TGeTEP7njy7sGFgnG6mcEWE7BfAnLhSbZRhdCxyHvGDF9
Count: 0
✔ Is initialized! (2406ms)
Transaction Signature: 56aQPsuWjyDV2vhuvhtfZ5NkbnGmTuiQVQakUEM8mEE3HWGir9rufxKYaN8m6byX2urEwSDdNF8SHDkdzyEVtxc9
Count: 1
✔ Increment (897ms)
2 passing (3s)
Update Counter Account Type
- from
#[account]
pub struct Counter {
pub count: u64,
}
- to, add InitSpace constraint
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64, // 8 bytes
pub bump: u8, // 1 byte
}
Diff
#[account]
+ #[derive(InitSpace)]
pub struct Counter {
pub count: u64,
+ pub bump: u8,
}
build
Update Initialize Instruction
- add pda constraints
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init,
payer = user,
space = 8 + 8
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub user: Signer<'info>,
// Create and initialize `Counter` account using a PDA as the address
#[account(
init,
seeds = [b"counter"], // optional seeds for pda
bump, // bump seed for pda
payer = user,
space = 8 + Counter::INIT_SPACE
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
Diff
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub user: Signer<'info>,
- #[account(
- init,
- payer = user,
- space = 8 + 8
- )]
+ #[account(
+ init,
+ seeds = [b"counter"],
+ bump,
+ payer = user,
+ space = 8 + Counter::INIT_SPACE
+ )]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
- implement instruction
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &ctx.accounts.counter;
msg!("Counter account created! Current count: {}", counter.count);
Ok(())
}
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.bump = ctx.bumps.counter; // store bump seed in `Counter` account
msg!("Counter account created! Current count: {}", counter.count);
msg!("Counter bump: {}", counter.bump);
Ok(())
}
Diff
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
- let counter = &ctx.accounts.counter;
+ let counter = &mut ctx.accounts.counter;
+ counter.bump = *ctx.bumps.get("counter").unwrap(); // store bump seed in `Counter` account
msg!("Counter account created! Current count: {}", counter.count);
+ msg!("Counter bump: {}", counter.bump);
Ok(())
}
build
Update Increment Instruction
- define required accounts
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
// The address of the `Counter` account must be a PDA derived with the specified `seeds`
#[account(
mut,
seeds = [b"counter"], // optional seeds for pda
bump = counter.bump, // bump seed for pda stored in `Counter` account
)]
pub counter: Account<'info, Counter>,
}
Diff
#[derive(Accounts)]
pub struct Increment<'info> {
- #[account(mut)]
+ #[account(
+ mut,
+ seeds = [b"counter"],
+ bump = counter.bump,
+ )]
pub counter: Account<'info, Counter>,
}
- business logic stays the same
- todo, remove checked_add, not needed with anchor
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
msg!("Previous counter: {}", counter.count);
counter.count = counter.count.checked_add(1).unwrap();
msg!("Counter incremented! Current count: {}", counter.count);
Ok(())
}
build
Deploy Program
- deploy program, airdrop if needed
deploy
Output
$ deploy
Deploying... This could take a while depending on the program size and network conditions.
Deployment successful. Completed in 17s.
Derive PDA Client Side
- add pda
const [counterPDA] = PublicKey.findProgramAddressSync(
[Buffer.from("counter")],
program.programId,
);
Update Initialize Test
it("Is initialized!", async () => {
// Invoke the initialize instruction
const transactionSignature = await program.methods
.initialize()
.accounts({
counter: counterAccount.publicKey,
})
.signers([counterAccount]) // include counter keypair as additional signer
.rpc({ skipPreflight: true });
// Fetch the counter account data
const accountData = await program.account.counter.fetch(
counterAccount.publicKey,
);
console.log(`Transaction Signature: ${transactionSignature}`);
console.log(`Count: ${accountData.count}`);
});
Diff
todo
Update Increment Test
it("Increment", async () => {
// Invoke the increment instruction
const transactionSignature = await program.methods
.increment()
.accounts({
counter: counterAccount.publicKey,
})
.rpc();
// Fetch the counter account data
const accountData = await program.account.counter.fetch(
counterAccount.publicKey,
);
console.log(`Transaction Signature: ${transactionSignature}`);
console.log(`Count: ${accountData.count}`);
});
Diff
todo
Run Test
- run test
test
Output
Running tests...
anchor.test.ts:
counter
Transaction Signature: 3pVEPm3SEzr64eLqkauMQBLWHQQ5aiZUoNDnwUWfSe1TGeTEP7njy7sGFgnG6mcEWE7BfAnLhSbZRhdCxyHvGDF9
Count: 0
✔ Is initialized! (2406ms)
Transaction Signature: 56aQPsuWjyDV2vhuvhtfZ5NkbnGmTuiQVQakUEM8mEE3HWGir9rufxKYaN8m6byX2urEwSDdNF8SHDkdzyEVtxc9
Count: 1
✔ Increment (897ms)
2 passing (3s)
- run test, will fail initialize instrucion bc account using pda already exists
test
Output
todo
Close Program
- run test
solana program close <program_id>
solana program close J2WA6mGXrutGo1ZSRijNWh7tDPmLbZm8UmQfffwVEHhq
Output
$ solana program close J2WA6mGXrutGo1ZSRijNWh7tDPmLbZm8UmQfffwVEHhq
Loading Solana CLI...
Success.
Closed Program Id J2WA6mGXrutGo1ZSRijNWh7tDPmLbZm8UmQfffwVEHhq, 2.85580632 SOL reclaimed