Introduction to Solana
Before building on Solana, you need to understand several fundamental concepts that make Solana unique. This guide covers accounts, transactions, programs, and their interactions.
Accounts on Solana
Solana's architecture centers around accounts: data containers that store information on the blockchain. Think of accounts as individual files in a filesystem, where each file has specific properties and an owner who controls it.
Every Solana account has the same basic structure:
pub struct Account {
/// lamports in the account
pub lamports: u64,
/// data held in this account
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub data: Vec<u8>,
/// the program that owns this account. If executable, the program that loads this account.
pub owner: Pubkey,
/// this account's data contains a loaded program (and is now read-only)
pub executable: bool,
/// the epoch at which this account will next owe rent
pub rent_epoch: Epoch,
}
Every account has a unique 32-byte address, displayed as a base58-encoded string (e.g., 14grJpemFaf88c8tiVb77W7TYg2W3ir6pfkKz3YjhhZ5
). This address serves as the account's identifier on the blockchain and is how you locate specific data.
Accounts can store up to 10 MiB of data, which can contain either executable program code or program-specific data.
All accounts require a lamport deposit proportional to their data size to become "rent-exempt." The term "rent" is historical, as lamports were originally deducted from accounts each epoch, but this feature is now disabled. Today, the deposit works more like a refundable deposit. As long as your account maintains the minimum balance for its data size, it remains rent-exempt and persists indefinitely. When you no longer need an account, you can close it and recover this deposit entirely.
Every account is owned by a program, and only that owning program can modify the account's data or withdraw its lamports. However, anyone can increase an account's lamport balance, which is useful for funding operations or paying rent without relying on calling the program itself.
Signing authority works differently depending on ownership. Accounts owned by the System Program can sign transactions to modify their own data, transfer ownership, or reclaim stored lamports. Once ownership transfers to another program, that program gains complete control over the account regardless of whether you still possess the private key. This transfer of control is permanent and irreversible.
Account Types
The most common account type is the System Account, which stores lamports (the smallest unit of SOL) and is owned by the System Program. These function as basic wallet accounts that users interact with directly for sending and receiving SOL.
Token Accounts serve a specialized purpose, storing SPL token information, including ownership and token metadata. The Token Program owns these accounts and manages all token-related operations across the Solana ecosystem. Token Accounts are Data Accounts.
Data Accounts store application-specific information and are owned by custom programs. These accounts hold your application's state and can be structured however your program requires, from simple user profiles to complex financial data.
Finally, Program Accounts contain the executable code that runs on Solana, which is where the smart contract lives. These accounts are marked as executable: true
and store the program's logic that processes instructions and manages state.
Working with Account Data
Here's how programs interact with account data:
#[derive(BorshSerialize, BorshDeserialize)]
pub struct UserAccount {
pub name: String,
pub balance: u64,
pub posts: Vec<u32>,
}
pub fn update_user_data(accounts: &[AccountInfo], new_name: String) -> ProgramResult {
let user_account = &accounts[0];
// Deserialize existing data
let mut user_data = UserAccount::try_from_slice(&user_account.data.borrow())?;
// Modify the data
user_data.name = new_name;
// Serialize back to account
user_data.serialize(&mut &mut user_account.data.borrow_mut()[..])?;
Ok(())
}
Unlike databases where you simply insert records, Solana accounts must be explicitly created and funded before use.
Transactions on Solana
Solana transactions are atomic operations that can contain multiple instructions. All instructions within a transaction either succeed together or fail together: there's no partial execution.
A transaction consists of:
- Instructions: Individual operations to perform
- Accounts: Specific accounts each instruction will read from or write to
- Signers: Accounts that authorize the transaction
Transaction {
instructions: [
// Instruction 1: Transfer SOL
system_program::transfer(from_wallet, to_wallet, amount),
// Instruction 2: Update user profile
my_program::update_profile(user_account, new_name),
// Instruction 3: Log activity
my_program::log_activity(activity_account, "transfer", amount),
],
accounts: [from_wallet, to_wallet, user_account, activity_account]
signers: [user_keypair],
}
Transaction Requirements and Fees
Transactions are limited to 1,232 bytes total, which constrains how many instructions and accounts you can include.
Each instruction within a transaction requires three essential components: the program address to invoke, all accounts the instruction will read from or write to, and any additional data such as function arguments.
Instructions execute sequentially in the order you specify within the transaction.
Every transaction incurs a base fee of 5,000 lamports per signature to compensate validators for processing your transaction.
You can also pay an optional prioritization fee to increase the likelihood that the current leader processes your transaction quickly. This prioritization fee is calculated as your compute unit limit multiplied by your compute unit price (measured in micro-lamports).
prioritization_fee = compute_unit_limit × compute_unit_price
Programs on Solana
Programs on Solana are fundamentally stateless, meaning they don't maintain any internal state between function calls. Instead, they receive accounts as input, process the data within those accounts, and return the modified results.
This stateless design ensures predictable behavior and enables powerful composability patterns.
The programs themselves are stored in special accounts marked as executable: true
, containing the compiled binary code that executes when invoked.
Users interact with these programs by sending transactions that contain specific instructions, each targeting particular program functions with the necessary account data and parameters.
use solana_program::prelude::*;
#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
pub name: String,
pub created_at: i64,
}
pub fn create_user(
accounts: &[AccountInfo],
name: String,
) -> ProgramResult {
let user_account = &accounts[0];
let user = User {
name,
created_at: Clock::get()?.unix_timestamp,
};
user.serialize(&mut &mut user_account.data.borrow_mut()[..])?;
Ok(())
}
Programs can be updated by their designated upgrade authority, allowing developers to fix bugs and add features after deployment. However, removing this upgrade authority makes the program permanently immutable, providing users with guarantees that the code will never change.
For transparency and security, users can verify that on-chain programs match their public source code through verifiable builds, ensuring the deployed bytecode corresponds exactly to the published source.
Program Derived Addresses (PDAs)
PDAs are deterministically generated addresses that enable powerful programmability patterns. They're created using seeds and a program ID, producing addresses without corresponding private keys.
PDAs use SHA-256
hashing with specific inputs, including your custom seeds, a bump value to ensure the result is off-curve, the program ID that will own the PDA, and a constant marker.
When the hash produces an on-curve address (which happens approximately 50% of the time), the system iterates from bump 255 down to 254, 253, and so on until finding an off-curve result.
use solana_nostd_sha256::hashv;
const PDA_MARKER: &[u8; 21] = b"ProgramDerivedAddress";
let pda = hashv(&[
seed_data.as_ref(), // Your custom seeds
&[bump], // Bump to ensure off-curve
program_id.as_ref(), // Program that owns this PDA
PDA_MARKER,
]);
Benefits
The deterministic nature of PDAs eliminates the need to store addresses: you can regenerate them from the same seeds whenever needed.
This creates predictable addressing schemes that function like hashmap structures on-chain. More importantly, programs can sign for their own PDAs, enabling autonomous asset management without exposing private keys:
let seeds = &[b"vault", user.as_ref(), &[bump]];
invoke_signed(
&transfer_instruction,
&[from_pda, to_account, token_program],
&[&seeds[..]], // Program proves PDA control
)?;
Cross Program Invocation (CPI)
CPIs enable programs to call other programs within the same transaction, creating true composability where multiple programs can interact atomically without external coordination.
This allows developers to build complex applications by combining existing programs rather than rebuilding functionality from scratch.
CPIs follow the same pattern as regular instructions, requiring you to specify the target program, the accounts it needs, and the instruction data, with the main difference being that they can be performed inside other programs.
The calling program maintains control over the flow while delegating specific operations to specialized programs:
let cpi_accounts = TransferAccounts {
from: source_account.clone(),
to: destination_account.clone(),
authority: authority_account.clone(),
};
let cpi_ctx = CpiContext::new(token_program.clone(), cpi_accounts);
token_program::cpi::transfer(cpi_ctx, amount)?;
Constraints and Capabilities
Original transaction signers maintain their authority throughout CPI chains, allowing programs to act on behalf of users seamlessly.
However, programs can only make CPIs up to 4 levels deep (A → B → C → D
) to prevent infinite recursion. Programs can also sign for their PDAs in CPIs using CpiContext::new_with_signer
, enabling sophisticated autonomous operations.
This composability enables complex operations across multiple programs within a single atomic transaction, making Solana applications highly modular and interoperable.