Intermediate · LESSON 09 OF 12
9. Anchor validation and negative tests
By the end, you can translate invariants into constraints and executable checks.
Read the workflow
The test suite changes one security-relevant input at a time: issuer authority, supplied address, duplicate claim, level, or course revision. It expects invalid cases to fail at the proper transaction, account-validation, or handler boundary, then checks that protected state did not change. A successful control confirms that legitimate claims still work.
- ISSUER AUTHORIZATION
- WRONG AUTHORITY
- CANONICAL ADDRESS
- DUPLICATE CLAIM
- LEVEL / REVISION
Read a text version: Choose a test harness by evidence boundary
Move from pure helper checks to an in-process SVM or direct instruction harness, then use a local Surfpool or legacy validator when the claim depends on RPC and validator behavior. Mocked RPC tests cover client handling of delays, nulls, and malformed responses, but do not prove on-chain enforcement. Crucible explores stateful action sequences against invariants, while a clean bounded run cannot prove every input safe. Every failure case also needs an unchanged-state assertion and a successful control.
- PURE UNIT
- LITESVM
- MOLLUSK
- SURFPOOL / VALIDATOR
- MOCK RPC
- CRUCIBLE FUZZ
FIELD NOTE 01
Types are a starting point
Anchor account types and constraints cover different checks. `Signer` or a signer constraint checks that the account signed the transaction; it does not prove that this signer is the authority stored in another account. `Account<T>` checks the owner program and deserializes the expected type. `has_one` can compare a stored public key with the supplied account, while `seeds` and `bump` validate a PDA relation. Combine the checks required by the invariant; no one type or annotation proves every relationship. Anchor performs its account-context checks before the handler runs, so identify that boundary in the tests and read the behavior for the exact framework version you use.
FIELD NOTE 02
Test the attacker’s inputs
For a state-changing instruction, test a valid-but-unauthorized signer, wrong owner program, wrong stored authority, wrong mint, wrong PDA, duplicate invocation, and boundary values. A missing required transaction signature is rejected before program logic executes; test that transaction-level gate separately from an instruction that executes and fails an Anchor account constraint. For each failure, assert the expected boundary and preserve relevant account data and token balances. An error string alone does not prove that protected state stayed unchanged.
FIELD NOTE 03
Build a local feedback loop
Pin tool versions and use a local test environment. Keep fixtures separate from live keys. Reproduce a successful instruction first, then mutate one input at a time. Record which invariant each test protects so future refactoring does not turn a useful test into a snapshot of incidental implementation details. Anchor 1.0 changed `anchor init` to generate a LiteSVM test template by default; TypeScript/Mocha remains an explicit option. For an existing workspace, inspect its pinned CLI version and `Anchor.toml` instead of assuming the generated harness. This course’s registry lab pins Anchor 1.2.0 and LiteSVM 0.16.0.
FIELD NOTE 04
Negative tests define the security boundary
A passing happy-path test shows only that one expected sequence works. For every instruction, write who may call it, which account keys and relationships must match, which fields can change, and what must remain unchanged on failure. Test a wrong signer who did sign, wrong PDA seeds, wrong mint, stale state, duplicate invocation, and unexpected writable inputs. A transaction missing a required signature is rejected before the program handler; do not describe it as a handler-level rejection. Capture the expected error at its actual boundary and compare protected state before and after the attempt. Add a successful control case so the security change does not block the intended caller.
FIELD NOTE 05
Choose the test boundary deliberately
Choose the smallest test harness that observes the behavior you claim. LiteSVM executes compiled program bytes in a fast in-process Solana VM and lets a test control account state and sysvars; it is not an RPC node. Mollusk is a lower-level Rust harness that executes instructions against account states you supply; it does not build a validator runtime or AccountsDB. For a local integration path that needs RPC and validator behavior, Anchor v1.2.0 uses Surfpool for localnet `anchor test` by default; `--validator legacy` selects `solana-test-validator`. In-process templates can set `skip_local_validator = true`. Record the versions and `Anchor.toml` that choose the actual path. A mocked RPC client tests how application code handles responses, not whether the program enforces an invariant.
FIELD NOTE 06
Turn each invariant into an adversarial case
For every rule, name the expected failure boundary. If only the learner may update a record, change the signer. If only a particular mint is allowed, pass a different valid mint. If a PDA must be canonical, change a seed or bump. If an exam claim is one time, replay it. In every failure test, assert both the error and the postcondition that protected account state did not change. Also test a successful instruction so a security fix cannot silently make legitimate learners unable to complete the flow.
FIELD NOTE 07
Use properties and fuzzing to find input gaps
Example tests cover the cases you remember to write. Property tests vary addresses, amounts, or instruction sequences against a stated rule such as “failed authorization never changes protected state.” Anchor’s `anchor fuzz` uses the Crucible engine for coverage-guided stateful action sequences and checks invariants after each action. Define fixture setup, valid actions, and the property that must always hold; run with a finite time or resource budget. If it finds a crash, replay and minimize the reproducing sequence, then keep that minimized case as a deterministic regression. A clean fuzz run means no counterexample was found within that run; it does not prove every input safe. Verify tool and SBF compatibility for the pinned project.
FIELD NOTE 08
Turn an invariant into a reviewable test artifact
For a one-time learner claim, write the invariant first: only the expected learner can create the canonical course record once, and any rejected attempt leaves the registry unchanged. Prepare a valid control fixture, then change one condition at a time: a different signer who really signed, a noncanonical address, a second claim, or a stale course revision. At the correct failure boundary, assert the expected error and compare the protected account bytes or decoded state before and after. Keep client-response tests separate from program-enforcement tests, and record the fixture, runtime, tool versions, result, and remaining limits beside the test.
MAKE THE CONNECTION
A detail worth keeping
Positive tests establish that a supported caller can make progress. Negative tests establish the security boundary. For an unauthorized attempt, inspect account bytes, balances, and mint count before and after, not just the error message. Include property-based variations for addresses and amounts where available.
Work through an example
Use a test validator with disposable fixtures and pinned program/tool versions. Document the exact command and expected result. Never point automated tests at mainnet signing credentials. A green test suite is evidence for those test cases, not a protocol audit.
assert!(!send(&mut svm, &issuer, duplicate));
let record = read_record(&svm, &credential);
assert_eq!(record.revision, 2);
assert_eq!(record.content_hash, CONTENT_HASH);Use the free Test Harness Selection Lab to choose evidence-matched tools for five testing goals. Then write a test matrix for a credential instruction with six hostile inputs. For each, record the test layer, expected rejection, unchanged account state, and a valid control.
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.
- Open the offline Test Harness Selection Lab. For each fictional goal, choose the smallest harness that observes the stated behavior and read the limit attached to the answer.
- Download Anchor credential lab 01 and unzip it outside the course repository.
- Follow its README to verify Anchor 1.2.0, the Solana CLI, and the pinned LiteSVM test toolchain in WSL Ubuntu.
- Run anchor build and identify the compiled SBF program artifact.
- Run anchor test and inspect the four LiteSVM cases for issuer authorization, canonical addresses, duplicate claims, levels, and revisions.
- Explain why this teaching registry is not an NFT and how the production passport instead uses Metaplex Core.
Before you move on, check your reasoning
- Write a seven case threat-driven test matrix.
- Make each negative case assert unchanged state.
- Run a clean local test from a fresh checkout and record versions.
OPEN SELF-CHECK · UNGRADED
Pause and test your reasoning
Practice 1 of 3
An Anchor test passes for one expected signer and one valid PDA. What does that establish?
- The assertions passed for the tested setup and cases; additional negative, boundary, and deployment checks remain necessary.
- Every signer, PDA, deployment, RPC endpoint, and production account layout is safe.
- The program cannot be vulnerable because Anchor generated the account context.
Reveal the answer and explanation
One best answer: The assertions passed for the tested setup and cases; additional negative, boundary, and deployment checks remain necessary.
A test is evidence about the cases and assertions it actually exercised. Add wrong-signer, wrong-owner, wrong-PDA, replay, boundary, and state-invariant failures; local test success is not proof of deployment or RPC health.
Sources: Anchor Testing ↗ · Fuzzing ↗
Practice 2 of 3
A negative test expects an instruction to reject a substituted token account. What should it assert after failure?
- Any returned error string proves the intended security boundary rejected this specific attack.
- Assert rejection and unchanged protected state.
- One successful negative test proves every substituted account is rejected without state changes.
Reveal the answer and explanation
One best answer: Assert rejection and unchanged protected state.
A useful adversarial test checks the enforcement boundary and reloads affected state. An error alone may come from the wrong layer and does not prove the invariant was preserved.
Sources: Account Constraints ↗ · LiteSVM ↗
Practice 3 of 3
A local LiteSVM suite passes for a program build. What deployment claim does this support?
- It proves Mainnet deployment.
- A local LiteSVM run demonstrates the tested cases but says nothing about the deployed bytes or upgrade authority.
- Local success does not prove the deployed binary or authority.
Reveal the answer and explanation
One best answer: Local success does not prove the deployed binary or authority.
Local execution tests are valuable evidence about the tested artifact and cases. They do not identify a deployed program or verify its binary provenance, upgrade authority, or runtime dependencies.
Sources: LiteSVM ↗ · Anchor Testing ↗
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.