Create your own
Lesson illustration

Custom Discriminators with Anchor's `#[account(discriminator)]`

Hello! Welcome to the first lesson in our module on Advanced Anchor and Program Lifecycle.

In our previous work, we've defined various account structures using the #[account] macro to store program state. We've treated this as a bit of a magic wand that prepares our structs for on-chain use. Today, we'll look under the hood to understand one of the most critical security features this macro provides: the account discriminator.

Our goal is to understand how Anchor uses this mechanism to manage and differentiate between multiple account types within a single program. This is fundamental for building secure and robust applications that handle diverse data, such as user profiles, application configurations, and other custom states, without letting them get mixed up.

The Problem: On-Chain Type Confusion

On the Solana blockchain, all account data is fundamentally just a sequence of bytes. A single program can own thousands of accounts, and from the runtime's perspective, they're all just buffers of data.

This presents a challenge: if your program has an account for storing UserProfile data and another for AdminConfig data, how do you prevent an instruction designed to update a user's name from being accidentally (or maliciously) sent the AdminConfig account? Without a type-checking mechanism, the instruction might write user data into the configuration account, potentially corrupting it or leading to a security breach.

Your experience as a front-end developer gives you a good parallel here. In JavaScript, if you pass an object with a { name: 'Alice' } shape to a function expecting { configValue: 123 }, you might get a runtime error or unexpected behavior. In TypeScript, a static type checker would catch this before you even run the code. On-chain, we need a reliable runtime check, and that's exactly what discriminators provide.

What is an Account Discriminator?

Anchor's solution to this problem is the discriminator. It is a unique, 8-byte identifier that the #[account] macro automatically adds to the beginning of an account's data when it's created.

To understand the specifics, please read the following section from the official Anchor documentation. It explains what the #[account] macro does and introduces the concept of the discriminator.

Program Structure

This reading from the Anchor documentation explains the core functionalities of the #[account] attribute, with a special focus on the account discriminator.

Please read the sections titled '#[account] attribute' and 'Account Discriminator'. Focus on what the discriminator is, how it's generated, and where it's stored.

As you've just read, here are the key takeaways:

  • Automatic & Unique: The #[account] macro generates a discriminator for each account struct.
  • Generation: It's calculated by taking the first 8 bytes of the SHA-256 hash of the string account:<YourStructName>. For a struct named UserProfile, the input to the hash would be account:UserProfile. This ensures that UserProfile and AdminConfig have different, non-colliding discriminators.
  • Storage: The discriminator is stored as the first 8 bytes of the account's data buffer. This is why, when using #[account(init...)], we must always add 8 to our space calculation to reserve room for it.
Test your understanding!

You are creating a program to manage player data for a game. You define a struct PlayerStats to hold a player's score and health.

#[account]
pub struct PlayerStats {
    pub score: u64,
    pub health: u16,
}
  1. How is the discriminator for PlayerStats accounts generated?
  2. What is the minimum space required to initialize an account of this type?
Show answer
  1. The discriminator is the first 8 bytes of the SHA-256 hash of the string "account:PlayerStats".
  2. The space required is 8 (discriminator) + 8 (for u64 score) + 2 (for u16 health) = 18 bytes.

How Anchor Uses Discriminators for Security

Knowing that a discriminator exists is one thing; understanding how it protects your program is the crucial part.

Whenever your instruction context uses Account<'info, MyStruct>, Anchor performs an automatic check before it even attempts to deserialize the data. It:

  1. Reads the first 8 bytes from the data buffer of the account passed into the instruction.
  2. Compares these bytes with the known, pre-calculated discriminator for MyStruct.
  3. If they match, deserialization proceeds, and your instruction logic runs.
  4. If they do not match, Anchor immediately halts execution and returns an AccountDiscriminatorMismatch error. The transaction fails.

This simple, automated check is a powerful defense against a class of vulnerabilities known as "account confusion" or "type confusion" attacks.

The following article provides an excellent explanation of why this is so important, with a clear example.

Discriminator. What is a Discriminator? | by chungchan1701

This article provides a deeper look into the necessity of discriminators and illustrates their security role with a practical scenario.

Read the sections 'Discriminators in Anchor Account Processing' and the example that follows, which begins with 'Let’s illustrate the importance...'. Focus on how the discriminator check prevents an instruction from operating on the wrong account type.

The example in the article is perfect: even if AccountA and AccountB have the exact same data layout and are owned by the same program, Anchor can tell them apart because their names are different, resulting in different discriminators.

A Practical Example: A Multi-Account Blog Program

Let's solidify this with a concrete example. Imagine a simple blog program that has two distinct types of accounts:

  1. BlogState: A singleton account (one per program) that stores global state, like the total number of posts.
  2. Post: An account created for each blog post, storing its content and author.

Here is how we would define these account structs in lib.rs:

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod multi_account_blog {
    use super::*;

    // Instruction to initialize the blog
    pub fn init_blog(ctx: Context<InitBlog>) -> Result<()> {
        let blog_state = &mut ctx.accounts.blog_state;
        blog_state.authority = *ctx.accounts.authority.key;
        blog_state.post_count = 0;
        Ok(())
    }

    // Instruction to create a new post
    pub fn create_post(ctx: Context<CreatePost>, content: String) -> Result<()> {
        let post = &mut ctx.accounts.post;
        post.author = *ctx.accounts.author.key;
        post.content = content;
        post.timestamp = Clock::get()?.unix_timestamp;

        let blog_state = &mut ctx.accounts.blog_state;
        blog_state.post_count = blog_state.post_count.checked_add(1).unwrap();
        Ok(())
    }
}

// Account Structs
#[account]
pub struct BlogState {
    pub authority: Pubkey,
    pub post_count: u64,
}

#[account]
pub struct Post {
    pub author: Pubkey,
    pub timestamp: i64,
    pub content: String,
}

// Instruction Contexts
#[derive(Accounts)]
pub struct InitBlog<'info> {
    #[account(
        init,
        payer = authority,
        space = 8 + 32 + 8 // Discriminator + Pubkey + u64
    )]
    pub blog_state: Account<'info, BlogState>,
    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct CreatePost<'info> {
    #[account(mut)]
    pub blog_state: Account<'info, BlogState>,

    #[account(
        init,
        payer = author,
        // Space for discriminator, author, timestamp, and content (4 bytes for length + 50 chars)
        space = 8 + 32 + 8 + (4 + 50),
        seeds = [b"post", blog_state.post_count.to_le_bytes().as_ref()],
        bump
    )]
    pub post: Account<'info, Post>,

    #[account(mut)]
    pub author: Signer<'info>,
    pub system_program: Program<'info, System>,
}

In the CreatePost instruction, the context requires two of our custom accounts: blog_state (of type BlogState) and post (of type Post).

  • When the instruction is called, Anchor will check that the account passed for blog_state has the discriminator for BlogState.
  • It will also validate that the account being initialized as post has enough space for a Post struct (including its discriminator).

If a client were to accidentally pass a Post account into an instruction expecting the BlogState account, the transaction would fail immediately due to the discriminator mismatch. We get this robust, runtime type-safety for free, just by using #[account] and Account<'info, T>.

Conclusion

In this lesson, we've pulled back the curtain on one of Anchor's most important automated features.

Key Takeaways:

  • The #[account] macro automatically prepends a unique 8-byte discriminator to every account's data.
  • This discriminator is derived from the struct's name (e.g., hash("account:MyStruct")), ensuring different account types have different discriminators.
  • When you use Account<'info, MyStruct> in an instruction context, Anchor transparently validates that the provided account has the correct discriminator before proceeding.
  • This mechanism is a critical security feature that prevents account confusion attacks, where an instruction for one account type could be tricked into operating on another. It provides essential runtime type safety for your on-chain data.

Preview of the Next Lesson

We've now seen how Anchor helps us manage different types of accounts. But what happens when the structure of a single account type needs to change after the program is deployed? In our next lesson, we will explore versioning strategies for program state, a crucial topic for managing upgrades and data migrations in a live application.

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

Sign up