← Solana Builder

Intermediate · LESSON 10 OF 12

10. Native Rust processors and explicit account checks

By the end, you can trace how a native rust solana processor validates instruction bytes and caller-supplied accounts before changing program state, and compare that work with anchor and pinocchio.

Look up Solana terms in the glossary

In this lesson8 field notes · practice · sources · checkpoint
Native Rust, Anchor, and Pinocchio use different syntax, but each must enforce account identity, schema, authority, and transaction privileges.
Native Rust, Anchor, and Pinocchio use different syntax, but each must enforce account identity, schema, authority, and transaction privileges.Open full-size visual ↗
Read a text version of this diagram

Three columns compare Native Rust, Anchor, and Pinocchio as implementation approaches. Each column leads into the same shared safety gate: decode the instruction contract, verify expected account identity and runtime owner, validate schema and canonical address, check signer and stored application authority, then enforce writable and CPI privileges. A successful checked transition may commit; any failed validation returns an error without applying the intended state change. Framework syntax can automate some checks, but the runtime invariants and business rules remain the program team’s responsibility.

  1. CALLER-SUPPLIED BYTES + ACCOUNTS
  2. EXACT DISPATCH + BOUNDED DECODE
  3. OWNER + PDA + SCHEMA VALIDATION
  4. SIGNER + STORED AUTHORITY + PRIVILEGES
  5. CHECKED STATE TRANSITION + CPI
  6. COMMIT OR REJECT
A local build-to-test path compiles the Rust processor, loads it in LiteSVM, checks valid and rejected instructions, then verifies counter bytes.
Build and test a native Rust processor locally. A local build-to-test path compiles the Rust processor, loads it in LiteSVM, checks valid and rejected instructions, then verifies counter bytes.Open full-size visual ↗
Read a text version: Build and test a native Rust processor locally

The workflow begins by compiling the program source to an SBF artifact. The LiteSVM test loads that exact local artifact into an in-memory runtime and submits a signed increment using a canonical PDA. Negative cases change one condition at a time, including owner, address, signer, schema, instruction shape, or arithmetic. The final evidence compares the full state bytes before and after every rejection and confirms a valid call changes only the counter field. No public cluster, wallet file, or real SOL is needed.

  1. Compile the processor to SBF
  2. Load that exact build into LiteSVM
  3. Submit a signed canonical increment
  4. Reject malformed or unauthorized inputs
  5. Compare account bytes and mutation tests
FIELD NOTE 01

Choose an abstraction before writing a program

A custom program is not a prerequisite for using Solana. Transfers, token minting, token transfers, and account creation are usually instructions to existing programs; a client can compose those instructions without redeploying their logic. Write a program only when the desired rules or persistent state cannot be expressed safely with the existing programs. Solana's Rust guide recommends Anchor for beginners. Because this Intermediate lesson follows an Anchor-focused module, it uses Native Rust to expose processor checks that framework constraints can help express. When custom logic is justified, choose a framework for the team and deployment constraints. Native Rust exposes the processor and account handling directly. Anchor supplies conventions, generated interfaces, and declarative account constraints. Pinocchio offers a lower-level account-processing interface with explicit parsing. Solana's official Rust SDK page currently warns that Pinocchio remains in development and may introduce breaking changes; pin its exact version, retain the lockfile, and review migration notes before upgrading. These are different authoring choices for code that still runs under the same SVM account, instruction, privilege, and ownership rules. None makes business authorization correct automatically.

FIELD NOTE 02

The processor receives untrusted inputs

A native processor is called with its program ID, an ordered slice of AccountInfo values, and instruction bytes. Treat all three as inputs to validate. The transaction signer chose the account list; a malicious client can reorder, substitute, omit, duplicate, or append accounts and encode malformed instruction data. Dispatch the instruction tag explicitly, check its version and exact byte length, decode integers with a specified endianness, and reject unknown tags or truncated/trailing bytes according to the instruction contract. Use fallible reads and checked arithmetic. Avoid indexing, unwrap, or unchecked deserialization on caller-controlled bytes. Parsing failure should return an error before account data is mutated.

FIELD NOTE 03

Check identity, ownership, signer, and writability separately

A program can read accounts passed to it, but the runtime owner field identifies which program may modify an account data buffer or debit its lamports. A writable transaction meta only grants requested write access; it does not transfer ownership or application authority. A signer flag proves that the matching transaction key signed; it does not prove the key is the authority recorded for this particular state. Check the expected account address and order or explicitly parse named accounts, expected owner program, account type and data length, signer status, writable status, and any mint or token-program relationship. Then compare the signer key to the authority encoded in the verified state. Each check answers a different question and should have a deliberate failure case.

FIELD NOTE 04

Define the byte layout as part of the program interface

Native programs own their account serialization choices. For a fixed-size record, specify every field, its byte count, integer endianness, discriminator or type tag, schema version, and total account space. For variable data, define bounded lengths and how a reader rejects oversized or malformed values. Deserialize only after checking the owner and the minimum or exact length required by the selected version. When a layout changes, define migration behavior instead of interpreting old bytes as the new structure. Keep the state borrow short: read and validate first, compute the proposed transition with checked operations, then borrow mutably and write the complete new representation. A failed guard must leave the submitted state unchanged.

FIELD NOTE 05

Bind the requested action to the correct state

Suppose an increment instruction is meant to change one counter for one learner. Derive the expected PDA from the documented seed bytes, learner public key, and this program ID; verify the supplied address and canonical bump. Confirm the account is owned by this program, has the counter discriminator/version and exact layout, and stores the same learner key that signed. Reject a mismatched seed, stale schema, wrong authority, or arithmetic overflow. A PDA has no private key; only its deriving program can provide its signer privilege to a CPI through invoke_signed with the correct seeds. That mechanism is not a substitute for checking which account is being signed for or why.

FIELD NOTE 06

Audit every cross-program invocation

A native CPI builds an instruction for a specific callee program and supplies the required account metas. Verify the intended callee program ID, account identities, writable and signer requirements, and the relationship between the state transition and CPI result. Runtime privilege rules prevent a callee from acquiring signer or writable privileges the caller did not have, except a PDA signer the caller program derives with invoke_signed. Do not forward every remaining account blindly. A downstream Token or Token-2022 call can fail because an extension requires extra accounts; document the selected token program, mint extensions, expected hook accounts, and failure behavior. If a CPI fails, propagate the error so the transaction does not continue with application state that assumes success.

FIELD NOTE 07

Test rejection paths and unchanged state

A happy-path increment proves very little. Use a deterministic local SVM harness such as LiteSVM to run the compiled program against a controlled account set. Test a valid owner and authority, then substitute the wrong owner, wrong PDA, unrelated signer, non-signer, read-only state account, malformed instruction tag, truncated data, incompatible schema version, and a counter at its maximum. For every rejected instruction, assert the precise error category where stable and compare the account bytes and lamports before and after. Add CPI tests for the wrong callee and missing extension accounts where relevant. A local harness is fast and reproducible, but it is not a full validator, a live RPC provider, or proof that a production deployment uses these exact bytes.

FIELD NOTE 08

Compare frameworks without confusing syntax for security

Read the same account-checking example in Solana Foundation’s Native Rust, Anchor, and Pinocchio variants. Anchor constraints can encode owner, signer, PDA, and relationship checks declaratively; still review the generated assumptions, instruction semantics, CPI targets, and business rules. Native Rust makes parsing and validation visible but leaves more boilerplate for the author to get right. Pinocchio exposes a lower-level account-processing interface; it does not remove identity, ownership, or authorization checks. Record the repository commit, crate versions, lockfile, and toolchain used, then run the example’s documented tests locally. For a real program, prefer the abstraction your maintainers can test, review, upgrade, and operate correctly.

MAKE THE CONNECTION

A detail worth keeping.

Separate the runtime account owner from the application authority stored in bytes. An account owned by your program can still name the wrong wallet as its product-level authority. Conversely, a valid wallet signature does not grant permission to rewrite data owned by another program. Test both directions: wrong runtime owner and wrong stored signer.

Work through an example

Treat instruction encoding and account layout as versioned interfaces. Fix an explicit tag and byte order; bound variable payloads; reject malformed, unknown, truncated, or ambiguous encodings; and make schema migration a separate audited transition. Keep the parser total over arbitrary input: every byte sequence should return a defined error or a validated instruction, never panic.

Illustrative native-processor validation outline, not a complete compiling program. Pin the Solana crate versions and finish the address, schema, and stored-authority checks before any mutable borrow.
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    if instruction_data.len() != 1 || instruction_data[0] != INCREMENT_TAG {
        return Err(ProgramError::InvalidInstructionData);
    }
    let mut iter = accounts.iter();
    let state = next_account_info(&mut iter)?;
    let authority = next_account_info(&mut iter)?;
    if state.owner != program_id {
        return Err(ProgramError::IncorrectProgramId);
    }
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }
    if !state.is_writable {
        return Err(ProgramError::InvalidArgument);
    }
    // Verify the canonical PDA, versioned layout, and stored authority.
    // Compute a checked transition, then borrow and write state.
    Ok(())
}

Worked design — CounterState v1 has a fixed 41-byte layout: one version byte, a 32-byte authority public key, and an eight-byte little-endian count. Its PDA seeds are the literal bytes for “counter” and the authority key, derived under the program ID with the canonical bump. The increment instruction is exactly one byte with tag 1. The processor first requires exactly the state and authority accounts in the declared order; checks the state address, this program as runtime owner, writable status, 41-byte length, version 1, signer status, stored authority equality, and the PDA derivation; reads the count and uses checked_add; then writes only the eight count bytes. It rejects an unrelated signer even if that signer supplies a valid counter for someone else. Design separate tests for each guard, including a valid signer with the wrong PDA and a correct PDA whose stored authority differs. Snapshot the account before each negative case and prove bytes and lamports are unchanged. The initialize instruction and any System Program CPI are separate behavior and need their own validation and tests.

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.

  1. Download the Native Rust Counter Lab from this lesson or the Intermediate offline pack. In WSL or Linux, run cargo build-sbf and cargo test --locked; LiteSVM executes the compiled local program without a cluster.
  2. Compare the Native Rust, Anchor, and Pinocchio Checking Accounts examples maintained by Solana Foundation. Pin the repository commit and exact crate versions, keep the lockfile, and read the README and release notes because Pinocchio APIs can change.
  3. Write a fixed-size CounterState v1 interface with exact byte offsets, total size, instruction tag, seeds, expected owner, signer, writable account, and one allowed state transition. Do not include email, name, or other personal data.
  4. Build a negative-test matrix: wrong program owner, wrong PDA, unrelated signer, non-signer, read-only account, malformed tag, short state, unknown version, and arithmetic overflow. For each case predict the error and whether bytes or lamports may change.
  5. Run the downloaded lab and record the Rust, SBF, and LiteSVM versions, command, expected result, and observed output. The tests are local; do not claim a public deployment or validator rehearsal.
  6. Mutate one validation rule in a disposable copy of the lab. Confirm its negative test fails, restore the rule, and rerun the suite. Do not use a production keypair or deploy the teaching program to a public cluster.

Before you move on, check your reasoning

  • A
  • c
  • c
  • o
  • u
  • n
  • t
  • a
  • r
  • r
  • a
  • y
  • s
  • a
  • r
  • e
  • c
  • a
  • l
  • l
  • e
  • r
  • -
  • s
  • e
  • l
  • e
  • c
  • t
  • e
  • d
  • ,
  • s
  • o
  • a
  • c
  • c
  • o
  • u
  • n
  • t
  • o
  • r
  • d
  • e
  • r
  • a
  • n
  • d
  • r
  • e
  • m
  • a
  • i
  • n
  • i
  • n
  • g
  • a
  • c
  • c
  • o
  • u
  • n
  • t
  • s
  • a
  • r
  • e
  • p
  • a
  • r
  • t
  • o
  • f
  • t
  • h
  • e
  • p
  • u
  • b
  • l
  • i
  • c
  • a
  • t
  • t
  • a
  • c
  • k
  • s
  • u
  • r
  • f
  • a
  • c
  • e
  • .
  • V
  • a
  • l
  • i
  • d
  • a
  • t
  • e
  • t
  • h
  • e
  • e
  • x
  • a
  • c
  • t
  • e
  • x
  • p
  • e
  • c
  • t
  • e
  • d
  • k
  • e
  • y
  • ,
  • o
  • w
  • n
  • e
  • r
  • ,
  • d
  • a
  • t
  • a
  • t
  • y
  • p
  • e
  • ,
  • p
  • r
  • i
  • v
  • i
  • l
  • e
  • g
  • e
  • s
  • ,
  • a
  • n
  • d
  • r
  • e
  • l
  • a
  • t
  • i
  • o
  • n
  • s
  • h
  • i
  • p
  • s
  • f
  • o
  • r
  • e
  • v
  • e
  • r
  • y
  • r
  • e
  • q
  • u
  • i
  • r
  • e
  • d
  • a
  • c
  • c
  • o
  • u
  • n
  • t
  • .
  • W
  • h
  • e
  • n
  • a
  • C
  • P
  • I
  • u
  • s
  • e
  • s
  • a
  • t
  • o
  • k
  • e
  • n
  • e
  • x
  • t
  • e
  • n
  • s
  • i
  • o
  • n
  • ,
  • d
  • e
  • f
  • i
  • n
  • e
  • t
  • h
  • e
  • s
  • u
  • p
  • p
  • o
  • r
  • t
  • e
  • d
  • t
  • o
  • k
  • e
  • n
  • p
  • r
  • o
  • g
  • r
  • a
  • m
  • a
  • n
  • d
  • h
  • o
  • o
  • k
  • -
  • a
  • c
  • c
  • o
  • u
  • n
  • t
  • c
  • o
  • n
  • t
  • r
  • a
  • c
  • t
  • r
  • a
  • t
  • h
  • e
  • r
  • t
  • h
  • a
  • n
  • f
  • o
  • r
  • w
  • a
  • r
  • d
  • i
  • n
  • g
  • a
  • n
  • u
  • n
  • c
  • h
  • e
  • c
  • k
  • e
  • d
  • t
  • a
  • i
  • l
  • o
  • f
  • a
  • c
  • c
  • o
  • u
  • n
  • t
  • s
  • .

OPEN SELF-CHECK · UNGRADED

Pause and test your reasoning

Practice 1 of 3

A processor receives a valid learner signature and a writable counter account. Which independent checks still establish the intended counter?

  1. Check the canonical PDA, owner, schema, and signer authority.
  2. Accept a writable account from any owner after any learner signs a valid message, even when its data layout is unknown.
  3. Ask an RPC provider to decide which on-chain address the program should update.
Reveal the answer and explanation

One best answer: Check the canonical PDA, owner, schema, and signer authority.

Signer and writable flags answer only two narrow runtime questions. They do not bind the supplied state address, runtime owner, versioned byte layout, or application-level authority. Check each invariant before writing.

Sources: Account Structure · Program Execution · Program-Derived Addresses

Practice 2 of 3

A native instruction accepts one byte, but receives a recognized tag followed by unbounded extra data. What should its parser do?

  1. Assume the runtime checks custom instruction schema and silently ignores extra bytes.
  2. Reject bytes outside the declared contract or decode a documented, bounded versioned payload.
  3. Write the extra bytes into the first supplied writable account to preserve forward compatibility.
Reveal the answer and explanation

One best answer: Reject bytes outside the declared contract or decode a documented, bounded versioned payload.

The runtime validates transaction encoding, not custom instruction semantics. Explicit tags, versions, bounds, and fallible decoding prevent different program versions from interpreting the same byte sequence differently.

Sources: Developing Programs in Rust · Program Execution

Practice 3 of 3

A required Token-2022 transfer-hook CPI returns an error after the processor prepared its state update. What is the correct result?

  1. Ignore the error because the outer wallet signature proves the transfer succeeded.
  2. Retry with every remaining account without validating the callee or account metas.
  3. Propagate failure so the transaction cannot commit application state that claims the transfer succeeded.
Reveal the answer and explanation

One best answer: Propagate failure so the transaction cannot commit application state that claims the transfer succeeded.

A signature authorizes a transaction message; it does not mean every invoked instruction succeeds. Validate the CPI target and accounts, propagate errors, and test that application state remains consistent when a required downstream instruction fails.

Sources: Cross Program Invocation · Program Examples

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-10-02. Protocol documentation can change; check the linked page’s current version as you use this material.

  1. Developing Programs in Rust ↗
  2. Solana Rust SDK ↗
  3. Program Examples ↗
  4. Account Structure ↗
  5. Program Execution ↗
  6. Cross Program Invocation ↗
  7. Program-Derived Addresses ↗
  8. Anchor Account Constraints ↗
  9. Solana Foundation Program Examples ↗

Report an issue with this lesson

YOUR PROGRESS, SAVED

Ready for the checkpoint?

Make a free account to save your progress and unlock the quizzes.

Create free account