Deep Dive Technical Guide

Understanding
Solana Internals

A comprehensive guide to Solana's architecture, account model, program execution, consensus, security, and optimization — written for Rust developers ready to build on-chain.

~400ms
Block Time
65k+
TPS Theoretical
$0.00025
Avg TX Cost
~6.4s
Finality
Scroll to explore ↓
Chapter 01

The Mental Model

If you only take one thing from this guide, take this: Solana is a database of accounts. Everything — wallets, tokens, NFTs, smart contracts, on-chain state — is just an account. Programs read and write to accounts. That is the entire model.

Core Axiom
Everything Is an Account

Solana's global state is a gigantic key-value store. The key is a 32-byte public key (address). The value is an account — a struct holding lamports, a data byte array, an owner program ID, and some flags. There is nothing else. No storage slots, no contract state, no special objects. Just accounts.

Core Axiom
Programs Are Stateless

Unlike Ethereum where a contract stores its own state, Solana programs store zero state. A program is just compiled code living in an executable account. All state lives in separate data accounts that the program owns. When a transaction invokes a program, it passes in the accounts the program needs to read or write. The program processes them and returns.

This separation of code and state is what enables Sealevel's parallel execution — the runtime knows exactly which memory each transaction touches because accounts are passed explicitly.

The Solana Model vs Ethereum Model
graph LR subgraph ETH["Ethereum Model"] C1["Contract A"] C1 --- S1["storage slot 0"] C1 --- S2["storage slot 1"] C1 --- S3["storage slot N"] end subgraph SOL["Solana Model"] P1["Program X
code only, no state"] P1 -.->|reads/writes| A1["Account 1
owned by X"] P1 -.->|reads/writes| A2["Account 2
owned by X"] P1 -.->|reads/writes| A3["Account N
owned by X"] end style ETH fill:#1a1218,stroke:#FF4466,color:#e4e4f0 style SOL fill:#0f1a14,stroke:#14F195,color:#e4e4f0 style P1 fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style A1 fill:#0f1a1a,stroke:#00D4FF,color:#e4e4f0 style A2 fill:#0f1a1a,stroke:#00D4FF,color:#e4e4f0 style A3 fill:#0f1a1a,stroke:#00D4FF,color:#e4e4f0

This distinction has profound implications. On Ethereum, you call a contract and it reads its own storage. On Solana, you tell the runtime which accounts to load, then the program processes them. This means the runtime can schedule non-overlapping transactions in parallel — if Transaction A writes to Account 1 and Transaction B writes to Account 2, they execute simultaneously.

Why This Matters for You

As a Rust developer building on Solana, your entire job boils down to three things:

Task 1
Design Accounts

Define the data layout of your on-chain state. What fields? How many bytes? Which program owns them?

Task 2
Write Instructions

Write program logic that validates, reads, and writes account data in response to user instructions.

Task 3
Validate Everything

Ensure every account passed to your program is exactly what you expect. This is where security lives or dies.

Chapter 02

Accounts Deep Dive

An account is Solana's atomic unit of state. Every single byte of data that exists on-chain lives inside an account. Understanding their structure and rules is non-negotiable.

Definition
Account

A record in Solana's global state indexed by a 32-byte public key. Contains six fields: lamports (u64 balance), data (byte array of arbitrary content), owner (program ID that controls it), executable (bool flag), rent_epoch (u64), and the implicit key (its address). Maximum data size: 10 MB. Minimum balance determined by rent exemption.

Account Memory Layout
graph TB subgraph ACC["Account at Address: 7xKX...9fG2"] KEY["key: Pubkey — 32 bytes, the address itself"] LAM["lamports: u64 — balance in lamports, 1 SOL = 10^9"] DATA["data: Vec of u8 — arbitrary bytes, your program state"] OWNER["owner: Pubkey — the program that controls this account"] EXEC["executable: bool — if true, this account contains runnable BPF code"] RENT["rent_epoch: u64 — last epoch rent was collected"] end KEY --- LAM --- DATA --- OWNER --- EXEC --- RENT style ACC fill:#12121e,stroke:#9945FF,color:#e4e4f0 style KEY fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style DATA fill:#0f1a1a,stroke:#00D4FF,color:#e4e4f0 style OWNER fill:#0f1a14,stroke:#14F195,color:#e4e4f0

The Five Iron Rules of Accounts

These rules are enforced by the Solana runtime. Violating them causes your transaction to fail. Understand them deeply — they define what is and is not possible on Solana.

Rule 1
Only the Owner Can Modify Data

If an account's owner field is set to Program X, then only Program X can change the data bytes. Any other program's attempt to write to that account will be rejected by the runtime. However, any program can read any account's data — there is no read protection on Solana. All on-chain data is public.

Rule 2
Only the System Program Can Debit Lamports

Transferring SOL (debiting lamports) from an account requires the System Program. However, any program can credit lamports to any account — you can always add money, but only the System Program (or the owning program for PDAs) can take it out.

Rule 3
Only the Owner Can Assign a New Owner

Ownership transfer is one-way and permanent. Once you assign an account to a different program, only that new program can change it back. This is how account creation works: the System Program creates the account (owns it initially), then assigns ownership to your program.

Rule 4
Accounts Must Be Rent-Exempt

Every account must hold enough lamports to cover approximately 2 years of rent (~6,960 lamports per byte). Accounts below this threshold can be garbage-collected. In practice, you always make accounts rent-exempt at creation. The rent deposit is recoverable — when you close an account, you get the lamports back.

Rule 5
Account Data Must Be Pre-Allocated

Unlike Ethereum's dynamic storage, Solana accounts have a fixed size declared at creation. You cannot resize an account after creation (except via realloc, which has limitations). You must know your data layout upfront and allocate sufficient space, including padding for future fields.

Account Types in Practice

Type
System Account (Wallet)

Owned by the System Program (11111...1111). Has a lamport balance and empty data. This is what a keypair generates. The holder of the private key can sign transactions to transfer SOL.

Type
Program Account

Stores compiled BPF bytecode. Marked executable: true. Owned by the BPF Loader. Immutable once deployed (unless using an upgrade authority). A program's code and its data are always separate accounts.

Type
Data Account

Holds serialized state for a program. Owned by the program that created it. This is where your application's state lives — vault balances, user profiles, game state, anything. Anchor accounts always start with an 8-byte discriminator.

Type
PDA (Program Derived Address)

A special address derived deterministically from seeds + program ID. No private key exists. Only the deriving program can sign for it. The backbone of program-controlled vaults, escrows, and autonomous on-chain logic.

Data Serialization — How Bytes Map to Structs

Solana accounts store raw bytes. Your program must serialize (write) and deserialize (read) structs to/from those bytes. The standard is Borsh (Binary Object Representation Serializer for Hashing) — a deterministic, schema-based binary format.

Rust
// Your Anchor account struct
#[account]
#[derive(InitSpace)]
pub struct Vault {
    pub owner: Pubkey,       // 32 bytes
    pub balance: u64,       // 8 bytes
    pub created_at: i64,    // 8 bytes
    pub is_locked: bool,    // 1 byte
    pub bump: u8,           // 1 byte
}
// Total: 8 (discriminator) + 32 + 8 + 8 + 1 + 1 = 58 bytes
// The Anchor discriminator (first 8 bytes) = SHA256("account:Vault")[..8]
// Rent-exempt minimum ≈ 58 × 6,960 = ~403,680 lamports ≈ 0.0004 SOL
Rust TypeBorsh SizeSolana Usage
bool1 byteFlags, initialization state
u8 / i81 byteBumps, enum discriminators
u32 / i324 bytesShort counters
u64 / i648 bytesLamports, timestamps, token amounts
u12816 bytesLarge math, price accumulators
Pubkey32 bytesAddresses, owner references
String4 + lenMetadata (4-byte length prefix)
Vec<T>4 + (n × size(T))Dynamic lists (pre-allocate max!)
Option<T>1 + size(T)Optional fields (1-byte tag)
Anchor discriminator8 bytesFirst 8 bytes of every Anchor account
Chapter 03

Programs & Execution

A Solana program is a stateless function: it takes in a list of accounts and instruction data, executes logic, mutates account state, and returns success or failure. That's it.

Definition
Program

Compiled BPF/SBF bytecode stored in an executable account. A program exposes a single entrypoint function that receives three arguments: program_id (its own address), accounts (array of account references), and instruction_data (opaque bytes). The program deserializes the instruction data, validates accounts, executes logic, and writes back to accounts.

Program Execution Pipeline
sequenceDiagram participant C as Client participant R as RPC Node participant RT as Sealevel Runtime participant P as Your Program participant A as Accounts DB C->>R: Submit signed transaction R->>RT: Forward to leader validator RT->>RT: Verify all signatures RT->>A: Load referenced accounts RT->>P: entrypoint(program_id, accounts, data) P->>P: Deserialize instruction data P->>P: Validate accounts and constraints P->>A: Read current state P->>P: Execute business logic P->>A: Write updated state P-->>RT: Return Ok or ProgramError RT->>RT: Commit changes if Ok, rollback if Err RT-->>R: Confirmation R-->>C: Transaction result

The Raw Entrypoint vs Anchor

You can write programs at two levels. The raw entrypoint gives you maximum control. Anchor gives you maximum productivity. Here's the same logic at both levels:

Raw Solana
// Raw entrypoint — you handle EVERYTHING manually
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint, entrypoint::ProgramResult,
    msg, program_error::ProgramError, pubkey::Pubkey,
};

entrypoint!(process_instruction);

fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    let vault = next_account_info(accounts_iter)?;
    let user = next_account_info(accounts_iter)?;

    // YOU must manually check: is the user a signer?
    if !user.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    // YOU must check: does this program own the vault?
    if vault.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }
    // YOU must deserialize, validate, process, serialize...
    msg!("Processing instruction");
    Ok(())
}
Anchor
// Anchor — declarative constraints handle validation automatically
use anchor_lang::prelude::*;

#[program]
pub mod vault {
    use super::*;

    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
        // Signer check? Anchor did it (Signer type).
        // Owner check? Anchor did it (Account type).
        // PDA derivation? Anchor did it (seeds + bump).
        // Deserialization? Anchor did it.
        // You just write the business logic:
        ctx.accounts.vault.balance = ctx.accounts.vault.balance
            .checked_add(amount)
            .ok_or(VaultError::Overflow)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(
        mut,
        seeds = [b"vault", user.key().as_ref()],
        bump = vault.bump,
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

Native Programs — Solana's Built-in Infrastructure

Solana ships with programs that handle core functionality. Your programs interact with them via CPI.

ProgramAddress PrefixResponsibility
System Program1111...1111Create accounts, transfer SOL, assign owners, allocate space
SPL TokenTokenkegQ...Create/transfer/burn fungible and non-fungible tokens
Token-2022TokenzQdB...Extended token program: transfer fees, metadata, confidential transfers
Associated TokenATokenGPv...Derive deterministic token account addresses per wallet per mint
BPF Loader v2BPFLoade...Deploy and upgrade user programs
Compute BudgetComputeB...Request additional CU, set priority fees
Address Lookup TableAddressL...Create/manage ALTs for versioned transactions
SysvarsVariousClock (timestamp), Rent (min balance), EpochSchedule, SlotHashes
Chapter 04

Transactions

A transaction is an atomic bundle of instructions. All succeed or all fail. Understanding their structure, limits, and lifecycle is critical for building production systems.

Definition
Transaction

An atomic, ordered set of one or more instructions submitted to the network for execution. Contains: an array of Ed25519 signatures, and a message comprising a header, account address array, recent blockhash, and instruction array. Hard-capped at 1,232 bytes.

Transaction Structure
graph TB subgraph TX["Transaction — max 1,232 bytes"] subgraph SIG["Signatures"] S1["Ed25519 Sig 1 — fee payer"] S2["Ed25519 Sig 2 — co-signer"] end subgraph MSG["Message"] H["Header: num_required_sigs, num_readonly_signed, num_readonly_unsigned"] K["Account Keys: ordered array of all Pubkeys"] BH["Recent Blockhash: 32 bytes, prevents replay, expires in ~60-90s"] subgraph IXS["Instructions"] I1["IX 1: program_id_index, account_indices, data_bytes"] I2["IX 2: program_id_index, account_indices, data_bytes"] end end end style TX fill:#12121e,stroke:#FFCF36,color:#e4e4f0 style SIG fill:#1a1a10,stroke:#FFCF36,color:#e4e4f0 style MSG fill:#0f1a1a,stroke:#00D4FF,color:#e4e4f0 style IXS fill:#0f1a14,stroke:#14F195,color:#e4e4f0
Definition
Instruction

The atomic unit of execution. Each instruction invokes exactly one program and specifies: which program to call (by index into the account keys array), which accounts the program needs (each tagged as signer and/or writable), and opaque data bytes that the program interprets as its payload (e.g., instruction variant + arguments).

Definition
Recent Blockhash

A 32-byte hash of a recent block, included in every transaction. It serves two purposes: replay protection (the network deduplicates transactions by signature, and the blockhash ensures the same logical transaction produces a different signature each time) and expiry (transactions are dropped if the blockhash is older than ~150 slots / ~60 seconds).

Transaction Lifecycle

From Creation to Finality
graph LR A["Build TX"] --> B["Sign with
private key"] B --> C["Submit to
RPC node"] C --> D["Forwarded to
current leader"] D --> E["Leader includes
in block"] E --> F["Validators
replay and vote"] F --> G["Processed"] G --> H["Confirmed
1+ votes"] H --> I["Finalized
31+ votes"] style A fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style E fill:#0f1a14,stroke:#14F195,color:#e4e4f0 style I fill:#0f1a1a,stroke:#00D4FF,color:#e4e4f0

Account Tags — The Key to Parallelism

Every account referenced in a transaction is tagged with metadata. The runtime uses these tags to schedule parallel execution:

TagMeaningRuntime Impact
is_signer: trueTransaction must include a valid Ed25519 signature from this keySignature verification before execution
is_writable: trueThe instruction may modify this account's data or lamportsWrite-locked — no other TX can write simultaneously
Read-onlyNeither signer nor writableRead-shared — multiple TXs can read in parallel

Parallelism insight: Two transactions that only overlap on read-only accounts execute in parallel. Transactions that share a writable account must be serialized. This is why popular accounts (like a DEX pool) can become bottlenecks — every swap writes to the same account, forcing sequential execution.

Versioned Transactions & Address Lookup Tables

Definition
Versioned Transaction (v0)

An enhanced transaction format that supports Address Lookup Tables (ALTs). Legacy transactions include every account address inline (32 bytes each), limiting them to ~35 accounts. Versioned transactions reference ALT entries by 1-byte index, allowing up to 256 accounts per transaction while staying within the 1,232-byte limit.

Definition
Address Lookup Table (ALT)

An on-chain account storing a list of up to 256 public keys. Transactions reference these keys by index (1 byte) instead of inline (32 bytes). ALTs are essential for complex DeFi transactions that touch many accounts — swaps across multiple pools, liquidation bots, and batch operations.

Chapter 05

Consensus — PoH & Tower BFT

Solana doesn't use a single consensus mechanism. It uses a combination of Proof of History (a verifiable clock), Tower BFT (a PBFT variant), and several propagation innovations to achieve sub-second block times.

Definition
Proof of History (PoH)

A sequential chain of SHA-256 hashes where each hash includes the previous hash as input: hash_n = SHA256(hash_n-1). Because SHA-256 is inherently sequential (you cannot parallelize it), the number of hashes between two events proves that real wall-clock time has elapsed. Transactions are mixed into the hash chain, giving them a cryptographic timestamp before consensus.

PoH is NOT a consensus mechanism — it's a clock. It provides a globally-consistent ordering of events, allowing validators to agree on transaction order without communicating. This removes the most expensive part of traditional BFT consensus.

Proof of History Hash Chain
graph LR H1["SHA256
hash_1"] --> H2["SHA256
hash_2"] H2 --> H3["SHA256
hash_3"] H3 --> TX["TX inserted
at hash_3"] TX --> H4["SHA256
hash_3 + TX"] H4 --> H5["SHA256
hash_5"] H5 --> H6["SHA256
hash_6"] style TX fill:#0f1a14,stroke:#14F195,color:#e4e4f0 style H1 fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style H2 fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style H3 fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style H4 fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style H5 fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style H6 fill:#1a1230,stroke:#9945FF,color:#e4e4f0
Definition
Tower BFT

Solana's consensus algorithm — a PBFT (Practical Byzantine Fault Tolerance) variant optimized with PoH. Validators vote on the PoH-ordered ledger. Key innovation: exponential lockouts. Each consecutive vote on the same fork doubles the lockout period (the time the validator must wait to switch forks). After 32 votes, the lockout is ~2^32 slots — effectively permanent. This creates rapid economic finality without the O(n²) message complexity of traditional PBFT.

Tower BFT Voting with Exponential Lockout
sequenceDiagram participant L as Leader participant PH as PoH Generator participant V1 as Validator A participant V2 as Validator B participant V3 as Validator C L->>PH: Produce PoH stream with ordered TXs PH->>V1: Shreds via Turbine tree PH->>V2: Shreds via Turbine tree PH->>V3: Shreds via Turbine tree Note over V1,V3: Each validator replays and verifies V1->>L: Vote slot N, lockout=2 V2->>L: Vote slot N, lockout=2 V3->>L: Vote slot N, lockout=2 V1->>L: Vote slot N+1, lockout=4 V2->>L: Vote slot N+1, lockout=4 Note over V1,V3: Lockout doubles each vote: 2, 4, 8, 16, 32... Note over L: 2/3 stake voted on slot N = CONFIRMED

The Eight Innovations

Innovation
Sealevel

Parallel smart contract runtime. Transactions declare which accounts they read/write. Non-overlapping transactions execute simultaneously on separate cores. This is Solana's throughput multiplier.

Innovation
Turbine

Block propagation protocol inspired by BitTorrent. The leader breaks blocks into small packets called shreds and distributes them through a tree of validators. Each node only needs to forward a fraction of the data, eliminating the leader as a bottleneck.

Innovation
Gulf Stream

Mempool-less transaction forwarding. Transactions are pushed directly to the expected next leaders, not held in a global mempool. This reduces confirmation latency and memory pressure across the network.

Innovation
Cloudbreak

Horizontally-scaled accounts database using memory-mapped files. Supports concurrent reads and writes across SSDs, enabling the throughput needed for Sealevel's parallel execution.

Definition
Slot

A ~400ms window during which one leader produces a block. A leader gets 4 consecutive slots (~1.6s). If the leader fails to produce, the slot is skipped.

Definition
Epoch

A period of ~432,000 slots (~2-3 days). Stake delegations, validator rewards, and the leader schedule are recalculated at epoch boundaries. Leader schedule is known 2 epochs ahead.

Chapter 06

PDAs & Cross-Program Invocations

PDAs and CPIs are the composability primitives of Solana. PDAs let programs own accounts autonomously. CPIs let programs call other programs. Together, they enable the entire DeFi stack.

Definition
PDA (Program Derived Address)

An address computed from SHA256(seeds + program_id + bump) that is guaranteed to not lie on the Ed25519 curve. Because it's off-curve, no private key exists — nobody can produce a valid signature for it externally. Only the deriving program can authorize operations on behalf of the PDA by providing the seeds and bump in a CPI signer context. The bump (also called nonce) is the highest value 0-255 that pushes the hash off the curve.

PDA Derivation
graph LR S["Seeds: b_vault + user_pubkey"] --> SHA["SHA256 hash"] P["Program ID"] --> SHA B["Bump: 253"] --> SHA SHA --> CHECK{"On Ed25519 curve?"} CHECK -->|Yes| RETRY["Decrement bump, retry"] RETRY --> SHA CHECK -->|No| PDA["Valid PDA Address"] style S fill:#0f1a1a,stroke:#00D4FF,color:#e4e4f0 style P fill:#0f1a14,stroke:#14F195,color:#e4e4f0 style PDA fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style CHECK fill:#1a1a10,stroke:#FFCF36,color:#e4e4f0
Rust — PDA in Anchor
// Finding a PDA: deterministic from seeds + program_id
let (vault_pda, bump) = Pubkey::find_program_address(
    &[b"vault", user.key().as_ref()],
    program_id,
);

// Anchor constraint — validates PDA derivation automatically
#[account(
    init,
    payer = user,
    space = 8 + Vault::INIT_SPACE,
    seeds = [b"vault", user.key().as_ref()],
    bump,
)]
pub vault: Account<'info, Vault>,

// On subsequent calls, verify the PDA:
#[account(
    mut,
    seeds = [b"vault", user.key().as_ref()],
    bump = vault.bump, // stored bump for efficiency
)]
pub vault: Account<'info, Vault>,

Best practice: Always store the bump in the account data. Re-deriving with find_program_address costs ~500 CU per call because it iterates bump values. Using a stored bump with create_program_address costs ~20 CU.

Definition
CPI (Cross-Program Invocation)

When program A calls program B during execution. For example, your vault program calling the SPL Token Program to transfer tokens. CPIs allow programs to compose like building blocks. Maximum nesting depth: 4 levels. The calling program can pass its PDA signer seeds so the target program sees the PDA as a signer — this is how programs authorize token transfers from their vaults.

Rust — CPI Token Transfer from PDA
use anchor_spl::token::{self, Transfer, Token};

pub fn withdraw_tokens(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    // PDA signer seeds — the runtime verifies these derive the PDA
    let seeds = &[
        b"vault",
        ctx.accounts.user.key.as_ref(),
        &[ctx.accounts.vault.bump],
    ];
    let signer = &[&seeds[..]];

    // CPI: call Token Program's transfer instruction
    let cpi_ctx = CpiContext::new_with_signer(
        ctx.accounts.token_program.to_account_info(),
        Transfer {
            from: ctx.accounts.vault_token_account.to_account_info(),
            to: ctx.accounts.user_token_account.to_account_info(),
            authority: ctx.accounts.vault.to_account_info(), // PDA authority
        },
        signer, // PDA seeds prove the vault authorizes this
    );
    token::transfer(cpi_ctx, amount)?;
    Ok(())
}
Chapter 07

SPL Tokens & NFTs

Unlike Ethereum, Solana uses a single shared Token Program for ALL tokens. This means token logic is not duplicated across thousands of contracts — it's one battle-tested program managing every fungible and non-fungible token through separate accounts.

SPL Token Architecture
graph TB TP["SPL Token Program
single program, all tokens"] -->|owns| M["Mint Account
supply: 1000000
decimals: 6
mint_authority: Alice"] TP -->|owns| TA1["Alice Token Account
mint: USDC
owner: Alice wallet
amount: 500000"] TP -->|owns| TA2["Bob Token Account
mint: USDC
owner: Bob wallet
amount: 250000"] M -.->|tracks supply of| TA1 M -.->|tracks supply of| TA2 style TP fill:#0f1a14,stroke:#14F195,color:#e4e4f0 style M fill:#1a1230,stroke:#9945FF,color:#e4e4f0 style TA1 fill:#0f1a1a,stroke:#00D4FF,color:#e4e4f0 style TA2 fill:#0f1a1a,stroke:#00D4FF,color:#e4e4f0
Definition
Mint Account

Defines a token type. Stores: total supply, decimals (6 for USDC, 9 for SOL), mint authority (can create new tokens, or null for fixed supply), and freeze authority (can freeze token accounts). One mint per token type.

Definition
Token Account

Holds a user's balance of one specific token. Stores: which mint it belongs to, the owner wallet that controls it, and the amount. Each user needs a separate token account for each token they hold. Owned by the Token Program.

Definition
Associated Token Account (ATA)

A deterministically derived token account address from PDA(wallet_pubkey, token_program, mint_pubkey). Convention: every wallet has exactly one ATA per mint. You can compute anyone's ATA address client-side without querying the chain.

Definition
Token-2022 (Token Extensions)

Next-gen token program with modular extensions: transfer fees, non-transferable (soulbound) tokens, interest-bearing tokens, confidential transfers, on-mint metadata, permanent delegate, default account state, and more.

NFTs on Solana

Definition
NFT

An SPL token with supply = 1, decimals = 0, plus a Metaplex Metadata account. The metadata stores: name, symbol, URI (pointing to off-chain JSON with image, attributes), seller fee basis points (royalties), and verified creator list. Compressed NFTs (cNFTs) use Merkle trees to store millions of NFTs at ~$0.0005 each vs ~$2 for standard NFTs.

Chapter 08

Security — The Hard Part

The majority of Solana exploits share one root cause: insufficient account validation. Your program receives arbitrary accounts from arbitrary callers. If you don't verify every single assumption about those accounts, attackers will pass in crafted accounts that subvert your logic.

Security Axiom
Trust Nothing. Validate Everything.

Every account passed to your program is user-supplied. The runtime guarantees signatures and basic ownership — everything else is your responsibility. An attacker can pass any account address, any data, any program. Your program must verify that every account is exactly what it expects.

The Vulnerability Catalog

Vulnerability #1
Missing Signer Check

What happens: Your program modifies state without verifying that the caller actually signed the transaction. An attacker calls your withdraw function and passes someone else's vault — and since you never checked that the vault owner signed, the transaction succeeds.

Fix: Always use Signer<'info> in Anchor, or manually verify account.is_signer in raw programs. Never trust that an account is who it claims to be without a signature.

Vulnerable
// BAD: Anyone can call this, no signer verification
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let vault = &mut ctx.accounts.vault;
    vault.balance -= amount; // ← No check that the caller owns this vault!
    Ok(())
}
Secure
// GOOD: Anchor verifies signer + has_one constraint
#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(
        mut,
        seeds = [b"vault", owner.key().as_ref()],
        bump = vault.bump,
        has_one = owner @ VaultError::Unauthorized, // ← verifies vault.owner == owner.key()
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut)]
    pub owner: Signer<'info>, // ← must have signed the TX
}
Vulnerability #2
Missing Owner Check

What happens: Your program deserializes an account without verifying that the expected program owns it. An attacker creates their own account with fake data that matches your struct layout, passes it in, and your program treats it as legitimate.

Fix: Anchor's Account<'info, T> automatically verifies the account owner matches the declaring program AND checks the 8-byte discriminator. In raw programs, always check account.owner == expected_program_id.

Vulnerability #3
Integer Overflow / Underflow

What happens: Using standard arithmetic (+, -, *) can silently wrap around. A balance of 1 minus 2 wraps to u64::MAX (18,446,744,073,709,551,615 lamports). An attacker turns 1 lamport into 18 billion SOL.

Fix: Use checked_add, checked_sub, checked_mul, checked_div for ALL arithmetic. Return an error on overflow instead of wrapping.

Safe Arithmetic
// ALWAYS use checked math on-chain
vault.balance = vault.balance
    .checked_sub(amount)
    .ok_or(VaultError::Overflow)?;

user_balance = user_balance
    .checked_add(amount)
    .ok_or(VaultError::Overflow)?;

// For complex math, use u128 intermediate values
let fee = (amount as u128)
    .checked_mul(fee_bps as u128).unwrap()
    .checked_div(10_000).unwrap() as u64;
Vulnerability #4
Reinitialization Attack

What happens: An attacker calls your initialize instruction on an account that's already been initialized, resetting its state. If your vault held 100 SOL and the balance gets reset to 0, those funds are effectively stolen.

Fix: Anchor's #[account(init)] uses a discriminator and will fail if the account already has one. In raw programs, always check an is_initialized flag and reject if already set.

Vulnerability #5
PDA Substitution

What happens: An attacker passes a PDA derived with different seeds than expected. Your program assumes the PDA matches certain seeds (e.g., ["vault", user_key]) but never verifies the derivation. The attacker passes a PDA derived from ["vault", attacker_key] and accesses someone else's vault.

Fix: Always re-derive the PDA in your validation. Anchor's seeds = [...], bump constraint does this automatically.

Vulnerability #6
Closing Account Without Zeroing Data

What happens: When closing an account, if you only transfer lamports without zeroing the data, the account can be re-opened in the same transaction (by another instruction that sends it lamports before the runtime garbage-collects it). The stale data is now live again.

Fix: Use Anchor's #[account(close = recipient)] which automatically zeroes data, transfers lamports, and sets the discriminator to a closed state. In raw code, always memset data to zero before transferring lamports out.

Vulnerability #7
Arbitrary CPI Target

What happens: Your program makes a CPI to a program address passed by the user without verifying it's the expected program. An attacker passes their own malicious program that mimics the interface but does something different — like approving unlimited token spending.

Fix: Use Anchor's Program<'info, Token> which verifies the program ID. In raw code, hardcode expected program IDs and compare: if token_program.key() != spl_token::ID.

Security Checklist

Before every deployment, verify:

✓ Every signer is checked (Anchor: Signer type) • Every account owner is validated (Anchor: Account<T>) • Every PDA derivation is verified (seeds + bump) • All arithmetic uses checked operations • has_one constraints link accounts to their owners • Close instructions zero data before lamport transfer • CPI targets are hardcoded or validated • No reinitialization possible • Token account mints are verified • No unchecked account casting or raw deserialization

Chapter 09

Optimization

Solana programs run in a constrained environment: limited compute units, limited stack, limited transaction size. Optimized programs are cheaper, faster, and less likely to hit runtime limits.

Definition
Compute Units (CU)

Solana's measure of computational cost. Default budget: 200,000 CU per transaction. Maximum: 1,400,000 CU (requested via ComputeBudget instruction). Every BPF instruction, syscall, memory access, and CPI costs CU. Exceeding the budget fails the transaction. Priority fees are priced per CU: total_fee = base_fee + (CU_price × CU_consumed).

Compute Unit Costs — Know Your Budget

OperationApprox CU CostOptimization
SHA-256 hash (64 bytes)~100 CUMinimize hashing in loops
find_program_address~1,500 CUStore bump, use create_program_address (~20 CU)
CPI call~1,000+ CU overheadBatch operations, reduce CPI count
Borsh deserialize~200-1,000 CUUse zero-copy for large accounts
msg! logging~100 CU per callRemove in production
Create account (CPI to System)~5,000 CUPre-create accounts off-chain when possible
Token transfer (CPI to Token)~4,500 CUBatch transfers, use Transfer checked
Memory allocation (heap)~50 CU / allocationReuse buffers, prefer stack

Optimization Techniques

1. Store the PDA Bump

Rust
// SLOW: re-derives bump every time (~1,500 CU)
let (pda, bump) = Pubkey::find_program_address(&[b"vault", user.as_ref()], pid);

// FAST: use stored bump (~20 CU)
let pda = Pubkey::create_program_address(
    &[b"vault", user.as_ref(), &[stored_bump]],
    pid
)?;

2. Zero-Copy Deserialization for Large Accounts

Rust
// Normal: copies entire account data into a struct (~1,000 CU for big accounts)
#[account]
pub struct BigState { /* fields */ }

// Zero-copy: maps directly onto the account data buffer (near-zero CU)
#[account(zero_copy)]
#[repr(C)]  // Required: C memory layout, no padding surprises
pub struct BigState {
    pub entries: [Entry; 256],  // Fixed-size arrays, no Vec
}

// In account constraint:
#[account(mut)]
pub state: AccountLoader<'info, BigState>, // Not Account, but AccountLoader

3. Pack Multiple Values Into Fewer Bytes

Rust
// Instead of separate bools (1 byte each):
pub is_initialized: bool,  // 1 byte
pub is_locked: bool,       // 1 byte
pub is_frozen: bool,       // 1 byte = 3 bytes total

// Pack into a single u8 bitflag (1 byte total, saves rent):
pub flags: u8,  // bit 0 = initialized, bit 1 = locked, bit 2 = frozen

// Helpers:
const INITIALIZED: u8 = 1 << 0;
const LOCKED: u8 = 1 << 1;
fn is_initialized(&self) -> bool { self.flags & INITIALIZED != 0 }

4. Reduce Transaction Size

Transaction size is 1,232 bytes — every byte counts. Use Address Lookup Tables for transactions referencing many accounts. Use short instruction data (prefer enums over strings). Use PDAs instead of passing extra accounts. Combine multiple operations into single instructions where safe.

5. Minimize CPI Calls

Rust
// BAD: Two separate CPIs (~9,000 CU)
token::transfer(ctx1, amount_a)?;  // ~4,500 CU
token::transfer(ctx2, amount_b)?;  // ~4,500 CU

// BETTER: Single CPI if your architecture allows it
// Or use direct lamport manipulation for SOL transfers:
**source.try_borrow_mut_lamports()? -= amount;
**dest.try_borrow_mut_lamports()? += amount;
// This costs ~200 CU vs ~4,500 for CPI
Chapter 10

Building Real Programs

Here is a complete, production-style vault program demonstrating every concept from this guide: accounts, PDAs, CPIs, security validation, and optimization.

Environment Setup

Shell
# Install Rust, Solana CLI, and Anchor
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
cargo install --git https://github.com/coral-xyz/anchor avm --force
avm install latest && avm use latest

# Configure for devnet
solana config set --url devnet
solana-keygen new
solana airdrop 5

# Create project
anchor init sol-vault && cd sol-vault

Complete Vault Program

Rust — programs/sol-vault/src/lib.rs
use anchor_lang::prelude::*;
use anchor_lang::system_program;

declare_id!("YOUR_PROGRAM_ID");

#[program]
pub mod sol_vault {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let vault = &mut ctx.accounts.vault;
        vault.owner = ctx.accounts.owner.key();
        vault.balance = 0;
        vault.created_at = Clock::get()?.unix_timestamp;
        vault.bump = ctx.bumps.vault;
        Ok(())
    }

    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
        require!(amount > 0, VaultError::ZeroAmount);

        // CPI to System Program to transfer SOL
        system_program::transfer(
            CpiContext::new(
                ctx.accounts.system_program.to_account_info(),
                system_program::Transfer {
                    from: ctx.accounts.owner.to_account_info(),
                    to: ctx.accounts.vault.to_account_info(),
                },
            ),
            amount,
        )?;

        let vault = &mut ctx.accounts.vault;
        vault.balance = vault.balance
            .checked_add(amount)
            .ok_or(VaultError::Overflow)?;
        Ok(())
    }

    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        require!(amount > 0, VaultError::ZeroAmount);
        let vault = &mut ctx.accounts.vault;
        require!(vault.balance >= amount, VaultError::InsufficientBalance);

        vault.balance = vault.balance
            .checked_sub(amount)
            .ok_or(VaultError::Overflow)?;

        // Direct lamport transfer from PDA — cheaper than CPI
        **vault.to_account_info().try_borrow_mut_lamports()? -= amount;
        **ctx.accounts.owner.to_account_info().try_borrow_mut_lamports()? += amount;

        Ok(())
    }

    pub fn close_vault(ctx: Context<CloseVault>) -> Result<()> {
        // Anchor's close constraint handles:
        // 1. Zeroing the account data
        // 2. Transferring all lamports to owner
        // 3. Setting discriminator to closed state
        Ok(())
    }
}

// ═══════════ ACCOUNT VALIDATION STRUCTS ═══════════

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = owner,
        space = 8 + Vault::INIT_SPACE,
        seeds = [b"vault", owner.key().as_ref()],
        bump,
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut)]
    pub owner: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(
        mut,
        seeds = [b"vault", owner.key().as_ref()],
        bump = vault.bump,
        has_one = owner @ VaultError::Unauthorized,
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut)]
    pub owner: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(
        mut,
        seeds = [b"vault", owner.key().as_ref()],
        bump = vault.bump,
        has_one = owner @ VaultError::Unauthorized,
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut)]
    pub owner: Signer<'info>,
}

#[derive(Accounts)]
pub struct CloseVault<'info> {
    #[account(
        mut,
        seeds = [b"vault", owner.key().as_ref()],
        bump = vault.bump,
        has_one = owner @ VaultError::Unauthorized,
        close = owner, // ← zeros data + sends lamports to owner
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut)]
    pub owner: Signer<'info>,
}

// ═══════════ STATE ═══════════

#[account]
#[derive(InitSpace)]
pub struct Vault {
    pub owner: Pubkey,        // 32 bytes
    pub balance: u64,        //  8 bytes
    pub created_at: i64,     //  8 bytes
    pub bump: u8,            //  1 byte  = 49 + 8 discriminator = 57 bytes
}

// ═══════════ ERRORS ═══════════

#[error_code]
pub enum VaultError {
    #[msg("Insufficient vault balance")]
    InsufficientBalance,
    #[msg("Arithmetic overflow")]
    Overflow,
    #[msg("Unauthorized: you do not own this vault")]
    Unauthorized,
    #[msg("Amount must be greater than zero")]
    ZeroAmount,
}

TypeScript Client

TypeScript
import * as anchor from "@coral-xyz/anchor";
import { PublicKey, SystemProgram } from "@solana/web3.js";

const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.SolVault;
const owner = provider.wallet.publicKey;

// Derive PDA client-side — deterministic, no RPC call needed
const [vaultPda, bump] = PublicKey.findProgramAddressSync(
    [Buffer.from("vault"), owner.toBuffer()],
    program.programId
);

// Initialize
await program.methods.initialize()
    .accounts({ vault: vaultPda, owner, systemProgram: SystemProgram.programId })
    .rpc();

// Deposit 1 SOL
await program.methods.deposit(new anchor.BN(1_000_000_000))
    .accounts({ vault: vaultPda, owner, systemProgram: SystemProgram.programId })
    .rpc();

// Withdraw 0.5 SOL
await program.methods.withdraw(new anchor.BN(500_000_000))
    .accounts({ vault: vaultPda, owner })
    .rpc();

// Read state
const vault = await program.account.vault.fetch(vaultPda);
console.log(`Balance: ${vault.balance.toNumber() / 1e9} SOL`);

Essential Resources

ResourcePurpose
solana.com/docsOfficial documentation — accounts, runtime, RPC API
book.anchor-lang.comAnchor framework reference and tutorials
solanacookbook.comPractical recipes for common patterns
beta.solpg.ioBrowser-based Solana IDE — deploy without local setup
docs.helius.devRPC, webhooks, DAS API for NFTs and tokens
soldev.appCurated tutorials, courses, and project ideas
github.com/coral-xyz/anchorAnchor source — read the examples folder
sec3.devSecurity auditing tools and vulnerability database
Final Thought
Build, Deploy, Break, Repeat

Everything in Solana reduces to: design your accounts, write instructions that validate and mutate them, secure every assumption, and optimize for the compute budget. The account model is the foundation. PDAs give programs autonomy. CPIs give them composability. Security is non-negotiable. Now go deploy something to devnet.