✳ SOLANA ACADEMYCourse overviewGlossary

Intermediate · LESSON 06 OF 12

6. Programs and explicit account state

By the end, you can model an instruction and its required accounts.

The caller supplies accounts; the program checks ownership, type, address, signer, authority, and revision before changing or rejecting state.
6. Programs and explicit account state workflow. The caller supplies accounts; the program checks ownership, type, address, signer, authority, and revision before changing or rejecting state.
An illustrative fixed-size LearnerRecord account has a 32-byte PDA address and the five account fields: lamports, data, owner, executable, and deprecated rent_epoch. Writable is a transaction access meta, not authority. Its 94 data bytes contain an 8-byte Anchor discriminator, 32-byte learner key, 16-byte course ID, 32-byte revision, and six completion flags. Before mutation, derive the expected PDA, validate ownership and type, match the learner signer to the stored authority, and check the active course revision. Any failed gate rejects the instruction.
Account layout and Anchor validation gates. An illustrative fixed-size LearnerRecord account has a 32-byte PDA address and the five account fields: lamports, data, owner, executable, and deprecated rent_epoch. Writable is a transaction access meta, not authority. Its 94 data bytes contain an 8-byte Anchor discriminator, 32-byte learner key, 16-byte course ID, 32-byte revision, and six completion flags. Before mutation, derive the expected PDA, validate ownership and type, match the learner signer to the stored authority, and check the active course revision. Any failed gate rejects the instruction.
Growth increases serialized data length and the named realloc payer funds the rent-exempt minimum difference. Shrinking an Anchor account sends every lamport above the new minimum to realloc::payer, including intentional surplus. Never apply this shrink behavior to a custody vault; keep value and resizable metadata in separate accounts. Bound user-controlled sizes and test both data and balances.
Account data resizing and lamport flow. Growth increases serialized data length and the named realloc payer funds the rent-exempt minimum difference. Shrinking an Anchor account sends every lamport above the new minimum to realloc::payer, including intentional surplus. Never apply this shrink behavior to a custody vault; keep value and resizable metadata in separate accounts. Bound user-controlled sizes and test both data and balances.

Read the workflow

The caller chooses the accounts passed to an instruction. Before using them, the program verifies expected owners, account types, and canonical addresses; it separately checks transaction signers and stored authority relationships. The course or claim revision is also checked before updating state. Invalid combinations must be rejected without changing protected state.

  1. CALLER SELECTS ACCOUNTS
  2. OWNER + TYPE + PDA CHECKS
  3. SIGNER + AUTHORITY + REVISION
  4. STATE UPDATE OR REJECT

FIELD NOTE 01

Code and state are separate

Solana program code lives in an executable account, but programs are stateless: mutable application state lives in separate accounts passed to instructions. A program does not privately store each user's mutable balance. Persistent state layouts and authority rules must be designed explicitly.

FIELD NOTE 02

Design a learner record

A fictional learner record can contain an authority public key, a course identifier, a revision, and completion flags. Before changing it, validate the account owner, data layout, expected address, and authorized signer. A client selecting the correct account is helpful, but a malicious client can submit different accounts.

FIELD NOTE 03

Start with invariants

Write invariants before implementation: only the designated issuer can attest an exam result; a learner cannot overwrite another learner’s record; a record belongs to one curriculum revision; duplicate certification does not create contradictory state. Derive negative tests directly from these statements.

FIELD NOTE 04

Programs validate state they did not create

A program receives account addresses from a caller. The runtime lets only the owner program change account data or debit its lamports; any program may credit a writable account. Marking an account writable does not grant ownership. Before interpreting data, the instruction must still verify that each supplied account is the one the application intended. A client-side label or type annotation does not enforce that relationship on chain. Bind the address to canonical seeds, check stored authorities and expected mints, and validate signer and writable privileges. Keep initialization rules separate from update rules, and define who may create, change, close, or reassign each piece of state. Then test a valid account of the wrong type, an unrelated authority, and a writable account where read-only access is sufficient.

FIELD NOTE 05

Treat an instruction interface as a contract

For each instruction, write the account list, data fields, signer requirements, writable flags, and state transition before implementing the handler. A learner-record update might require a program-owned record, the learner authority, and a trusted issuer or assessment authority. Say which account must match each relationship; “valid account” is not a precise rule. Mark fields that are immutable after initialization, calculate allocated data space for the chosen serialization, and define how a curriculum revision is stored. This interface is part of the program’s public behavior, so document how clients can discover it and what errors they should expect.

FIELD NOTE 06

Budget the serialized account data

Use a concrete fixed-size, non-zero-copy Anchor example to budget storage. A `LearnerRecord` account could store a learner `Pubkey` (32 bytes), a course ID `[u8; 16]` (16 bytes), a revision hash `[u8; 32]` (32 bytes), and six completion flags `[bool; 6]` (6 bytes). With Anchor's default 8-byte account discriminator, its serialized data needs 94 bytes: `8 + 32 + 16 + 32 + 6`. The account's 32-byte address is separate from `data`; the runtime account also has `lamports`, `owner`, `executable`, and the deprecated `rent_epoch` fields. The owner is a program ID and controls data writes. An Anchor `mut` constraint asks for writable access in the instruction; it does not grant ownership or application authority. Variable-length strings and vectors need space for their length prefixes and maximum contents. Recalculate when the schema changes, and use the different layout rules for zero-copy accounts. This `LearnerRecord` is only a teaching example, not the academy's credential-account schema.

FIELD NOTE 07

Validate every supplied account at the boundary

A caller chooses the accounts it passes to an instruction. On-chain code must check the program owner, expected PDA derivation, stored authority, mint or course identity, and writable or signer requirements before it trusts data. Do not rely on an interface type, a hidden form field, or the order selected by a friendly client. A negative test should substitute a real account of the wrong type, another learner’s record, the wrong authority, and an unexpected writable account. Check not only that the instruction returns an error, but also that every protected account remains unchanged.

FIELD NOTE 08

Plan how state survives course revisions

Account layouts and course policies evolve. A record should identify the schema version and the curriculum revision it represents, while the program defines whether a newer lesson revision updates the same record or creates a separate one. Avoid changing seed rules casually: a new address can make an existing learner appear to have no record, or permit a second claim. When layouts change, specify migration or compatibility behavior and test reading both old and current records. A browser database may hold convenient progress data, but an on-chain program must verify any claim that depends on it rather than trusting a client’s copy.

FIELD NOTE 09

Resize data without misdirecting lamports

Anchor’s `#[account(realloc = ..., realloc::payer = ..., realloc::zero = ...)]` constraint resizes a program-owned account at the beginning of instruction processing. On growth, the named payer funds the additional lamports needed to keep the larger account rent-exempt; the network transaction fee payer and the realloc payer are distinct roles unless the application deliberately makes them the same. Query the chosen cluster for the current minimum for the exact target size instead of hardcoding a SOL amount. On shrink, current Anchor v1 constraint behavior sends every lamport above the resized account’s new rent-exempt minimum to `realloc::payer`. It does not cap the refund to the rent reserve that was added for the account. If a vault also holds deposits, escrow principal, protocol fees, or another user claim, this can send that value to the refund recipient. Do not use subtractive `realloc` on a custody account; keep user value in a separately governed vault and resize only an account whose balance policy permits the transfer. Bound the new serialized size with an application limit and checked arithmetic before performing the resize; the runtime currently limits one account’s growth to 10 KiB per instruction and account data to 10 MiB. Those are upper bounds, not an application storage design. A successful test should check old and new data length, schema contents, the payer’s balance, the target account’s full lamport balance, and any separate vault balance. Include growth, shrink, maximum, over-maximum, and failed-migration cases. Anchor account-space rules vary for Borsh and zero-copy layouts, so size each representation using the matching rules and include the discriminator when required.

MAKE THE CONNECTION

A detail worth keeping

A Solana program receives candidate accounts from a caller. Distinguish the runtime owner field from an application authority field: the owner program controls data writes, while an authority public key inside that data may decide which signer can request a permitted change. A PDA, expected data type, stored authority, writable status, and associated mint are separate checks. A well-typed account is not automatically the one your product intended to update.

Work through an example

Read the example as several independent gates: `Account<'info, LearnerRecord>` checks program ownership and deserializes the expected Anchor account type; `seeds` and `bump` check the learner/course PDA; `Signer` requires the learner to sign; `has_one` compares that signer with the record’s stored learner key; `mut` allows the program to persist an update; and the revision constraint rejects stale work. None of these proves that an off-chain exam was honestly graded: a trusted issuer or verified claim must establish that separate fact. If the course changes, retain the exact revision the record assessed.

Illustrative Anchor account-validation excerpt. The handler, account type, and custom error are omitted; use the project’s pinned Anchor version when compiling.
#[derive(Accounts)]
#[instruction(course_id: [u8; 16], revision: [u8; 32])]
pub struct CompleteCourse<'info> {
    #[account(
        mut,
        seeds = [b"learner", learner.key().as_ref(), course_id.as_ref()],
        bump,
        has_one = learner,
        constraint = record.revision == revision @ AcademyError::RevisionMismatch
    )]
    pub record: Account<'info, LearnerRecord>,
    pub learner: Signer<'info>,
}

Design a learner-record update for one wallet and one course revision. State the account owner, canonical PDA seeds, stored authority field, signer, writable accounts, revision check, and state transition. Then predict which validation rejects a second learner’s record, a missing signature, and a stale revision before changing code. For each resize, record old and new data lengths, the chosen cluster’s rent-exempt minima, the transaction fee payer, `realloc::payer`, all lamports held before and after, the account’s economic purpose, and the schema version. Refuse to resize a vault when its balance includes user funds unless the design preserves those funds independently.

GUIDED PRACTICE · INTERMEDIATE

Do the work, one step at a time

Use fictional addresses and disposable test data. Do not enter a recovery phrase or private key. Unless a lesson explicitly says “read-only Devnet,” use paper or a local test and do not connect to a public network.

  1. Use the Account Resize Safety Lab with its five fictional cases; choose a resize policy before reading the explanation.
  2. For the learner record, compare the quoted minimum for the old and new serialized sizes. Identify which account funds a growth top-up and keep that payer distinct from the transaction fee payer unless the design explicitly joins them.
  3. For the custody vault, trace every lamport above the new minimum to `realloc::payer`. Explain why a rent refund assumption could redirect depositor funds, and propose a separate storage account or non-shrinking vault design.
  4. For the user-controlled vector length, set a product-level maximum, use checked size arithmetic, and reject over-limit input before resizing; include the runtime’s 10 KiB per-instruction growth bound as a separate limit.
  5. For a schema migration, define the accepted old version, new version, field conversion, idempotent retry behavior, maximum size, authorized payer, and rollback evidence.
  6. Write tests for growth funding, ordinary shrink, vault protection, zero/max/max-plus-one lengths, arithmetic overflow, schema bytes, and unchanged protected balances after every rejected path.

Before you move on, check your reasoning

  • Describe the learner record fields without adding personal information.
  • Write preconditions and postconditions for issue_credential.
  • Test wrong authority, wrong course revision, and repeated issue.

OPEN SELF-CHECK · UNGRADED

Pause and test your reasoning

Practice 1 of 3

A writable account has the expected bytes but an unexpected runtime owner. Should the program update it?

  1. Yes; writable proves the account belongs to the program.
  2. No; validate the expected address, runtime owner, account type, and relevant authority before using its data.
  3. Yes; matching bytes are sufficient if the client selected the address.
Reveal the answer and explanation

One best answer: No; validate the expected address, runtime owner, account type, and relevant authority before using its data.

Writable is an instruction access privilege, not proof of account ownership or identity. A robust handler validates the PDA or expected address, owning program, decoded type, signer, and stored authority before changing state.

Sources: Account Structure ↗ · Account Constraints ↗

Practice 2 of 3

An Anchor handler accepts an unchecked account. What must protect its data before the handler trusts it?

  1. Writable access and nonzero lamports are enough to trust any account supplied by the client.
  2. A nonzero lamport balance and writable flag prove the supplied account is safe to trust.
  3. Validate address, owner, layout, and authority.
Reveal the answer and explanation

One best answer: Validate address, owner, layout, and authority.

Unchecked account wrappers deliberately omit typed ownership checks. Programs must enforce every required identity and state condition at the instruction boundary before mutation. Anchor constraints can enforce several of these checks declaratively.

Sources: Account Types ↗ · Account Constraints ↗

Practice 3 of 3

A program-owned account has the expected runtime owner. Is that alone enough to authorize an update?

  1. Runtime ownership alone does not authorize an update.
  2. Owner means anyone may write.
  3. Once the runtime owner matches, any writable signer can change the account without checking its stored authority or state.
Reveal the answer and explanation

One best answer: Runtime ownership alone does not authorize an update.

Runtime owner identifies which program can modify data; it does not satisfy the application’s signer, PDA, stored-authority, or business-state rules by itself. Each requirement needs an explicit validation path.

Sources: Accounts ↗ · Account Constraints ↗

This practice is not scored, saved, or part of a certificate. Account-based lesson checkpoints and level exams remain separate.

Scored checkpoints and level exams are available online through a free account. They are not included in this pack.

Optional lab files

These files are included with the pack. Follow the safety and toolchain notes in the lesson before running a lab; a Devnet read requires an internet connection.

READ THE SOURCE

Go deeper in the official docs

Reviewed on 2026-10-02. Protocol documentation can change; check the linked page’s current version when you reconnect.