Module 2: Solana Architecture Deep Dive¶
This module explores Solana's unique architecture that enables high throughput and low latency. Understanding these fundamentals is essential for building efficient programs.
Learning Objectives¶
By the end of this module, you will understand:
- How Proof of History (PoH) enables Solana's speed
- The accounts model and how data is stored
- Programs vs smart contracts
- Program Derived Addresses (PDAs)
- Transaction structure and compute units
- Network clusters and their purposes
1. Proof of History (PoH)¶
The Problem: Distributed Time¶
In distributed systems, agreeing on time is hard. Traditional blockchains waste resources agreeing on ordering:
Node A: "TX1 happened at 10:00:01"
Node B: "TX1 happened at 10:00:02"
Node C: "TX1 happened at 10:00:01"
└── Who is correct? Need consensus!
Solana's Solution: Cryptographic Clock¶
PoH creates a verifiable sequence of hashes that proves time has passed:
┌─────────────────────────────────────────┐
│ Proof of History Chain │
└─────────────────────────────────────────┘
Hash 0 ──SHA256──► Hash 1 ──SHA256──► Hash 2 ──SHA256──► Hash 3
│ │ │ │
│ ▼ │ │
│ Event A │ │
│ (mixed into hash) ▼ │
│ Event B │
│ (mixed into hash) │
▼ ▼
Genesis Current State
How PoH Works¶
// Simplified PoH concept
fn proof_of_history(initial_hash: [u8; 32], events: Vec<Event>) -> Vec<HashEntry> {
let mut entries = vec![];
let mut current_hash = initial_hash;
let mut counter = 0;
loop {
// Continuously hash
current_hash = sha256(¤t_hash);
counter += 1;
// When an event arrives, mix it in
if let Some(event) = events.pop() {
current_hash = sha256(&[current_hash, event.hash()].concat());
entries.push(HashEntry {
hash: current_hash,
counter,
event: Some(event),
});
}
}
}
Key Insight¶
Each hash proves it came after the previous one. To verify:
- Take any hash in the sequence
- Run SHA256 the specified number of times
- Confirm you get the next hash
This creates a trustless timestamp without communication overhead.
PoH vs Other Approaches¶
| Approach | How it Orders | Overhead |
|---|---|---|
| Bitcoin (PoW) | Longest chain wins | High energy, slow |
| Ethereum (PoS) | Validator voting | Communication rounds |
| Solana (PoH) | Cryptographic sequence | Single hash verification |
2. Tower BFT Consensus¶
PoH is the clock; Tower BFT is the consensus mechanism.
How Tower BFT Works¶
Validators vote on the PoH sequence. Each vote has increasing lockout:
Vote 1: "Hash at slot 100 is valid" → Locked for 2 slots
Vote 2: "Hash at slot 102 is valid" → Locked for 4 slots
Vote 3: "Hash at slot 106 is valid" → Locked for 8 slots
Vote 4: "Hash at slot 114 is valid" → Locked for 16 slots
...
Vote 32: Locked for 2^32 slots (~100 years)
Exponential Lockout¶
After 32 consecutive votes on a fork, a validator is effectively permanently committed. This provides finality without explicit finality messages.
Optimistic Confirmation¶
Solana achieves fast confirmation through optimistic processing:
| Confirmation Level | Time | Meaning |
|---|---|---|
| Processed | ~400ms | Transaction seen by leader |
| Confirmed | ~400ms | Supermajority voted (66%+) |
| Finalized | ~12s | 31+ votes on block (max rollback ~400ms) |
3. The Accounts Model¶
Solana uses an accounts model, not UTXO (Bitcoin) or global state (Ethereum).
Account Structure¶
Every piece of data on Solana lives in an account:
┌─────────────────────────────────────────────────────────────┐
│ Account │
├─────────────────────────────────────────────────────────────┤
│ lamports: u64 │ Balance in lamports (1 SOL = 10^9) │
├────────────────────────┼────────────────────────────────────┤
│ data: Vec<u8> │ Arbitrary data (up to 10MB) │
├────────────────────────┼────────────────────────────────────┤
│ owner: Pubkey │ Program that controls this account │
├────────────────────────┼────────────────────────────────────┤
│ executable: bool │ Is this account a program? │
├────────────────────────┼────────────────────────────────────┤
│ rent_epoch: u64 │ Next epoch rent is due │
└────────────────────────┴────────────────────────────────────┘
Account Types¶
// System Account (wallet)
Account {
lamports: 1_000_000_000, // 1 SOL
data: [], // Empty
owner: SystemProgram, // System Program owns it
executable: false,
rent_epoch: 0,
}
// Program Account
Account {
lamports: 5_000_000,
data: [BPF bytecode...], // Compiled program
owner: BPFLoader, // BPF Loader owns programs
executable: true, // Can be invoked
rent_epoch: 0,
}
// Data Account (PDA)
Account {
lamports: 2_000_000,
data: [user state...], // Serialized data
owner: YourProgram, // Your program owns it
executable: false,
rent_epoch: 0,
}
Owner Rules¶
Critical security concept:
- Only the owner can modify
data - Only the owner can debit
lamports - Anyone can credit
lamports - Only the System Program can assign new owners
┌──────────────────┐ ┌──────────────────┐
│ Your Program │ ──owns──► │ Data Account │
│ (executable) │ │ (your state) │
└──────────────────┘ └──────────────────┘
│ │
│ can modify │ cannot modify
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Account data │ │ Other data │
└──────────────┘ └──────────────┘
Accounts vs EVM Comparison¶
| Aspect | Solana | Ethereum |
|---|---|---|
| State Location | Separate accounts | Contract storage |
| Code Location | Separate executable account | Same as state |
| Parallelism | Accounts specified upfront | Sequential |
| Size Limit | 10MB per account | 24KB code, unlimited storage |
| Cost Model | Rent + tx fees | Gas |
4. Programs¶
Programs vs Smart Contracts¶
| Solana "Program" | Ethereum "Smart Contract" |
|---|---|
| Stateless (code only) | Stateful (code + storage) |
| State in separate accounts | State in contract storage |
| Compiled to BPF bytecode | Compiled to EVM bytecode |
| Can be upgradeable | Usually immutable |
Program Execution Model¶
┌─────────────────────────────────────────────────────────────────┐
│ Transaction │
├─────────────────────────────────────────────────────────────────┤
│ Instruction 1: Call Program A with accounts [1, 2, 3] │
│ Instruction 2: Call Program B with accounts [3, 4] │
│ Instruction 3: Call Program A with accounts [1, 5] │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Solana Runtime │
├─────────────────────────────────────────────────────────────────┤
│ 1. Load Program A │
│ 2. Pass accounts [1, 2, 3] and instruction data │
│ 3. Execute Program A │
│ 4. Load Program B │
│ 5. Pass accounts [3, 4] and instruction data │
│ 6. Execute Program B │
│ ... and so on │
└─────────────────────────────────────────────────────────────────┘
Native vs BPF Programs¶
Native Programs (built into runtime):
├── System Program - Create accounts, transfer SOL
├── Token Program - SPL token operations
├── Associated Token - Create associated token accounts
├── BPF Loader - Deploy and upgrade programs
└── Stake/Vote Programs - Staking and voting
BPF Programs (user-deployed):
├── Your custom programs
├── DeFi protocols (Raydium, Orca, etc.)
├── NFT programs (Metaplex)
└── Any Anchor program
Upgradeable Programs¶
Solana programs can be upgraded (unlike most Ethereum contracts):
┌────────────────────┐ ┌────────────────────┐
│ Program Account │────►│ Program Data │
│ (proxy) │ │ (actual code) │
│ │ │ │
│ executable: true │ │ executable: false │
│ owner: BPFLoader │ │ owner: BPFLoader │
└────────────────────┘ └────────────────────┘
│
│ Upgrade Authority
▼
┌────────────────────┐
│ Authority Keypair │
│ (can update code) │
└────────────────────┘
5. Program Derived Addresses (PDAs)¶
PDAs are the foundation of Solana program architecture.
What is a PDA?¶
A PDA is an address that:
- Has no private key (cannot sign transactions)
- Is derived deterministically from seeds
- Can only be "signed" by the program that derived it
PDA Derivation¶
// PDA = hash(seeds, program_id, "ProgramDerivedAddress")
// If result is on the Ed25519 curve, add bump and retry
use solana_program::pubkey::Pubkey;
let seeds = &[
b"user-stats", // Static seed
user_pubkey.as_ref(), // Dynamic seed (user's wallet)
];
let (pda, bump) = Pubkey::find_program_address(seeds, &program_id);
// pda: The derived address
// bump: The "bump seed" that makes it off-curve (0-255)
Why PDAs?¶
Problem: How does a program "own" accounts?
Without PDAs:
┌────────────┐
│ Program │ ──cannot sign──► Account creation fails!
└────────────┘
With PDAs:
┌────────────┐ ┌─────────────────┐
│ Program │ ──derives PDA──► │ PDA Account │
└────────────┘ │ owner: Program │
│ └─────────────────┘
│ invoke_signed()
▼
"Signs" with seeds + bump
Common PDA Patterns¶
// 1. Global state (one per program)
let (config_pda, _) = Pubkey::find_program_address(
&[b"config"],
&program_id
);
// 2. User-specific state
let (user_pda, _) = Pubkey::find_program_address(
&[b"user", user.key().as_ref()],
&program_id
);
// 3. Relationship between entities
let (escrow_pda, _) = Pubkey::find_program_address(
&[b"escrow", seller.key().as_ref(), buyer.key().as_ref()],
&program_id
);
// 4. Vault for holding tokens
let (vault_pda, _) = Pubkey::find_program_address(
&[b"vault", mint.key().as_ref()],
&program_id
);
PDA vs Keypair Accounts¶
| Aspect | PDA | Keypair Account |
|---|---|---|
| Private key | None | Exists |
| Creation | Program derives | User generates |
| Signing | Program with invoke_signed | Wallet signs |
| Deterministic | Yes (same seeds = same address) | No |
| Use case | Program-owned state | User wallets |
6. Rent and Account Lifecycle¶
Why Rent?¶
Storing data on-chain has costs. Rent ensures:
- Accounts pay for storage
- Unused accounts eventually get cleaned up
- Network remains sustainable
Rent Calculation¶
// Rent is based on data size
// As of 2024: ~0.00089088 SOL per byte per year
let data_size: usize = 165; // bytes
let rent = Rent::get()?;
// Minimum balance for rent exemption (2 years worth)
let min_balance = rent.minimum_balance(data_size);
// ≈ 0.00147 SOL for 165 bytes
Rent Exemption¶
Most accounts are rent-exempt (prepay 2 years of rent):
┌─────────────────────────────────────────────────────────────┐
│ Rent-Exempt Account │
├─────────────────────────────────────────────────────────────┤
│ lamports >= minimum_balance(data.len()) │
│ │
│ Benefits: │
│ - Never charged rent │
│ - Never deleted │
│ - Most common approach │
└─────────────────────────────────────────────────────────────┘
Account Lifecycle¶
1. Creation
└── Fund with rent-exempt amount + any additional SOL
2. Active Use
└── Read/write by owner program
└── Remains rent-exempt as long as balance maintained
3. Closing (optional)
└── Zero out data
└── Transfer lamports to recipient
└── Account marked for deletion
Closing Accounts (Recovering Rent)¶
// In Anchor, closing is simple:
#[derive(Accounts)]
pub struct CloseAccount<'info> {
#[account(
mut,
close = recipient // Transfers lamports to recipient
)]
pub account_to_close: Account<'info, MyData>,
#[account(mut)]
pub recipient: SystemAccount<'info>,
}
7. Transaction Anatomy¶
Transaction Structure¶
┌─────────────────────────────────────────────────────────────────┐
│ Transaction │
├─────────────────────────────────────────────────────────────────┤
│ Signatures: [Sig1, Sig2, ...] │
│ └── Array of Ed25519 signatures │
├─────────────────────────────────────────────────────────────────┤
│ Message: │
│ ├── Header │
│ │ ├── num_required_signatures: u8 │
│ │ ├── num_readonly_signed_accounts: u8 │
│ │ └── num_readonly_unsigned_accounts: u8 │
│ ├── Account Keys: [Pubkey1, Pubkey2, ...] │
│ │ └── All accounts referenced in transaction │
│ ├── Recent Blockhash │
│ │ └── Prevents replay, expires in ~2 minutes │
│ └── Instructions: [ │
│ { │
│ program_id_index: u8, // Index into account keys │
│ accounts: [u8, ...], // Indices into account keys│
│ data: Vec<u8> // Instruction data │
│ }, │
│ ... │
│ ] │
└─────────────────────────────────────────────────────────────────┘
Instruction Structure¶
pub struct Instruction {
/// Program to execute
pub program_id: Pubkey,
/// Accounts required by the instruction
pub accounts: Vec<AccountMeta>,
/// Instruction data (serialized arguments)
pub data: Vec<u8>,
}
pub struct AccountMeta {
pub pubkey: Pubkey,
pub is_signer: bool, // Must sign transaction?
pub is_writable: bool, // Will be modified?
}
Transaction Limits¶
| Limit | Value |
|---|---|
| Max size | 1232 bytes |
| Max accounts | 64 (with lookup tables: 256) |
| Max instructions | Limited by size |
| Blockhash validity | ~150 blocks (~1-2 minutes) |
8. Compute Units and Fees¶
Compute Units (CU)¶
Every operation costs compute units:
// Operation costs (approximate)
SHA256 hash: ~100 CU
Ed25519 verify: ~2000 CU
Account load: ~100 CU per account
Cross-program invoke: ~1000 CU base
Memory allocation: ~1 CU per byte
Compute Budget¶
// Default: 200,000 CU per instruction
// Maximum: 1,400,000 CU per transaction
// Request more CU if needed:
ComputeBudgetInstruction::set_compute_unit_limit(400_000)
// Set priority fee (in micro-lamports per CU):
ComputeBudgetInstruction::set_compute_unit_price(1000)
Fee Calculation¶
Transaction Fee = Base Fee + Priority Fee
Base Fee = 5000 lamports (0.000005 SOL)
Priority Fee = Compute Units × Price per CU
Example:
- 200,000 CU used
- 1000 micro-lamports per CU priority
- Total: 5000 + (200,000 × 1000 / 1,000,000) = 5000 + 200 = 5200 lamports
Priority Fees and Congestion¶
Low Congestion: Base fee only (~0.000005 SOL)
Medium: + 1,000 micro-lamports/CU
High: + 10,000 micro-lamports/CU
Very High: + 100,000+ micro-lamports/CU
Typical transaction: 0.000005 - 0.001 SOL
9. Clusters¶
Available Clusters¶
| Cluster | Purpose | RPC Endpoint |
|---|---|---|
| Mainnet-beta | Production | https://api.mainnet-beta.solana.com |
| Devnet | Development | https://api.devnet.solana.com |
| Testnet | Validator testing | https://api.testnet.solana.com |
| Localnet | Local development | http://localhost:8899 |
Cluster Selection¶
# CLI configuration
solana config set --url devnet
solana config set --url mainnet-beta
solana config set --url localhost
# Check current config
solana config get
Devnet vs Mainnet¶
| Aspect | Devnet | Mainnet |
|---|---|---|
| SOL value | Free (airdrop) | Real money |
| Reset frequency | Occasional | Never |
| Program deployment | Free to test | Costs real SOL |
| Use case | Development | Production |
10. Interactive: Explore Solana Explorer¶
Exercise: Explore Account Structure
-
Open Solana Explorer
-
Search for a token mint (e.g., paste any SPL token address)
-
Observe the account structure:
- Lamports: Balance in lamports
- Owner: Token Program
- Data: Serialized mint data
-
Executable: false (data account)
-
Click on a transaction to see:
- Signatures
- Instructions
- Account changes
- Compute units used
Exercise: View a Program
-
Search for the Token Program:
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA -
Notice:
- Executable: true
- Owner: BPF Loader
- Data: Contains BPF bytecode
Exercise: Find a PDA
-
Use the CLI to derive a PDA:
-
Search for that PDA on Explorer
- Verify the owner is the Metaplex Token Metadata program
Summary¶
| Concept | Key Takeaway |
|---|---|
| Proof of History | Cryptographic clock for ordering without consensus overhead |
| Tower BFT | Exponential lockout voting for fast finality |
| Accounts | All data lives in accounts with owner, lamports, data |
| Programs | Stateless code that operates on accounts |
| PDAs | Deterministic addresses for program-owned state |
| Rent | Storage costs, usually prepaid for exemption |
| Transactions | Signed messages with instructions and account lists |
| Compute Units | Measure of computational work, affects fees |
Self-Assessment¶
Why can't PDAs sign transactions like regular keypairs?
PDAs are specifically derived to be off the Ed25519 curve. This means there's no valid private key that could produce that public key. Only the program that derived the PDA can "sign" for it using invoke_signed() with the original seeds.
What happens if an account doesn't have enough lamports for rent?
If an account falls below rent-exempt threshold and isn't rent-exempt, it will be charged rent each epoch. If the balance reaches zero, the account is purged from the ledger. Best practice: always fund accounts to be rent-exempt.
Why must transactions specify all accounts upfront?
Solana achieves parallelism by knowing which accounts each transaction will access before execution. Transactions touching different accounts can run simultaneously. This is why you must declare all accounts in the instruction.
Next Steps¶
Now that you understand Solana's architecture, let's learn Rust patterns for building programs: