Solana - What is a Program Derived Address?

Solana - What is a Program Derived Address?

Photo by Tim Evans on https://unsplash.com/@tjevans

·

4 min read

Account

First, a little refresher: In Solana, Programs are stateless; they store data separately into what we call "Accounts." As described by the official doc:

Accounts are similar to files in operating systems such as Linux in that they may hold arbitrary data that persists beyond the lifetime of a program.

So an account is just how Solana deals with data on the chain. It is just a buffer of bytes.

Account ownership

Accounts usually have an associated pair of public and private keys...🤨 why?

Since Accounts are stored on the Blockchain and are accessible by everyone, there is a problem of permissions: How can we make sure that you own your data and your data does not get overridden by someone else?

Cryptography provides an answer to this problem; by using public and private key pairs, we can manage permissions in the Blockchain without the need for a central authority. If we imagine an account as a file:

  1. The file can be identified using a public key.
  2. The writing permission of that file can be authorized using a private key.

Public Key: In the same way that a Linux user uses a path to look up a file, we use an address to look up an account in Solana. The address is usually:

  1. an ed25519 public key or a hash of it
  2. a program-derived account address (more on this later)

Private Key:

  1. The private key is used to determine ownership and write permission of an account.

Problem

Now onto our problem: sometimes programs need to store state to be useful. Let's say you wanted to create a voting app where everyone can vote. You would need to keep track of who voted for what. As we discussed, in Solana, State is stored externally from a program into what we call "accounts."

As we said earlier, the public key is enough to read an account's data. However, sometimes a program also needs to write to an account owned by the user. In that case, how to give that permission to the program without giving away the user's private key?

const newAccount = anchor.web3.Keypair.generate()

const tx = await program.rpc.doSomething('hello world', {
  accounts: {
    systemProgram: anchor.web3.SystemProgram.programId,
    authority: program.provider.wallet.publicKey,
    someAccount: newAccount.publicKey,
  },
  signers: [newAccount],
});

That works, but what if we want to keep using that same account every time? We would have to keep the key pair somewhere in the code, which could be a security leak. If anyone gets their hand on the private key, they could start polluting that account as they wish.

Program Derived Address (PDA)

There is another way: programs can own accounts using PDAs. By assigning PDAs to accounts, a program can claim ownership of accounts without having to deal with public and private keys.

With PDAs, a program can sign for specific addresses without a private key. Since PDAs are not public keys, they have no associated private keys.

How to create a PDA?

A PDA is instead FOUND than created. Basically, you keep running a function until you get a string that does not have a private key (hopefully, we have that function that does this for us)

findProgramDerivedAddress(programId, seeds, seedBump)

It’s important to note that our seed does not have to be hardcoded. A common practice is to generate PDAs using the public key of your user's wallet, allowing our program to store information about that user in its standalone account so that you would end up with a feature similar to a hashmap:

const initialize = async () => {
  const { pda, bump } = await getProgramDerivedAddress();
  const program = await getProgram();
  try {
    await program.rpc.initialize(new BN(bump), {
      accounts: {
        user: getProvider().wallet.publicKey,
        baseAccount: pda,
        systemProgram: web3.SystemProgram.programId,
      },
    });
  }
  ...
};

const getProgramDerivedAddress = async () => {
  const [pda, bump] = await PublicKey.findProgramAddress(
    [Buffer.from('my_seed')],
    programAddress
  );
  console.log(`Got ProgramDerivedAddress: bump: ${bump}, pubkey: ${pda.toBase58()}`);
  return { pda, bump };
};

const getBaseAccount = async () => {
  const { pda } = await getProgramDerivedAddress();
  const program = await getProgram();
  try {
    return await program.account.baseAccount.fetch(pda);;
  }
  ...
};

And on Solana Side (we are using Anchor here)

#[program]
pub mod moon {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, base_account_bump: u8) -> ProgramResult {
        ctx.accounts.base_account.bump = base_account_bump;
        Ok(())
    }
}

#[derive(Accounts)]
#[instruction(base_account_bump: u8)]
pub struct Initialize<'info> {
    // space: depends on what you gonna store and is required if you use a vec or array
    #[account(init, seeds = [b"my_seed".as_ref()], bump = base_account_bump, payer = user, space = 9000)]
    pub base_account: Account<'info, BaseAccount>,
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
#[derive(Default)]
pub struct BaseAccount {
    pub bump: u8,
    ...
}

A full-stack example can be found here


Alternatives Way to use Accounts

Curious about other ways to store accounts?


LINKS


About Me

Let's Build on Solana!

Web3 Tweets

Did you find this article valuable?

Support mwrites by becoming a sponsor. Any amount is appreciated!