Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Every Ethereum developer walks into Solana and trips over the same thing: Solana programs do not have their own storage. A contract on Ethereum owns its state — a mapping, a struct, a uint — and that state lives inside the contract's address. On Solana, programs are stateless code; all state is held in separate accounts that the program reads and writes during a transaction. You, the caller, must tell the program which accounts it will touch, up front. This is why Solana is fast (the runtime parallelises non-overlapping transactions) and why it feels alien for the first week. Getting this mental model right is the difference between fighting the runtime and flowing with it.
Compare two equivalent counters — one in Solidity, one in Anchor. Solidity keeps count inside contract storage at a fixed slot. Anchor puts count inside a Counter struct held in a separate account owned by the program. The Anchor transaction must name the account; the Solidity transaction does not. That naming is the entire difference.
// Anchor program — programs/counter/src/lib.rs
use anchor_lang::prelude::*;
declare_id!("Ctr111111111111111111111111111111111111111");
#[program]
pub mod counter {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
ctx.accounts.counter.count += 1;
Ok(())
}
}
#[account]
pub struct Counter { pub count: u64 }
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = user, space = 8 + 8)]
pub counter: Account<'info, Counter>,
#[account(mut)] pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)] pub counter: Account<'info, Counter>,
}
// Note: the program has no state. All state lives in the 'counter' account,
// which the caller passes in explicitly.
cargo runcount physically lives. It is NOT inside the program; it is inside the account at counter.publicKey. Confirm by fetching the account bytes directly with connection.getAccountInfo(counter.publicKey) and inspecting the 8-byte discriminator + 8-byte u64.increment without passing the counter account. The transaction will fail with a missing-account error before any Rust code runs. Note: Solana returns errors at the runtime layer, not from your program's body.program.account.counter.fetch(...)). On Ethereum this is eth_call; on Solana it is simply an RPC read. No execution happens — state is the bytes of the account.Use these three in order. Each builds on the one before.
In one paragraph, explain the difference between Ethereum's contract storage and Solana's account model — where does the state live in each, and who decides which state a transaction touches?
Walk me through what happens step by step when a Solana transaction calls a program: how does the runtime know which accounts to load, who checks they belong to the right program, and why does this architecture allow parallel execution?
I'm porting a 10-contract DeFi protocol from Solidity to Anchor. Walk me through the account architecture translation: where mappings become PDAs, how cross-contract calls become CPIs, and which Solidity patterns have no clean Solana equivalent.