← Solana Builder

Intermediate · LESSON 03 OF 06

3. Program-derived addresses

By the end, you can design deterministic state addresses without inventing private keys.

In this lesson8 field notes · practice · sources · checkpoint
Seed components and a program identifier produce a canonical off-curve address; deriving it creates no account and grants no authority by itself.
Seed components and a program identifier produce a canonical off-curve address; deriving it creates no account and grants no authority by itself.Open full-size visual ↗
Read a text version of this diagram

The program identifier and seed components are hashed to derive an off-curve address. Variable-length components need explicit boundaries so different logical tuples cannot collapse into the same seed bytes. Canonical bump search selects the expected address. The result has no private key; derivation does not create an account or grant a user permission.

  1. NAMESPACE
  2. WALLET + COURSE
  3. CANONICAL SEEDS
  4. UNIQUE CLAIM PDA
Two variable-length seed tuples collapse to identical bytes unless their boundary is encoded. A fixed-width or length-prefixed schema separates them. A canonical bump selects one PDA from multiple valid off-curve bump results, and on-chain validation enforces that address.
Encode seed boundaries explicitly, then validate the canonical bump so one logical record has one expected address.Open full-size visual ↗
FIELD NOTE 01

Deterministic addressing

A program-derived address is derived from seeds, a program ID, and a bump. It is off the Ed25519 curve and has no corresponding private key. The runtime can grant signer privilege for that address to its deriving program during an appropriately formed cross-program invocation.

FIELD NOTE 02

Address design is product design

For a learner credential, a seed namespace, learner public key, course identifier, and revision can define one deterministic claim address. This makes duplicate detection part of the account model. Validate seed lengths and encode variable input unambiguously. Creating the address and creating an account at that address are separate operations.

FIELD NOTE 03

Keep authority explicit

A predictable address is not a password. Everyone can derive it. Authorization comes from program checks and required signatures. Validate the expected derivation rather than accepting an arbitrary account merely because its data looks plausible.

FIELD NOTE 04

Seeds are a public address schema

A program-derived address is deterministically computed from a program ID, a sequence of seed bytes, and a bump that keeps the address off the Ed25519 curve. No private key exists for a PDA. A program can authorize it during a cross-program invocation by signing with the exact seeds and bump, which is why seed design defines a real authority boundary. Seeds are concatenated before hashing: for example, the tuples ["ab", "cd"] and ["abcd"] produce the same seed bytes if no boundary is encoded. Use a fixed domain prefix and unambiguous encodings such as fixed-width fields or explicit lengths. Validate the canonical bump and all user-controlled seeds. Document the byte-level formula as part of the program interface, then test expected derivations, wrong users, wrong mints, ambiguous inputs, and attempts to substitute another program’s PDA.

FIELD NOTE 05

Specify bytes, boundaries, and domain

A seed is a byte string, so specify the encoding of every input, not just its label. Use a fixed domain prefix to distinguish this account family from other addresses in the same program. Prefer fixed-size public-key bytes and bounded identifiers. Seeds are concatenated before hashing: ["ab", "cd"] and ["abcd"] therefore produce the same bytes unless your schema encodes their boundary. Use fixed widths, explicit lengths, or another injective encoding. Solana allows at most 16 seed slices in one derivation, each no longer than 32 bytes; the bump is appended as a seed, so count it in the 16. Encode oversized values deliberately across multiple bounded seeds without losing boundaries.

FIELD NOTE 06

Work a badge address from inputs to validation

Use a versioned schema such as [b"badge/v1", learner_pubkey_as_32_bytes, level_as_one_byte, revision_digest_as_32_bytes]. Each item is one bounded seed slice; the derivation helper appends the canonical bump as another slice. The prefix separates this account family and gives the byte layout a version. The client can calculate the address before it asks a program to act, but derivation alone neither creates the account nor authorizes a change. In Anchor, a typed account constraint can check the supplied address against these seeds and canonical bump, while `has_one` and field constraints compare the stored learner, level, and revision to the request. Those checks still do not replace the instruction's business authorization or account-creation rules. Changing the level or revision intentionally changes the address, so discovery and migration behavior must be part of the product policy.

FIELD NOTE 07

A public derivation is not private data

Anyone who knows the program ID and seed values can derive the PDA. Hashing or deriving an address does not conceal the learner, course, or revision when those inputs are public or enumerable. If a public account would reveal a sensitive relationship, do not put sensitive fields into its seeds and do not claim that an opaque-looking address provides privacy. For this academy, the public wallet address and level are already visible when a credential is minted, while email, legal name, score, and answer choices must stay off chain.

FIELD NOTE 08

Make canonical bumps part of validation

The SDK searches bump values in descending order and returns the first value that produces an off-curve address: the canonical bump. Other bump values can also yield valid off-curve addresses for the same application seeds. If a program accepts any such address, one logical record can have multiple PDAs, undermining one-account-per-learner or one-time-claim assumptions. Re-derive and validate the canonical address on chain; Anchor’s `seeds` and `bump` constraints are one way to enforce the expected relation. A bump is public and is not authorization. A derived address is also not an initialized account: account creation requires a separate instruction, payer, storage funding, and owner assignment. The lab tests address math only; local program execution and negative tests are needed to verify on-chain account rules.

MAKE THE CONNECTION

A detail worth keeping.

Every seed scheme has implications for uniqueness, privacy, and migration. Public keys and clear course labels in seeds can reveal relationships through account enumeration. Prefer deterministic but intentionally designed seed inputs, and do not mistake hashing public inputs for confidentiality.

Work through an example

A unique claim address derived from a namespace, wallet, tier, and revision distinguishes a retake or curriculum update if policy permits it. Define what a retake means before changing the seed: changing it without policy could accidentally let the same level mint again.

Illustrative Anchor account checks for a wallet-, level-, and revision-scoped badge. The BadgeRecord fields and instruction handler are omitted; this does not create an account or implement issuance.
#[derive(Accounts)]
#[instruction(level: u8, revision: [u8; 32])]
pub struct ReadBadge<'info> {
    pub learner: Signer<'info>,
    #[account(
        seeds = [b"badge/v1", learner.key().as_ref(), &[level], revision.as_ref()],
        bump,
        has_one = learner,
        constraint = badge.level == level,
        constraint = badge.revision == revision
    )]
    pub badge: Account<'info, BadgeRecord>,
}

Specify the exact seed byte schema for one badge per learner per level per curriculum revision. Include a fixed domain prefix, fixed-width wallet bytes, normalized level and revision encodings, unambiguous boundaries, and canonical bump policy. Demonstrate with fictional bytes why ["ab", "cd"] collides with ["abcd"] without boundary encoding, then show an injective encoding. Count the appended bump within the maximum 16 seed slices and keep each slice at or below 32 bytes. Explain how a noncanonical bump can create an alternate record address and name the on-chain check that rejects it. Use no real learner data.

GUIDED PRACTICE · INTERMEDIATE

Do the work, one step at a time.

Use fictional addresses and disposable test data. Do not enter a recovery phrase or private key. Most exercises work on paper or with a local test; only a lesson that clearly says “read-only Devnet” makes a public network request.

  1. Download and unzip PDA workbook 01; inspect its seed tuple before running code.
  2. Run npm ci with Node.js 20 or later to install the pinned Solana Kit dependency.
  3. Run npm test and explain how the suite checks stable derivation and learner/course/program separation.
  4. Derive a sample PDA using a public program address, public learner address, and lowercase course slug.
  5. Reject uppercase, Unicode, empty, and over-32-byte seed values; explain which authorization checks still belong on chain.
  6. Optionally compare the same fictional typed seeds with `solana find-program-derived-address` after checking the installed CLI version; this read-only derivation does not create an account or submit a transaction.

Before you move on, check your reasoning

  • Specify exact seed order and byte encoding.
  • Check the program-derived address for two fictional learners.
  • Explain why knowing a claim address does not authorize changing it.

OPEN SELF-CHECK · UNGRADED

Pause and test your reasoning

Several bumps produce viable off-curve addresses for the same seeds. Which address should a canonical PDA design use?

  1. The canonical bump returned by the standard derivation for those seeds and program ID.
  2. Any bump supplied by an untrusted caller, because every off-curve result is interchangeable.
  3. A random on-curve public key stored beside the PDA.
Reveal one best answer and its explanation

One best answer: The canonical bump returned by the standard derivation for those seeds and program ID.

A PDA is derived from seeds, a program ID, and a bump that yields an off-curve address. Use the canonical derivation consistently and validate it on-chain; accepting arbitrary alternatives can create multiple addresses for one logical record.

Sources: Program Derived Addresses (PDAs) · Program Derived Address with Anchor

This practice is not scored, saved, or part of a certificate. Account-based lesson checkpoints and level exams remain separate.

Never enter a recovery phrase or private key into a learning site or lesson exercise.

DOWNLOADABLE DEVELOPER LAB · OFFLINE

Derive a learner-scoped PDA.

This Solana Kit workbook derives a canonical address from a fixed domain seed, a public wallet address, and a course slug. It tests deterministic derivation, input separation, and seed limits locally. It makes no RPC request and contains no account-creation, signing, or fee-paying code.

Download PDA workbook 01

Requires Node.js 20+. Unzip the package, run npm ci, then npm test. Use only public addresses; never enter a private key or recovery phrase.

READ THE SOURCE

Go deeper in the official docs

Reviewed on 2026-09-28. Protocol documentation can change; check the linked page’s current version as you use this material.

  1. Program Derived Addresses (PDAs) ↗
  2. PDA Derivation ↗
  3. Program Derived Address with Anchor ↗
  4. Anchor Account Types ↗
  5. JavaScript and TypeScript SDK for Solana ↗
  6. Solana CLI Reference ↗

Report an issue with this lesson

OPEN LEARNING

Keep learning at your own pace.

This lesson, its guided practice, and all six course lessons are open to everyone. Account-based checkpoints, exams, and saved progress will open when free accounts are ready.

Continue to the next lesson ↗