Create your own
Lesson illustration

Custom Account Structures with `#[account]`

Hello! Welcome back to our journey into the Anchor framework.

In the last lesson, we established the basic architecture of an Anchor program. You learned how the #[program] macro turns a Rust module into a set of on-chain instructions and how the Context<T> object serves as a secure gateway for passing accounts and arguments to those instructions.

Today, we're going to zoom in on the T in Context<T>. We'll explore how to define the actual data structures for the accounts your program will own and manage. This lesson's learning outcome is to define custom account structures using the #[account] attribute for automatic serialization/deserialization. This is how you define your program's "state" or its on-chain database schema.

The Challenge: Storing Custom Data on Solana

From your work with native Solana programs in Module 5, you'll recall that all account data is just a buffer of raw bytes. To work with it, you had to manually serialize and deserialize your Rust structs using borsh. This process is not only tedious but also a common source of errors. If your client-side serialization logic doesn't perfectly match your on-chain deserialization logic, transactions will fail in cryptic ways.

Anchor dramatically simplifies this. Much like a web framework provides an ORM (Object-Relational Mapper) to abstract away raw SQL queries, Anchor abstracts away raw byte manipulation.

Solana Smart Contract Tutorial: Using the Anchor Framework

To see just how much simpler this is, let's watch this clip from Josh's DevBox. It directly compares defining account types in native Solana versus Anchor, highlighting how Anchor eliminates redundant definitions and generates the client-side interface for you.

Watch the segment from 00:44:22 to 00:45:36. Pay close attention to how the native approach requires defining the data structure twice (once in Rust, once in JavaScript), while Anchor requires only one definition in Rust and generates the rest.

This automatic generation of an Interface Definition Language (IDL) file is a concept that should be familiar from your front-end work with tools like GraphQL or gRPC, which generate client-side types from a schema. It creates a robust "API contract" between your on-chain program and your off-chain client.

The #[account] Macro: Your Schema Definition Tool

The magic behind this simplification is the #[account] attribute macro. When you apply this macro to a Rust struct, you're telling Anchor, "This is the schema for a type of account that my program will own and manage."

Let's read the official documentation to understand precisely what this macro does behind the scenes.

Program Structure

The Anchor documentation provides a definitive explanation of the #[account] attribute and a crucial related concept: the account discriminator.

Read the sections titled "#[account] attribute" and "Account Discriminator". Focus on the three key functionalities of the macro and what the discriminator is used for.

Based on that reading, let's summarize the three core jobs of the #[account] macro:

  1. Implements (De)serialization: It automatically implements the necessary traits (AccountSerialize, AccountDeserialize, etc.) to convert your struct to and from the byte array format stored in a Solana account.
  2. Assigns Program Owner: It bakes in the rule that any account of this type must be owned by the current program (the one specified in declare_id!).
  3. Sets a Discriminator: It automatically prepends a unique, 8-byte identifier to the account's data. This discriminator is derived from the struct's name (specifically, the first 8 bytes of the SHA256 hash of "account:<StructName>").

Here is a simple example:

use anchor_lang::prelude::*;

// ... declare_id! and other code ...

#[account]
pub struct Counter {
    pub count: u64,
    pub authority: Pubkey,
}

When you initialize an account of type Counter, its data buffer on-chain will look like this:

BytesContentDescription
0-7[...8 bytes...]8-byte discriminator for Counter
8-15[...8 bytes...]count (a u64)
16-47[...32 bytes...]authority (a Pubkey)

This discriminator is a powerful security feature. If a user accidentally passes a UserProfile account to an instruction expecting a Counter account, Anchor will see the mismatched discriminator and immediately fail the transaction, preventing a whole class of "account confusion" bugs.

Using Custom Accounts in Practice

Now, how do we use our new Counter struct in an instruction? We use it inside the #[derive(Accounts)] struct that you learned about in the last lesson.

However, you don't use the struct directly. Instead, you wrap it in Anchor's Account<'info, T> type.

#[derive(Accounts)]
pub struct Initialize<'info> {
    // ... other accounts ...
}

#[derive(Accounts)]
pub struct Increment<'info> {
    // This field holds our Counter account.
    // It is wrapped in the Account<'info, ...> type.
    #[account(mut)]
    pub counter: Account<'info, Counter>,
    
    // We also check that the signer of the transaction is the authority
    // stored on the counter account. We will cover this `has_one` constraint
    // in the next lesson.
    #[account(has_one = authority)]
    pub authority: Signer<'info>,
}

#[account]
pub struct Counter {
    pub count: u64,
    pub authority: Pubkey,
}

The Account<'info, Counter> wrapper is more than just a container. When Anchor deserializes the incoming account from the transaction, this wrapper performs several crucial validation steps:

  1. Ownership Check: It verifies that the owner field of the raw Solana account is the ID of your program.
  2. Discriminator Check: It reads the first 8 bytes of the account's data and ensures they match the Counter struct's discriminator.
  3. Deserialization: Only if the first two checks pass does it proceed to deserialize the rest of the data buffer into your Counter Rust struct.

Solana Smart Contract Tutorial: Using the Anchor Framework

To further clarify the role of these wrappers, watch this deep dive. The video explains the raw AccountInfo struct (which you used in native Solana) and contrasts it with Anchor's safe Account<T> wrapper.

Watch from 00:09:07 to 00:11:48. This will solidify your understanding of why Account<T> is the safe and preferred way to handle program-owned data accounts in Anchor.

Don't Forget the Space!

As you know, every Solana account must have its space allocated and be funded with enough lamports to be rent-exempt. When defining your account struct, you are also defining its size.

Calculating this size is straightforward but requires attention to detail:

  1. Start with 8 bytes for the Anchor discriminator. This is mandatory for every #[account] struct.
  2. Add the size of each field in your struct.

Here are the sizes of common types:

  • u8, i8, bool: 1 byte
  • u64, i64: 8 bytes
  • u128, i128: 16 bytes
  • Pubkey: 32 bytes
  • String, Vec<T>: 4 bytes for the length prefix, plus the size of the contents.

For our Counter struct: 8 (discriminator) + 8 (count) + 32 (authority) = 48 bytes.

You would use this space value when initializing the account, which we'll cover in detail in the next lesson.

Automating Space Calculation with InitSpace

Manually calculating space is error-prone, especially for complex structs. Anchor provides a convenient helper macro, InitSpace, to do this for you.

A Beginner's Guide to Building Solana Programs with Anchor

The article 'A Beginner's Guide to Building Solana Programs with Anchor' has an excellent section on account space. Let's read it to learn about the InitSpace macro.

Read the subsections "Anchor’s Internal Discriminator" and "Calculating Initial Space". Pay close attention to the #[derive(InitSpace)] example and how ExampleAccount::INIT_SPACE is used.

Using InitSpace, our Counter definition becomes much more robust:

use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)] // Add this derive macro
pub struct Counter {
    pub count: u64,
    pub authority: Pubkey,
}

// When initializing, we can now use:
// space = 8 + Counter::INIT_SPACE

Note: The documentation shows 8 + ExampleAccount::INIT_SPACE. Recent Anchor versions often include the discriminator in INIT_SPACE directly. Best practice is to use solana account <address> to verify the size of a created account during testing to be certain. For now, we will follow the pattern of adding 8 bytes explicitly for clarity.

For dynamic fields like String or Vec, you must provide a maximum length using the #[max_len] attribute for InitSpace to work correctly.

#[account]
#[derive(InitSpace)]
pub struct UserProfile {
    pub authority: Pubkey,
    #[max_len(50)] // Max 50 characters for the username
    pub username: String,
    #[max_len(280)] // Max 280 characters for the bio
    pub bio: String,
}
Test your understanding!

You are creating a BlogPost account for your blogging program. The struct needs to store the following information:

  • The public key of the author.
  • A title, which can be up to 100 characters long.
  • The content of the post, which can be up to 2000 characters long.
  1. Define the BlogPost struct using #[account] and derive InitSpace. Use the #[max_len] attribute for the dynamic fields.
  2. Manually calculate the total space needed for this account in bytes. Remember that a String requires 4 bytes for its length prefix.
Show answer
  1. Struct Definition:

    #[account]
    #[derive(InitSpace)]
    pub struct BlogPost {
        pub author: Pubkey,
        #[max_len(100)]
        pub title: String,
        #[max_len(2000)]
        pub content: String,
    }
    
  2. Manual Space Calculation:

    • Discriminator: 8 bytes
    • author (Pubkey): 32 bytes
    • title (String): 4 bytes (for length) + 100 bytes (for max content) = 104 bytes
    • content (String): 4 bytes (for length) + 2000 bytes (for max content) = 2004 bytes

    Total Space = 8 + 32 + 104 + 2004 = 2148 bytes

Conclusion

You have now mastered a fundamental piece of the Anchor puzzle. By defining custom data structures with #[account], you are creating the typed, secure, and self-describing state that your program will manage. This declarative approach allows you to focus on what your data is, leaving the low-level how of serialization and validation to the framework.

Key takeaways from this lesson:

  • The #[account] macro is used to define the schema for a program-owned data account.
  • It automatically handles serialization/deserialization and assigns a unique 8-byte discriminator for type safety.
  • In your Accounts struct, you access this data through the Account<'info, T> wrapper, which performs crucial ownership and discriminator checks.
  • You must pre-allocate space for accounts. This can be calculated manually (8 bytes for discriminator + size of fields) or automatically using #[derive(InitSpace)].

In our next lesson, we will build directly on this knowledge. You'll learn how to specify and validate instruction accounts using an Accounts struct. We will explore the powerful constraints like init, mut, signer, and has_one that you can apply within #[account(...)] to enforce the rules and relationships between accounts, which is the heart of Anchor's robust security model.

Can't find a good explanation? Sign up and we'll make it for you

Sign up