← Back to notes
Rust Ecosystem 2026-06-07 16:08 6 min read Local copy

Building SolaNotes: Deterministic PDA Design in a Solana Program

Building SolaNotes: Deterministic PDA Design in a Solana Program
Kode-n-Rolla
Kode-n-Rolla

Posted on Jun 7

Building SolaNotes: Deterministic PDA Design in a Solana Program
#solana #rust #blockchain

πŸ‘‹ Intro

Hey friend, my name is Pavel, also known as kode-n-rolla.

I am a Security Engineer, and today I want to walk through a small Solana program that helped me explore one of the most important foundations of Solana development: accounts.

Because on Solana, everything is an account.

Unlike EVM contracts, Solana programs do not keep their own internal storage in the same way. A program contains logic, while state lives in separate accounts passed into instructions. Once this idea clicks, the rest of Solana development starts to make much more sense.

That is why I built SolaNotes (Project lives here).

Let’s dive in. πŸ₯½

About

Small on-chain programs are useful when the scope is narrow enough to make every architectural decision visible.

That was the idea behind SolaNotes - a compact Solana/Anchor notes program built around deterministic PDA derivation, explicit ownership checks, fixed account sizing, monotonic identifiers, and testable state transitions.

The application domain is intentionally simple: users can initialize a profile, create notes, update their own notes, and close notes. But even this small workflow touches several important Solana development patterns:

  • how to model user-owned state;
  • how to derive deterministic accounts;
  • how to prevent unauthorized account mutation;
  • how to avoid accidental PDA reuse;
  • how to test both valid and invalid flows.

The goal was not to build a full notes product. The goal was to design a small on-chain workflow cleanly.

Core Idea

SolaNotes has two account types:

  • UserProfile
  • Note

Each user gets one profile PDA. Each note is derived from the user authority and a monotonic note id.

profile PDA = ["profile", authority]
note PDA = ["note", authority, note_id]

The profile stores the owner and the next note id. The note stores the actual note data.

The important detail is that note_count is not treated as "number of active notes". It is treated as a monotonic id source. Closing a note does not decrement it.

That design prevents accidental PDA reuse.

Account Model

Wallet["User wallet / signer"] --> Program["SolaNotes program"]

Program --> Profile["UserProfile PDA, seeds: ['profile', authority]"]
Profile --> Counter["note_count, monotonic id source"]

Program --> Note0["Note PDA #0, seeds: ['note', authority, 0]"]
Program --> Note1["Note PDA #1, seeds: ['note', authority, 1]"]

Note0 --> Data0["created_at, updated_at"]
Note1 --> Data1["created_at, updated_at"]

The UserProfile account stores:

authority: Pubkey
note_count: u64
created_at: i64

The Note account stores:

authority: Pubkey
note_id: u64
title: String
content: String
created_at: i64
updated_at: i64

The identity of a note is stable after creation:

  • authority does not change;
  • note_id does not change;
  • created_at does not change.

Only the note content and updated_at timestamp are mutable.

Instruction flow

SolaNotes exposes four instructions:

initialize
create_note
update_note
close_note

initialize

The initialize instruction creates the user profile PDA.

It sets:

  • the signer as the profile authority;
  • note_count to 0;
  • created_at from the Solana clock sysvar.

This creates the root account for all future notes owned by that signer.

create_note

The create_note instruction creates a new note PDA using the current profile counter.

The flow is:

User->>Program: create_note(title, content)
Program->>Program: validate title/content length
Program->>Profile: read note_count
Program->>Note: initialize PDA using authority + note_count
Program->>Note: write note data
Program->>Profile: increment note_count
Program-->>User: transaction confirmed

The note id comes from profile.note_count.

After the note is created, the counter is incremented using checked arithmetic. This keeps note creation deterministic while avoiding silent overflow behavior.

update_note

The update_note instruction allows the owner to update an existing note.

The note PDA is derived from:

["note", signer, note_id]

This means that the account structure itself already encodes ownership. The instruction also verifies that the passed note belongs to the signer.

The update changes:

  • title;
  • content;
  • updated_at.

It does not change:

  • authority;
  • note_id;
  • created_at.

close_note

The close_note instruction closes the note account and refunds rent to the signer.

The profile counter is not decremented.

This is a small but important design decision. If note #0 is closed, the next created note still gets id #1, not #0 again.

That keeps PDA derivation predictable and prevents identity reuse.

Validation model

The program uses two layers of validation.

The first layer is structural validation through Anchor account constraints:

  • seeds
  • bump
  • init
  • mut
  • close
  • account ownership constraints

The second layer is business-logic validation inside instruction handlers:

  • title length checks;
  • content length checks;
  • counter overflow checks.

This separation keeps the account model explicit. PDA structure and ownership are handled at the account level. Application-specific rules are handled in the instruction logic.

Why fixed-size account allocation matters

The program uses fixed limits for note data:

MAX_TITLE_LENGTH = 64
MAX_CONTENT_LENGTH = 512

The string length checks are performed using UTF-8 byte length, matching how the account size is calculated on-chain.

This avoids a common class of issues where the client assumes one size model, while the on-chain account allocation uses another.

The current version intentionally avoids realloc. That keeps the first version simpler and makes the account sizing rules easier to reason about.

Testing strategy

The test suite is written with Anchor TypeScript tests.

The tests cover both successful and rejected flows:

  • profile initialization;
  • first note creation;
  • second note creation;
  • note update by the owner;
  • rejected update attempt by another signer;
  • note closure;
  • verification that the closed account no longer exists;
  • verification that a closed note id is not reused.

One important test checks that an attacker cannot update another user's note.

The attacker signs the transaction, but the PDA derivation and account constraints do not match the attacker's authority. The transaction fails, and the original note data remains unchanged.

That test is small, but it validates the most important security property of the program: only the note owner can mutate their own note account.

What I intentionally left out

SolaNotes is intentionally compact.

The current version does not include:

  • note sharing;
  • indexing;
  • search;
  • dynamic resizing;
  • encryption;
  • frontend integration;
  • multi-user collaboration.

Those features would add product complexity. For this version, I wanted the core account architecture to stay visible.

The focus was:

one user β†’ one profile PDA β†’ many deterministic note PDAs

That constraint made it easier to reason about ownership, state transitions, and test coverage.

Engineering decisions:

  • I used one profile PDA per authority to keep user-owned state explicit.
  • I used a monotonic note counter to avoid PDA reuse after account closure.
  • I kept note identity immutable after creation.
  • I avoided realloc in the first version to keep account sizing predictable.
  • I tested rejected ownership flows, not only happy paths.

πŸ€” Final thoughts

SolaNotes is a small program, but it was useful as an engineering exercise in Solana account design.

The main takeaway is that even a minimal on-chain workflow benefits from disciplined architecture:

  • deterministic PDA derivation;
  • explicit ownership checks;
  • monotonic identifiers;
  • clear account sizing;
  • separate structural and business validation;
  • tests for both expected and rejected behavior.

Small scope does not mean casual implementation.

Sometimes a compact program is the best place to make the architecture explicit.

🀝 Connect Section

If you enjoyed this research breakdown, feel free to connect:

Stay Cyber Safe πŸ›Ÿ

Top comments (0)

Subscribe

For further actions, you may consider blocking this person and/or reporting abuse