Solana SDK Deep Dive: PDAs, Transactions & Program Architecture
Go beyond Hello World tutorials. Learn how PDAs are mathematically derived, why transactions need blockhashes, and the account validation patterns that prevent exploits.
I spent three weeks reverse-engineering how Anchor abstracts away Solana's complexity. Then I actually read the solana-sdk source code. Turns out, the "magic" is just well-designed primitives that most tutorials skip over.
This isn't another "Hello World" Solana tutorial. We're going deep into the SDK internals — PDAs, transaction anatomy, and the account model. By the end, you'll understand why things work, not just how.
What You'll Actually Learn
Most Solana tutorials throw code at you. Here's what we're covering differently:
- How PDAs are mathematically derived (and why bump seeds exist)
- The exact structure of a transaction — headers, signatures, instructions
- Why programs don't have
main()and what the entrypoint macro actually does - Account management patterns that won't get your program exploited
Let's start with the concept that confuses most developers.
Program-Derived Addresses (PDAs)
PDAs are addresses that don't have private keys. Programs can "sign" for them without needing a secret. This is the foundation of on-chain authority.
Seeds Go In
You provide arbitrary byte arrays — could be a string like b"vault", a user's pubkey, or any data. Max 16 seeds, each up to 32 bytes.
Hash Gets Computed
The SDK hashes your seeds + program ID + a "bump seed" (0-255) using SHA256.
Curve Check Happens
If the result lands ON the Ed25519 curve, it would have a private key. We don't want that. The bump seed shifts it OFF the curve.
PDA Emerges
The first bump (starting from 255, going down) that produces an off-curve address wins. That's your PDA.
The SDK tries bump=255 first because statistically, most seeds produce valid PDAs on the first try. Going down is just a convention — it could go up. What matters is determinism: same seeds + same program = same PDA, always.
The Code That Makes It Click
Client-side derivation looks like this:
let seeds = [b"vault", user_pubkey.as_ref()];
let (pda, bump_seed) = Pubkey::find_program_address(&seeds, &program_id);
On-chain verification — this is the part most tutorials skip:
// The program MUST verify the PDA matches
let expected_pda = Pubkey::create_program_address(
&[b"vault", user_pubkey.as_ref(), &[bump_seed]],
&program_id
)?;
assert_eq!(pda, expected_pda);
Hidden gem: Always pass the bump seed as part of your instruction data. Recalculating it on-chain with find_program_address burns ~10,000 compute units. Passing it directly and verifying with create_program_address costs ~1,500.
Why do PDAs need to be OFF the Ed25519 curve?
Anatomy of a Transaction
Transactions in Solana aren't just "send data to program." They're carefully structured packets with specific fields.
A transaction has two main parts:
- Signatures — Vector of Ed25519 signatures from all required signers
- Message — The actual payload containing:
- Header (counts of signers, readonly accounts)
- Account addresses (flat array, ordered specifically)
- Recent blockhash (prevents replay attacks)
- Instructions (the actual program calls)
Building a Transaction Step-by-Step
Here's the flow that actually happens:
// 1. Define your instruction data
let bank_instruction = BankInstruction::Initialize;
// 2. Create the instruction with program ID and accounts
let instruction = Instruction::new_with_borsh(
program_id,
&bank_instruction,
vec![
AccountMeta::new(payer.pubkey(), true), // writable, signer
AccountMeta::new(vault_pda, false), // writable, not signer
],
);
// 3. Bundle into a message
let message = Message::new(
&[instruction],
Some(&payer.pubkey()), // fee payer
);
// 4. Create unsigned transaction
let mut tx = Transaction::new_unsigned(message);
// 5. Get fresh blockhash and sign
let blockhash = client.get_latest_blockhash()?;
tx.sign(&[&payer], blockhash);
// 6. Send it
client.send_and_confirm_transaction(&tx)?;
Pro pattern: Use Transaction::partial_sign for multi-sig scenarios. Sign with available keys first, pass the partially-signed transaction around, then complete signing.
The Entrypoint Pattern
Here's something that trips up developers coming from other languages: Solana programs don't have a main() function.
The standard pattern looks like this:
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Your logic here
Ok(())
}
program_id
The public key of your deployed program. Use this to derive PDAs.
accounts
Array of ALL accounts your instruction might touch. Order matters — it must match what your client sends.
instruction_data
Raw bytes. You decide the format. Most use Borsh serialization.
Account Management That Won't Get You Hacked
Here's where 90% of Solana exploits come from: improper account validation.
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?;
let vault = next_account_info(account_info_iter)?;
// THE CHECKS THAT SAVE YOU:
assert!(payer.is_writable); // Can we modify it?
assert!(payer.is_signer); // Did they actually sign?
assert!(vault.is_writable);
assert_eq!(vault.owner, program_id); // Do WE own this account?
// Now you're safe to proceed
Ok(())
}
Critical: Always verify owner on accounts you read data from. An attacker can create a fake account with the same data layout and pass it in. If you don't check ownership, you'll read their malicious data.
A user passes an account to your program. You read balance data from it without checking the owner. What can go wrong?
Cross-Program Invocation (CPI)
Programs calling programs. This is where invoke and invoke_signed come in.
Use when calling another program with accounts that have normal signers:
invoke(
&system_instruction::transfer(from.key, to.key, lamports),
&[from.clone(), to.clone()],
)?;
The from account must have signed the original transaction.
Next Steps
You now understand the core primitives. Here's where to go from here:
- Build a token vault — Use PDAs to hold SPL tokens, practice CPI with the Token program
- Study Anchor's codegen — Now that you know the raw SDK, see how Anchor abstracts it
- Read exploit post-mortems — Solana security Discord has breakdowns of real hacks
The Solana SDK is maintained by Anza (formerly Solana Labs). The anza-xyz/solana-sdk repo is the canonical source. Reading it directly taught me more than any tutorial.
Building something on Solana? I'd love to hear what SDK patterns you're using. Hit me up on Twitter.