The Account Blueprint
Accounts are Solana's single storage abstraction: every wallet, program binary, token account, and app record is the same on-chain object. Once you see that uniformity - five fields, one owner, explicit locks - rent, CPIs, PDAs, and Sealevel read as consequences of one design choice, not special cases.
Account Model Basics is the hands-on entry; Account Anatomy, Rent & Rent-Exemption, Account Ownership & Permissions, Program-Owned State, and System Accounts vs. Program Accounts each zoom into one mechanism. This page sits underneath: why one account type exists, and how that enables parallel execution.
Summary
- Solana stores all on-chain value and state as accounts - fixed-shape records with
lamports,data,owner,executable, andrent_epoch- and only the owner program may mutatedata. - Insight: One abstraction means one permission model, one funding rule (rent), and one lock list; that list is what lets Sealevel run non-overlapping transactions in parallel.
- Key Concepts: single account abstraction, owner-writes, system-owned vs program-owned, executable accounts, rent exemption, account creation (allocate + assign + fund), PDAs, explicit account lists.
- When to Use: Designing where state lives, debugging wrong-owner or missing-account errors, sizing rent, explaining Solana to EVM-native teammates, or reasoning about which transactions can run side by side.
- Limitations/Trade-offs: You must size and fund accounts up front, declare every account a transaction touches, and split hot state across addresses for parallelism - no hidden global storage bag inside a program.
- Related Topics: account anatomy, rent, ownership permissions, program-owned state, system vs program accounts, Sealevel scheduling.
Foundations
Solana does not give wallets one type, contracts another, and storage a third. It gives you one account type at a public key address, interpreted by how many lamports it holds, which program owns it, whether it is executable, and what bytes sit in its data buffer.
That choice is deliberate. A uniform record keeps AccountsDB simple, makes permission checks local to a single owner pubkey, and turns "what can this transaction touch?" into a finite list of addresses the client must declare. Parallel scheduling becomes a graph problem over those addresses, not a guess about what a program might read later.
Every account exposes the same five core fields:
| Field | Role |
|---|---|
lamports | SOL balance in lamports (funding + fees + rent deposit) |
data | Opaque byte buffer (empty for many wallets; structured for app state) |
owner | Program ID allowed to write data and apply owner-side rules |
executable | true if this account is loadable program code |
rent_epoch | Legacy rent-tracking field; rent-exempt accounts effectively sit at the max epoch |
Think of global state as a map from address to this record - not as separate species for "contracts with storage" and "EOAs with balances":
Address (Pubkey)
|
v
+------------------+
| lamports | <- balance / rent deposit
| data: [u8] | <- empty, Borsh layout, or program bytes
| owner: Pubkey | <- who may write data
| executable: bool | <- program vs data
| rent_epoch | <- legacy / exempt marker
+------------------+Owner-writes is the central security rule: only the program named in owner may mutate that account's data. Other programs may read an account if it is passed in, and may move lamports under specific rules, but they cannot rewrite another program's bytes. Signers prove "who requested this"; the owner field proves "which program may change this storage."
Ordinary wallets are typically system-owned: System Program as owner, executable false, and usually empty data. Useful wallet state is the lamport balance (token balances live in other accounts). Application state lives in program-owned accounts whose owner is your program ID and whose buffer holds a layout that program understands. Programs themselves are accounts with executable: true, owned by a loader, holding bytecode rather than app fields.
That split - code in executable accounts, durable state in data accounts - is why Solana programs are called stateless: the executable has no private storage map; every durable field lives at another address your instruction must name.
Mechanics & Interactions
Creating an account is three jobs in one flow
Nothing useful exists at an unused address until something allocates space, assigns an owner, and funds the rent-exempt minimum for that size. The System Program usually does the lifecycle work; your program (or another owner) then interprets the buffer.
Conceptually:
payer (system-owned) new account
+----------------+ +------------------+
| lamports: $$$ | --fund rent-----> | lamports: min |
| data: [] | | data: [0; N] |
| owner: System | --allocate N----> | owner: YourProg |
+----------------+ --assign owner--> | executable: false|
+------------------+Rent exemption is a size-scaled lamport deposit: larger data locks more SOL in the account. Production practice funds that deposit once so the account stays alive; closing under the right instructions returns those lamports to a recipient. Underfunding is not a soft warning - non-exempt accounts do not persist the way production state requires.
System-owned vs program-owned vs executable
Three everyday roles share the same struct:
- System-owned wallet / fee payer - holds SOL; System Program transfers and creates; no app layout in
data. - Program-owned data account - holds structured state; only your program writes
data; often a PDA for deterministic addressing. - Executable program account - loader-owned,
executable: true; holds or points at program bytecode (upgradeable loaders often split program identity from a ProgramData account that stores the bytes).
The mental model is: identity is the address, permission is the owner, meaning is the layout of data, and "is this code?" is the executable flag. Owner is always a program ID; an admin pubkey is usually a field inside the buffer that your program checks itself.
PDAs: program-controlled addresses without private keys
A program-derived address (PDA) is still just an account address. It is derived from seeds and a program ID so no ordinary private key can sign for it; the owning program can authorize it via seeds on CPI. PDAs answer how a program holds vaults, configs, and per-user state without an on-chain secret key. Seed and bump details belong elsewhere; here, a PDA is not a second account type - only an address and signing convention over the same record.
Explicit account lists and Sealevel
Clients must declare every account an instruction will read or write. The runtime uses that list to load accounts from AccountsDB, mark each readonly or writable, lock overlapping writable accounts so conflicts do not interleave, and schedule non-overlapping work concurrently (Sealevel).
Tx A accounts: [User1, VaultA, TokenProg] (writable: User1, VaultA)
Tx B accounts: [User2, VaultB, TokenProg] (writable: User2, VaultB)
|
v
No shared writable accounts -> may run in parallel
Tx C accounts: [User1, VaultA, ...]
Conflicts with Tx A on VaultA writable -> must serializeIf the program needs an undeclared account, execution fails; there is no ambient "load whatever key I hash later" path for mutation. That friction is intentional: parallelism needs a closed world of addresses before execution starts. Many small, non-overlapping state accounts (for example one PDA per user) are not only an API style - they are how you leave headroom for concurrent throughput.
How the pieces interact in one instruction
A typical "update my profile" path:
- Client lists the user signer, profile PDA (writable), System Program if creating, and your program.
- Runtime locks those accounts by readonly/writable flags.
- Your program runs; only accounts it owns may have
datarewritten by its logic. - It deserializes
data, checks authority against the signer, writes new bytes, and returns. - Success commits the record; failure aborts the whole transaction.
Rent was set at creation. Ownership was set at assign. Parallelism was set by which addresses landed in the message. The instruction body only supplies business logic.
Advanced Considerations & Applications
The single account abstraction forces trade-offs that feel odd if you expect "one contract, infinite internal storage." You pay in up-front sizing, explicit wiring, and account sprawl; you gain clear ownership, reclaimable rent, and scheduler-visible conflict sets.
| Account role | Strength | Weakness | Best fit |
|---|---|---|---|
| System-owned wallet | Simple SOL holder; natural fee payer | Empty/untyped data; not for app schemas | Users, SOL treasuries, instruction payers |
| Program-owned data account | Typed state under owner-writes; closable rent | Allocate, fund, and pass every time | Config, escrow, per-user records, vaults |
| Executable program account | Shared logic; upgradeable loader patterns | Not app state; upgrades need authority care | On-chain program code only |
Granularity is the main performance lever. One giant global-state account serializes every writer. Many per-entity accounts let non-overlapping users run in parallel, at the cost of more addresses, more rent, and longer account lists. Compression and indexes help at scale, but the chain still thinks in accounts and declared locks.
CPI and ownership compose because ownership is absolute. To change a token account, you CPI into the Token Program - you cannot poke its bytes yourself. "Who may mutate this?" is a runtime fact, and Sealevel's trust boundary matches the same owner field you use for app accounts.
Upgradeability is account-shaped too. With the upgradeable loader, program identity and bytes can live in related accounts (program + ProgramData). Closing, reallocating, or migrating state is always "move or rewrite under owner rules," not "edit a hidden storage trie."
For design reviews, answer first: where each field lives, which program owns it, which authority sits inside data, space and rent cost, which addresses every client must pass, and which transaction pairs contend on the same writable account. Crisp answers cut ownership bugs and accidental hot-path serialization.
Common Misconceptions
- "Wallets, programs, and state are different on-chain object types." They are the same account structure; roles differ by
owner,executable, and how you interpretdata. - "My program stores fields inside the program binary." Executables hold code (via loaders); durable app fields live in separate program-owned accounts you create and pass in.
- "Owner means the user's wallet pubkey." Owner is always a program ID. User control is usually a signer check against an authority field stored in
data, or System Program rules for system-owned accounts. - "If I know an address, the runtime will fetch whatever I need." Mutation paths require the account to appear in the transaction's account list with the right writable flag; undeclared accounts are not ambiently available.
- "Rent is a monthly subscription forever." Production practice is rent-exempt funding at creation; the deposit scales with size and is reclaimable when the account is closed correctly.
- "One big account is simpler and fine at scale." It is simpler to code at first, but every writer serializes on that address; Sealevel cannot parallelize conflicting writable locks.
FAQs
What is the single most important idea in the account model?
Everything on-chain is an account with the same fields, and only the owner program may write that account's data - so permissions, rent, and parallel locks form one consistent system.
What are the five core account fields?
lamports (balance), data (byte buffer), owner (program allowed to write data), executable (whether the account is program code), and rent_epoch (legacy rent tracking; exempt accounts use the max epoch convention).
What does the owner-writes rule actually enforce?
The runtime rejects data mutations from any program that is not the account's owner. Your program can only rewrite accounts it owns; to change someone else's layout you must CPI to their owner program.
Why are ordinary wallets system-owned?
The System Program owns them so it can create accounts, assign owners, and transfer SOL under signature rules. Wallets typically have empty data and are not used as structured app storage.
Where should my app's structured state live?
In accounts owned by your program: allocate space, assign your program ID as owner, fund rent-exempt lamports, then serialize your layout into data on each update.
How do executable program accounts differ from data accounts?
They have executable: true and are owned by a loader; their payload is bytecode or loader bookkeeping, not your app schema. You invoke them as programs, not as ordinary mutable state bags.
What does "rent-exempt" mean in practice?
The account holds enough lamports for its data size that it is not subject to ongoing rent collection. The required deposit grows with allocated space; closing the account is how you reclaim those lamports.
What steps create a usable data account?
Allocate a data buffer of the right size, assign the intended owner program, and fund at least the rent-exempt minimum - usually paid by a system-owned fee payer in the same transaction that initializes fields.
Are PDAs a different kind of account?
No. A PDA is an address derived so a program can control it without a private key. Once created, it is still a normal account record with the same five fields.
Why must clients declare all accounts in the transaction?
So the runtime can load, lock, and schedule accounts before execution. Sealevel uses those lists to run non-overlapping transactions in parallel and to prevent undeclared state access.
How does the account model enable Sealevel parallelism?
Transactions that do not share conflicting writable accounts can execute concurrently because their lock sets do not intersect. Explicit account lists make those lock sets known up front.
What is the difference between account owner and account authority?
Owner is the program ID in the account metadata. Authority is usually an application-level pubkey stored inside data that your program checks against a transaction signer.
Can two programs own the same account?
No. There is a single owner field. Shared behavior requires CPI into the owning program or redesigning which program should own the state.
What happens when I close an account?
Under the owning program's rules, data is zeroed or the account is reassigned, and remaining lamports (including the rent deposit) transfer to a recipient - reclaiming capital locked for that state.
How should I think about token balances in this model?
SPL balances live in token accounts owned by the Token Program, not in the wallet's empty system-account data. The wallet is still an account; each token balance is another account with its own owner and layout.
Related
- Account Model Basics - hands-on account model examples
- Account Anatomy - field-by-field breakdown of the account record
- Rent & Rent-Exemption - size-based deposits and reclaim on close
- Account Ownership & Permissions - owner-writes and signer checks
- Program-Owned State - stateless programs and data accounts
- System Accounts vs. Program Accounts - wallet vs data vs executable roles
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, and @solana/kit 7.0.0.