Intermediate · LESSON 02 OF 06
2. Programs and explicit account state
By the end, you can model an instruction and its required accounts.
Optional reading history is saved only in this browser. It is not a scored checkpoint or exam result.
In this lesson8 field notes · practice · sources · checkpoint
Read a text version of this diagram
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.
- CALLER SELECTS ACCOUNTS
- OWNER + TYPE + PDA CHECKS
- SIGNER + AUTHORITY + REVISION
- STATE UPDATE OR REJECT
Read the annotated account layout in text
Solana keys account state by a separate 32-byte address. The runtime account has five fields: lamports, data, owner, executable, and deprecated rent_epoch. Writable is an instruction access meta, not one of those fields and not authority. The fixed-size Anchor LearnerRecord example stores an 8-byte default discriminator, 32-byte learner public key, 16-byte course ID, 32-byte revision hash, and six one-byte completion flags: 94 serialized data bytes total. The academy program ID is the owner; the learner key stored inside the data is a separate application authority. This is an illustrative non-zero-copy Borsh layout, not the academy credential account.
- Derive the expected address from the learner, course identifier, and academy program ID; validate the canonical bump.
- Decode the expected program-owned Anchor account type before reading its fields.
- Require a transaction signer and compare that key with the learner public key stored in the record.
- Check the active course revision before the handler changes completion state. A writable flag alone grants no ownership.
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.
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.
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.
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.
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.
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.
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.
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.
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.
#[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.
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. Most exercises work on paper or with a local test; only a lesson that clearly says “read-only Devnet” makes a public network request.
- On paper, define a fictional `LearnerRecord` with a learner public key, fixed-size course identifier, curriculum revision, and completion state. Do not include a name, email, score, or answer data.
- Write the update interface: record PDA seeds, stored authority, signer, which account is writable, required course/revision inputs, and the one allowed state transition.
- Walk through the Anchor excerpt. For every constraint, write the invariant it enforces and one request an attacker could try if that check were removed.
- Create five local negative cases: another learner’s valid PDA, a record with a different stored authority, a missing signer, a stale curriculum revision, and the wrong account type or owner.
- For each rejected case, assert both the expected error and that the record bytes and completion state are unchanged. Add a successful control case, then review initialization and account sizing separately: record who pays storage, who can close the record, and how a future schema migration preserves the assessed revision.
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
A writable account has the expected bytes but an unexpected runtime owner. Should the program update it?
Reveal one best answer and its 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
This practice is not scored, saved, or part of a certificate. Account-based lesson checkpoints and level exams remain separate.
Never enter a recovery phrase or private key into a learning site or lesson exercise.
READ THE SOURCE
Go deeper in the official docs
Reviewed on 2026-09-29. Protocol documentation can change; check the linked page’s current version as you use this material.
OPEN LEARNING
Keep learning at your own pace.
This lesson, its guided practice, and all six course lessons are open to everyone. Account-based checkpoints, exams, and saved progress will open when free accounts are ready.
Continue to the next lesson ↗