Skip to main content
The SolBid program uses Program Derived Addresses (PDAs) for all on-chain accounts. This ensures deterministic account addressing and eliminates the need for users to manage keypairs for program accounts.

What are PDAs?

Program Derived Addresses are special Solana addresses that:
  • Are derived deterministically from seeds and a program ID
  • Have no corresponding private key (cannot sign transactions)
  • Can only be controlled by the program that derived them
  • Allow programs to “sign” for these addresses using invoke_signed
All SolBid game state accounts (GameState, PlayerState, Bid) use PDAs to ensure secure, deterministic addressing.

PDA derivation utilities

The program provides three utility functions for PDA derivation, located in utils.rs:

game_pda_seeds

Derives the PDA for a GameState account.
pub fn game_pda_seeds(game_id: u64, program_id: &Pubkey) -> (Pubkey, u8)
Seeds: ["game", game_id.to_le_bytes()] Returns: (Pubkey, u8) - The derived address and bump seed See utils.rs:13-15 for implementation.
game_id
u64
required
Unique game identifier. Each game_id produces a unique PDA.
program_id
&Pubkey
required
The SolBid program’s public key. Used by find_program_address to derive the PDA.
Example:
let (game_pda, game_bump) = game_pda_seeds(1, program_id);
// game_pda: Program-derived address for game #1
// game_bump: Bump seed (usually 255 or lower)

player_pda_seeds

Derives the PDA for a PlayerState account.
pub fn player_pda_seeds(
    game_id: u64,
    player_pubkey: &Pubkey,
    bid_count: u64,
    program_id: &Pubkey,
) -> (Pubkey, u8)
Seeds: ["player", game_id.to_le_bytes(), player_pubkey.as_ref(), bid_count.to_le_bytes()] Returns: (Pubkey, u8) - The derived address and bump seed See utils.rs:24-34 for implementation.
game_id
u64
required
The game identifier this player state belongs to.
player_pubkey
&Pubkey
required
The public key of the player who placed the bid.
bid_count
u64
required
The bid sequence number when this player state was created. This allows the same player to have multiple PlayerState accounts (one per bid).
program_id
&Pubkey
required
The SolBid program’s public key.
Example:
let player_key = Pubkey::new_unique();
let (player_pda, player_bump) = player_pda_seeds(
    1,              // game_id
    &player_key,    // player's pubkey  
    3,              // bid_count (this was their 3rd bid in the game)
    program_id
);
The same player bidding multiple times will have different PlayerState PDAs because bid_count changes.

bid_pda_seeds

Derives the PDA for a Bid account.
pub fn bid_pda_seeds(game_id: u64, bid_number: u64, program_id: &Pubkey) -> (Pubkey, u8)
Seeds: ["bid", game_id.to_le_bytes(), bid_number.to_le_bytes()] Returns: (Pubkey, u8) - The derived address and bump seed See utils.rs:17-22 for implementation.
game_id
u64
required
The game identifier this bid belongs to.
bid_number
u64
required
Sequential bid number (1, 2, 3, …). Matches the game’s total_bids count when this bid was placed.
program_id
&Pubkey
required
The SolBid program’s public key.
Example:
let (bid_pda, bid_bump) = bid_pda_seeds(
    1,          // game_id
    5,          // bid_number (5th bid in the game)
    program_id
);

PDA usage in instructions

CreateGame instruction

The CreateGame instruction creates three PDAs:
// Derive game PDA
let (game_pda, game_bump) = game_pda_seeds(game_id, program_id);
if *game_account.key != game_pda {
    return Err(BiddingError::InvalidGameAccount.into());
}

// Derive initial player PDA (bid_count = 1)
let (player_pda, player_bump) = player_pda_seeds(
    game_id,
    payer_account.key,
    1,
    program_id
);

// Derive initial bid PDA (bid_number = 1)  
let (bid_pda, bid_bump) = bid_pda_seeds(game_id, 1, program_id);
See instructions/create_game.rs:38-61 for full implementation.

PlaceBid instruction

The PlaceBid instruction creates two new PDAs:
let new_bid_count = game_state.total_bids + 1;

// Derive new player PDA
let (new_player_pda, new_player_bump) = player_pda_seeds(
    game_state.game_id,
    bidder_account.key,
    new_bid_count,
    program_id
);

// Derive new bid PDA
let (new_bid_pda, new_bid_bump) = bid_pda_seeds(
    game_state.game_id,
    new_bid_count,
    program_id
);
See instructions/place_bid.rs:56-75 for full implementation.

Creating accounts with PDAs

The program uses invoke_signed to create PDA accounts:
invoke_signed(
    &system_instruction::create_account(
        payer_account.key,
        game_account.key,
        rent,
        GAME_ACCOUNT_SIZE as u64,
        program_id,
    ),
    &[payer_account.clone(), game_account.clone(), system_program.clone()],
    &[&[b"game", &game_id.to_le_bytes(), &[game_bump]]], // Signer seeds
)?;
The signer seeds must match the PDA derivation exactly:
  • For GameState: ["game", game_id, bump]
  • For PlayerState: ["player", game_id, player_pubkey, bid_count, bump]
  • For Bid: ["bid", game_id, bid_number, bump]
See instructions/create_game.rs:68-102 for examples.

PDA verification

Every instruction verifies that provided accounts match expected PDAs:
let (expected_pda, _bump) = game_pda_seeds(game_id, program_id);
if *provided_account.key != expected_pda {
    return Err(BiddingError::InvalidGameAccount.into());
}
This prevents account substitution attacks where malicious users try to pass incorrect accounts.
Always verify PDAs before using accounts. Never trust that a user-provided account is the correct PDA.

PDA examples by account type

Game PDA

Seeds: ["game", game_id]
game_idDerived Address (example)
1Game11...xyz1
2Game22...abc2
100Game33...def3

Player PDA

Seeds: ["player", game_id, player_pubkey, bid_count]
game_idplayer_pubkeybid_countDerived Address (example)
1Alice…1Play11...aaa1
1Alice…5Play22...aaa5 (same player, different bid)
1Bob…2Play33...bbb2

Bid PDA

Seeds: ["bid", game_id, bid_number]
game_idbid_numberDerived Address (example)
11Bid111...xyz1
12Bid222...xyz2
21Bid333...abc1 (different game)

Client-side PDA derivation

Clients must derive PDAs before calling instructions:
import { PublicKey } from '@solana/web3.js';

const PROGRAM_ID = new PublicKey('Your_Program_ID');

// Derive game PDA
const [gamePDA] = await PublicKey.findProgramAddress(
  [
    Buffer.from('game'),
    Buffer.from(new Uint8Array(new BigUint64Array([gameId]).buffer))
  ],
  PROGRAM_ID
);

// Derive player PDA
const [playerPDA] = await PublicKey.findProgramAddress(
  [
    Buffer.from('player'),
    Buffer.from(new Uint8Array(new BigUint64Array([gameId]).buffer)),
    playerPublicKey.toBuffer(),
    Buffer.from(new Uint8Array(new BigUint64Array([bidCount]).buffer))
  ],
  PROGRAM_ID
);

// Derive bid PDA
const [bidPDA] = await PublicKey.findProgramAddress(
  [
    Buffer.from('bid'),
    Buffer.from(new Uint8Array(new BigUint64Array([gameId]).buffer)),
    Buffer.from(new Uint8Array(new BigUint64Array([bidNumber]).buffer))
  ],
  PROGRAM_ID
);
U64 values must be converted to little-endian byte arrays to match the Rust implementation’s to_le_bytes() format.

Benefits of PDA-based architecture

  1. Deterministic addressing - Anyone can derive account addresses without on-chain queries
  2. No keypair management - Users don’t need to generate or store keypairs for game accounts
  3. Security - Only the program can sign for PDAs, preventing unauthorized modifications
  4. Collision resistance - Unique seeds ensure different games/players/bids never collide
  5. Simplified client code - Clients just need game_id and other parameters to find accounts

Common PDA errors

When working with PDAs, watch for these errors:
  • InvalidGameAccount - Provided game account doesn’t match expected PDA
  • InvalidPlayerAccount - Provided player account doesn’t match expected PDA
  • InvalidBidAccount - Provided bid account doesn’t match expected PDA
  • InvalidNewPlayerAccount - New player PDA mismatch in PlaceBid
  • InvalidNewBidAccount - New bid PDA mismatch in PlaceBid
See error.rs:8-21 for error definitions.

Build docs developers (and LLMs) love