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.
Program Derived Address
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.
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.
On Curve Address
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.
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
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.
The example below derives a PDA without providing any optional seeds.
You can run this example on
Solana Playground. The PDA and
bump seed output will always be the same:
The next example below adds an optional seed "helloWorld".
You can also run this example on
Solana Playground. The PDA and
bump seed output will always be the same:
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.
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.
Run this example above on
Solana Playground. Given the
same seeds and program ID, the PDA output will match the previous one:
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.
Run the example on
Solana Playground and you
should see the following output:
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.
Warning
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.
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.
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.
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.
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.
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.
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.
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.