Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/blueshift-gg/quasar/llms.txt

Use this file to discover all available pages before exploring further.

Program Derived Addresses (PDAs) are public keys that no private key corresponds to. They are derived deterministically from a set of seed bytes and a program ID, making them the canonical way to give a program ownership over on-chain state without a keypair. Quasar’s PDA implementation uses raw sol_sha256 and sol_curve_validate_point syscalls directly, cutting the per-attempt compute cost from ~1,500 CU to ~544 CU compared to sol_try_find_program_address.

How PDA Derivation Works

A PDA is the SHA-256 hash of seeds || bump_byte || program_id || "ProgramDerivedAddress" where the resulting 32 bytes fall off the Ed25519 curve. The derivation tries bump values from 255 down to 0 until an off-curve point is found.
PDA = sha256(seed₀ || seed₁ || … || bump || program_id || "ProgramDerivedAddress")
     where the result is off the Ed25519 curve
Storing the bump in your account data avoids re-deriving it on every instruction. Subsequent calls can call verify_program_address (~200 CU) which only hashes once and checks the result against the known address, rather than iterating.

Declaring Seeds with #[seeds]

Attach a #[seeds] attribute to an #[account] struct to declare typed PDA seed specs for that account type. The macro generates a seeds(arg0, arg1, …) associated function:
#[account(discriminator = 1, set_inner)]
#[seeds(b"escrow", maker: Address)]
pub struct Escrow {
    pub maker: Address,
    pub mint_a: Address,
    pub mint_b: Address,
    pub maker_ta_b: Address,
    pub receive: u64,
    pub bump: u8,  // stored bump enables BUMP_OFFSET fast path
}
The seed list (b"escrow", maker: Address) declares:
  • a static byte slice prefix b"escrow"
  • a dynamic argument maker of type Address
The generated Escrow::seeds(maker: &Address) function returns a seed spec that the address = constraint evaluates to verify (and if it’s an init, to derive) the PDA.

BUMP_OFFSET Fast Path

When the account struct contains a field named bump: u8, Discriminator::BUMP_OFFSET is automatically set to its byte offset within the account data. This lets verify_program_address (~200 CU) be used instead of based_try_find_program_address (~544 CU) when parsing already-existing accounts:
verify_program_address         ≈  200 CU  (one sha256 + equality check)
based_try_find_program_address ≈  544 CU  (iterating sha256 + curve checks, per attempt)
find_bump_for_address          ≈  544 CU  (iterating sha256 + keys_eq ~10 CU, per attempt)

The address = Constraint

In a #[derive(Accounts)] struct, use address = <expr> to assert that the account’s public key equals the result of a seed spec:
#[derive(Accounts)]
pub struct Make {
    #[account(mut)]
    pub maker: Signer,
    #[account(init, payer = maker, address = Escrow::seeds(maker.address()))]
    pub escrow: Account<Escrow>,
    // ...
}
For an init account the framework derives the PDA and stores the bump in ctx.bumps.escrow. For an existing account it reads the bump from account data (via BUMP_OFFSET) and calls verify_program_address.

PDA Functions

Quasar exposes three functions in quasar_lang::pda for working with PDAs directly:

verify_program_address

Verify that expected equals sha256(seeds || program_id || "ProgramDerivedAddress"). Seeds must already include the bump byte. ~200 CU. Use when you have the bump stored.

based_try_find_program_address

Iterate bump values 255→0, hashing each time and checking off-curve with sol_curve_validate_point. Returns (Address, bump). ~544 CU for bump=255. Use for init paths.

find_bump_for_address

Same iteration as above but replaces the sol_curve_validate_point (~100 CU) with a keys_eq comparison (~10 CU). Use when the expected PDA address is already known.

find_program_address_const

Compile-time PDA derivation using const_crypto. Useful for deriving constant PDAs in const items or type-level computations.

Direct Usage

use quasar_lang::pda;

// Verify with a known bump (fast path, ~544 CU)
let seeds = [
    pda::seed_bytes(b"escrow"),
    pda::seed_bytes(maker.address()),
    pda::seed_bytes(&[bump]),
];
pda::verify_program_address(&seeds, program_id, escrow.address())?;

// Find bump when address is already known
let bump = pda::find_bump_for_address(
    &[b"escrow", maker.address().as_ref()],
    program_id,
    escrow.address(),
)?;

CPI Signing with Stored Bumps

When a PDA must sign a CPI call, reconstruct the seed array from the stored bump and pass it via invoke_signed:
// From the escrow refund instruction
use quasar_lang::cpi::Seed;

let bump = [bumps.escrow];
let seeds = [
    Seed::from(b"escrow" as &[u8]),
    Seed::from(self.maker.address().as_ref()),
    Seed::from(bump.as_ref()),
];

self.token_program
    .transfer(
        &self.vault_ta_a,
        &self.maker_ta_a,
        &self.escrow,
        self.vault_ta_a.amount(),
    )
    .invoke_signed(&seeds)?;
invoke_signed accepts anything that implements CpiSignerSeeds — including [Seed<'_>; N] arrays and [Seed<'_>] slices.

Full Escrow PDA Workflow

1

Declare seeds on the account

#[account(discriminator = 1, set_inner)]
#[seeds(b"escrow", maker: Address)]
pub struct Escrow {
    pub maker: Address,
    // ... other fields ...
    pub bump: u8,
}
2

Initialize and store the bump

// In Make accounts struct:
#[account(init, payer = maker, address = Escrow::seeds(maker.address()))]
pub escrow: Account<Escrow>,

// In make_escrow handler method:
self.escrow.set_inner(EscrowInner {
    maker: *self.maker.address(),
    // ...
    bump: bumps.escrow,   // store the bump
});
3

Verify on subsequent instructions

// In Refund accounts struct:
#[account(
    mut,
    has_one(maker),
    close(dest = maker),
    address = Escrow::seeds(maker.address())
)]
pub escrow: Account<Escrow>,
// Reads bump from escrow.bump via BUMP_OFFSET → verify_program_address (~544 CU)
4

Sign CPIs with the stored bump

// In withdraw_tokens_and_close:
let bump = [bumps.escrow];
let seeds = [
    Seed::from(b"escrow" as &[u8]),
    Seed::from(self.maker.address().as_ref()),
    Seed::from(bump.as_ref()),
];
self.token_program
    .close_account(&self.vault_ta_a, &self.maker, &self.escrow)
    .invoke_signed(&seeds)
The address = constraint on an init account uses based_try_find_program_address which includes the on-curve check and is therefore safe even before the account exists on-chain. Non-init paths use find_bump_for_address which skips the on-curve check — correctness relies on the account already being in the transaction, meaning it exists on-chain and was therefore created as a valid off-curve PDA.

Build docs developers (and LLMs) love